/* joltmoq — freeq's AV media plane, for callers that are not Rust. * * A freeq call has two halves. *Signaling* rides IRC TAGMSGs * (`+freeq.at/av-start`, `av-join`, `av-leave`, and the server's `av-state` * broadcasts back), and is not here: a client that speaks IRC already has * everything it needs for that in whatever language it speaks IRC in. *Media* * rides MoQ — Media over QUIC — through the freeq SFU, and is what this is. * * The boundary begins once the session id and the SFU token are known, and * ends when the call does. * * Rules for every function below: * * - Only integers, doubles, borrowed UTF-8 strings and one borrowed pixel * pointer cross. Nothing here retains caller memory past a call, and * nothing hands out memory the caller must free. * - A returned string is valid until the next call to the same function. * Copy it if you need to keep it. * - An empty string argument means "no preference" — the system default. * - Booleans cross as 0 or 1. * - Panics are caught at the boundary, so a bug in a decoder is a black tile * rather than a dead process. * - Nothing calls back. Everything asynchronous is polled. * - One call at a time. There is one microphone, so there is one session. */ #ifndef JOLTMOQ_H #define JOLTMOQ_H #ifdef __cplusplus extern "C" { #endif /* Logging to stderr, honouring RUST_LOG. Safe to call twice. Worth calling * first while a call refuses to connect: the media plane says a great deal * about why, and says none of it otherwise. */ void joltmoq_init_logging(void); /* Android only. Hand over the process's JavaVM and its Activity, once, before * any call starts. * * The camera on the phone is Camera2, which is Java, and this library cannot * reach the handles a JNI call needs: android-activity's glue receives them and * the glue lives in libvidya.so, a different shared object. `ndk_context` does * not bridge that — its handles sit in a static, and a static is per object — * so the APK's glue reads them out of libvidya (vidya_android_vm, * vidya_android_activity) and passes them here. * * Everything else works without it. Only the camera calls fail, and they say * which call was missed. Not declared on other platforms, where there is * nothing to hand over. */ #ifdef __ANDROID__ void joltmoq_android_init(void *vm, void *activity); #endif /* Lifecycle * * `joltmoq_start` joins the call at `sfu_url` (see joltmoq_sfu_url) as `nick`. * `session_id` is what the server broadcast in `+freeq.at/av-id`; `instance` * is the per-device id from your own `av-join` — two devices signed in as the * same person need different ones, or their broadcast paths collide and each * unpublishes the other. * * Returns 1 when the media task started. That is not the same as connected: * poll the status for that. Starting a second call while one is live is * refused. * * `joltmoq_stop` waits briefly for MoQ to be torn down rather than aborting. * An abandoned broadcast lingers on the SFU, and peers subscribe to it and * hear silence from someone who looks present. */ int joltmoq_start(const char *sfu_url, const char *session_id, const char *nick, const char *instance, int muted, int speaker_muted, int camera, const char *camera_id, const char *mic_id, const char *speaker_id); void joltmoq_stop(void); int joltmoq_is_live(void); /* Controls * * Mic mute and speaker mute are deliberately separate: muting the speaker * (deafening) leaves peers hearing you if the mic is open. The camera device * is only held while publishing, so turning the camera off gives the hardware * back to the rest of the machine. * * A control with no call under it is inert, not an error — a UI that sends a * mute as the call is ending must not have to race it. * * `joltmoq_mic_level` is the live envelope, 0.0 to 1.0: what a level meter * draws. 0 when no call is up. */ void joltmoq_set_muted(int muted); void joltmoq_set_speaker_muted(int muted); void joltmoq_set_camera(int enabled); void joltmoq_set_camera_device(const char *id); void joltmoq_set_mic_device(const char *id); void joltmoq_set_speaker_device(const char *id); double joltmoq_mic_level(void); /* Status * * The media task runs on its own threads; this is how what it learns reaches * yours. Drain `joltmoq_poll_status` until it answers NONE wherever you poll — * beside a repaint is the natural place. Missing an update means a call that is * up still looks like it is connecting. * * The accessors describe the most recently dequeued status. `has_camera` is * false when there is no capture device for this call at all, so a camera * control has nothing to offer; `has_mic` is false for listen-only, where * audio is still published, as silence. */ #define JOLTMOQ_STATUS_NONE 0 #define JOLTMOQ_STATUS_LIVE 1 #define JOLTMOQ_STATUS_ENDED 2 #define JOLTMOQ_STATUS_FAILED 3 int joltmoq_poll_status(void); const char *joltmoq_status_text(void); /* why it failed; "" otherwise */ int joltmoq_status_has_camera(void); int joltmoq_status_has_mic(void); /* Video * * Loop `joltmoq_frame_poll` until it answers 0, reading the four accessors for * each frame it hands over and passing them to whatever paints. Only what * changed comes over: a participant sitting still costs nothing, and a decoder * running ahead of the window is coalesced to its newest frame rather than * queued behind stale ones. * * The key is the participant's nick, or "__local__" for the self-view. * * `joltmoq_frame_rgba` is width * height * 4 bytes, row-major, 8 bits a * channel, un-premultiplied and opaque. It is BORROWED, and only until the * next `joltmoq_frame_poll` — that is the point of polling rather than * copying: a frame is a megabyte or two, and copying it out so you can hand it * straight to a texture upload would be two copies a frame for nothing. * * `joltmoq_video_keys` is every feed the call currently carries, one a line. * Use it to notice a tile you should stop painting: someone who left stops * appearing, while their last frame would otherwise hang on the wall for the * rest of the call. */ int joltmoq_frame_poll(void); const char *joltmoq_frame_key(void); int joltmoq_frame_width(void); int joltmoq_frame_height(void); const unsigned char *joltmoq_frame_rgba(void); const char *joltmoq_video_keys(void); /* Devices * * One a line, each `idnamedefault`, where the last column is 1 for * the system default. Tab and newline delimit because a device name may hold * anything else — "EMEET SmartCam C960, Mono" has spaces and a comma in it. * * Enumerating opens nothing, so it is safe before a call and during one. Empty * is a normal answer on a machine with no such device. */ const char *joltmoq_cameras(void); const char *joltmoq_microphones(void); const char *joltmoq_speakers(void); /* Dialling * * `joltmoq_sfu_url` builds what `joltmoq_start` wants from whatever your * client knows the IRC server as — "irc.freeq.at:6697" and * "wss://irc.freeq.at/irc" both become "https://irc.freeq.at/av/moq" — plus * the JWT the server minted in `+freeq.at/av-token`. Empty string when the * server is not something a URL can be made of. * * `joltmoq_can_dial` answers whether the attempt is worth making. A remote SFU * with no token accepts the connection and closes it, and the MoQ client then * retries in a tight loop that looks, from the outside, exactly like a hang. * * `joltmoq_new_instance` is eight hex characters: the per-device id that keeps * two of your own devices from colliding on the SFU. Put it in your * `+freeq.at/av-instance` tag and hand the same one to `joltmoq_start`. */ const char *joltmoq_sfu_url(const char *server, const char *jwt, const char *instance); int joltmoq_can_dial(const char *server, const char *jwt); const char *joltmoq_new_instance(void); #ifdef __cplusplus } #endif #endif /* JOLTMOQ_H */