C
moq-c exposes MoQ to C, C++, and any language with a C FFI through a stable ABI: a generated moq.h, a static libmoq.a that links the whole Rust runtime in, and a pkg-config file for its native link dependencies. The OBS plugin is built on it. It was published as libmoq through 0.6; the header and library file names did not change.
Install
Each moq-c-v* release ships moq-c-<version>-<target>.tar.gz with include/moq.h, lib/libmoq.a, and a pkg-config file listing the system libraries the archive needs. They are a matched pair: compile against the header that came with the archive you link. Targets: Linux x86_64 and aarch64, macOS arm64, Windows x64.
export PKG_CONFIG_PATH="moq-c-$ver-$target/lib/pkgconfig"
cc app.c $(pkg-config --cflags --libs --static moq-c) -o appWith CMake, point CMAKE_PREFIX_PATH at the extracted archive, then find_package(moq-c) and link moq::c.
From source, add_subdirectory(rs/moq-c) in CMake gives the same moq::c target. A bare cargo build --release -p moq-c writes target/release/libmoq.a, and moq.h lands in the build script's OUT_DIR under include/, a hashed path that --message-format=json reports as out_dir. It writes no moq-c.pc; nix build .#moq-c produces the same install layout as the release tarball, pkg-config file included.
Shape of the API
- Handles and callbacks. Every object is an integer handle; every async result arrives on a required
moq_status_callbackwith avoid *user_data. A status> 0is a live result,0a clean close,< 0an error, and the last two are terminal: moq-c never touchesuser_dataagain, so free it there.*_cancelonly requests shutdown; the terminal callback still fires. - Errors. Negative return codes named by
MOQ_ERROR_*inmoq.h, withmoq_error()giving the reason for the last failure on the calling thread. Auth rejections (401, 403) have their own codes so you don't retry them. A protocol failure also fillsmoq_error_protocol()with the session or stream scope, the verbatim wire code, and a known kind; do not parsemoq_error()for that. - Threading. Any function from any thread. Raw publish calls block until the codec takes the frame, which paces a publisher.
- Connection health.
moq_session_stats()reports available metrics with per-field validity flags.moq_session_snapshot()samples those metrics and the negotiated draft name together from the same connection. Its protocol string is backed by static storage. Both return an offline error between reconnects and leave the destination untouched.moq_session_bandwidth()mints an allocator over the send estimate;moq_bandwidth_reserveclaims a share for an app-owned track, andmoq_encode_video/moq_encode_audiotake the same handle so the built-in video encoder follows the grant. - Raw playback. Raw audio and video consumers start at the newest cached group when opened, so rebuilding a live decoder skips the retained backlog.
- Raw audio encode.
moq_audio_encoder_output.codecnames the codec:"opus","pcm", or"aac".frame_duration_ussets the Opus frame length: 2500, 5000, 10000, 20000, 40000, or 60000, with 0 meaning the codec's default (20 ms for Opus, 1024 samples for AAC). AAC-LC encodes through the platform's encoder, so a host without one refuses it. - Audio channel layouts. A
channelscount also names the speaker layout, by the WAVE convention: 1 is mono, 2 stereo, 3 2.1, 4 quad, 5 5.0, 6 5.1, 7 6.1, and 8 7.1, interleaved front left, front right, center, LFE, back, then side.moq_decode_audioremixes to the count you ask for; past 8 channels the samples pass through but can't be remixed. - Raw decode output.
moq_video_decoder_outputselects the decoded CPU pixel format (MOQ_VIDEO_PIXEL_FORMAT_I420or_RGBA) and target size (width/height, both zero for native; otherwise even and non-zero). Unknown formats and invalid sizes failmoq_decode_videobefore subscribing. - Decoded frames. Each frame id owns its decoded picture until
moq_decode_video_frame_free, including aftermoq_decode_video_canceland the terminal callback. The firstmoq_decode_video_framecall converts it to the requested layout and size; itsdatapointer then stays put until the id is freed. A conversion failure returns a negative code for that frame only. Free frames promptly: held frames hold decoder buffers, and a decoder whose buffers are all held stalls. - Encoded video metadata.
moq_video_init.hintis a zero-initializedmoq_video_hintwithhas_*flags for coded dimensions, bitrate (bits per second), frame rate, and latency preference. Hints seed a video codec track's catalog; detected dimensions take precedence. - Client config. A zeroed
moq_client_configmeans the defaults for every knob, which is what lets a new one be appended without disturbing callers. Fields cover protocol (versions), TLS (tls_fingerprints,tls_roots,tls_cert/_key,tls_host_name), transport (bind,connect_timeout_us,failover_delay_us/resolution_delay_usfor Happy Eyeballs,websocket_enabled/_delay_us), and tuning (reconnect backoff,quic_*). Every duration is in microseconds. A knob whose default isn't zero carries ahas_*flag, so settingbackoff_timeout_us = 0needshas_backoff_timeout = trueto mean "retry forever" rather than "use the default".moq_client_defaults()reports what a NULL config dials with. - Server.
moq_server_listenbinds before it returns (a bad address or certificate fails there) and hands each incoming session toon_requestas a request handle. Readmoq_session_request_pathand_queryto route and authenticate, thenmoq_session_request_accept(a session handle, with origins likemoq_session_connect) ormoq_session_request_rejectwith an HTTP-style code (401 and 403 become the protocol's unauthorized close). An accepted session reports1once SETUP completes and never reconnects.moq_server_addrreports an ephemeral port andmoq_server_fingerprintsthe hashes a client pins for atls_generatecertificate.moq_server_closestops listening; its terminal callback fires once the sockets are released. - Demand. A watcher on a published track (
moq_publish_track_demand,moq_publish_media_demand,moq_encode_video_demand,moq_encode_audio_demand) callson_demandwithMOQ_DEMAND_USEDorMOQ_DEMAND_UNUSEDright away and again on every change, so an encoder on a battery-powered device runs only while someone is watching. The first call is the current state, so a track that went unused before the watcher existed still reports it.moq_publish_demand_cancelstops it; the terminal callback still fires. A container has no single demand and is refused. Demand follows the last real subscriber: an origin that served the track drops its source copy on the unused edge, so a cache linger never delays it. Only a relay keeps what it already delivered warm for 30 seconds, and a returning subscriber is served that cache only once the publisher confirms it is still current. - Requests.
moq_publish_dynamicserves subscriptions to tracks the broadcast never declared: each arrives as a request handle, read its name withmoq_track_request_name, thenmoq_track_request_accept(a raw track handle),moq_track_request_video/_audio(the media handlemoq_publish_video/_audioreturn), ormoq_track_request_abortwith an application code the subscriber sees. Without a live handler an unknown name is refused.moq_publish_track_dynamicdoes the same for fetches of groups a track no longer has cached, delivered asmoq_group_request_*(sequence,priority,frame_start);moq_group_request_acceptstarts the producer atframe_startso written frames keep their group indices. Register it withmoq_track_request_dynamicbefore accepting a track that was itself requested by a fetch, so that pending group survives the transition. Both handlers stop withmoq_publish_dynamic_cancel. - Everything the bindings can do (list): media publish and consume with the catalog managed for you, raw pixels and PCM with the codec inside (
moq_encode_video,moq_encode_audio, and themoq_decode_*mirrors), raw tracks with timestamps and datagrams, JSON and binary data tracks (snapshot or stream, each advertised in the catalog for as long as it lives), group fetch, catalog sections, shared video properties, and stalled hints. The three advertising operations aremoq_origin_create_broadcast(unannounced producer, invisible to everyone),moq_publish_announce/moq_publish_unannounce(exact-path advertisement),moq_publish_close(ends the broadcast for good and releases its handle), andmoq_origin_dynamic(a claim over a path prefix and everything beneath it;""for everything). A route is a capability, not an inventory.moq_origin_announcedtakes amoq_announce_config(NULL or zeroed for everything): a literal prefix and an optional relative pattern filter; eachmoq_announce_eventhas akind(MOQ_ANNOUNCE_KIND_START,_UPDATE,_END, or_LIVEonce every route live when the listener started has been delivered, with no prefix or captures);prefixstays relative to the origin, whilecapturesreports what each wildcard matched whenhas_capturesis true. Paths with a.-prefixed segment below the prefix are hidden; sethiddenor name the dot segment inprefixto list them.
moq_client_config config;
memset(&config, 0, sizeof(config)); // zeroed means "the defaults", every field
moq_string fp = { hex_sha256, strlen(hex_sha256) };
config.tls_fingerprints = &fp; // trust one self-signed relay
config.tls_fingerprints_len = 1;
int session = moq_session_connect(url, url_len, &config, origin, 0, on_status, user_data);
if (session < 0)
return fail(moq_error());For a locally encoded media track, call moq_publish_media_flush(media, timestamp_us) after moq_publish_media_frame with the same broadcast-clock PTS. The monotonic handoff time is sampled inside moq-c. Do not call it for file, pipe, or network imports; those remain clock-free. Invalid handles and unrepresentable timestamps return a negative error code.
Call moq_publish_media_discontinuity(media) when the source seeks, pauses, or changes its time base. It publishes a timeline marker and restarts handoff measurement without lowering advertised jitter. Resume with timestamps that continue forward on the broadcast media clock; this does not permit timestamp rewinds. On a video track, resume with a keyframe: a delta frame before it fails.
Connection stats
Every field in moq_connection_stats carries a matching <field>_valid flag, false when the transport backend does not report it (a false flag is not the same as zero). Native QUIC reports every metric; the browser WebTransport reports few or none.
| Field | Unit | Meaning |
|---|---|---|
rtt_us | microseconds | Smoothed round-trip time. |
estimated_send_rate_bps | bits per second | Send bandwidth from the congestion controller. |
estimated_recv_rate_bps | bits per second | Receive bandwidth from MoQ PROBE. |
bytes_sent | bytes | Total sent, including retransmissions and overhead. |
bytes_received | bytes | Total received, including duplicates and overhead. |
bytes_lost | bytes | Total lost, detected via retransmission or acknowledgement. |
packets_sent | datagrams | Total datagrams sent. |
packets_received | datagrams | Total datagrams received. |
packets_lost | datagrams | Total datagrams detected as lost. |
The header is the reference; each function carries a doc comment. Source and a worked example: rs/moq-c, API docs on docs.rs/moq-c.
Raw track publisher metadata has an optional maximum age. Omitting it imposes no publisher age limit; zero keeps the live edge. Local cache limits still apply, and media imports explicitly retain 30 seconds. See publisher retention.