---
title: C Libraries
description: C bindings for Media over QUIC via libmoq
---

# C Libraries

The C bindings expose [Media over QUIC](/) to C and C++ applications. Built on top of the Rust [`moq-net`](/lib/rs/crate/moq-net) crate via FFI, with no Rust toolchain required at link time.

## Libraries

### libmoq

[![docs.rs](https://docs.rs/libmoq/badge.svg)](https://docs.rs/libmoq)

A C-callable shared and static library exposing the MoQ pub/sub API. Header files are generated by `cbindgen` and ship alongside the prebuilt binaries.

**Features:**

- Static (`libmoq.a`) and dynamic (`libmoq.so` / `libmoq.dylib` / `moq.dll`) library targets
- Auto-generated C header (`moq.h`)
- No Rust runtime exposed to consumers
- Works with any toolchain that can link a C library (CMake, Make, Meson, etc.)

[Learn more](/lib/rs/crate/libmoq)

## Installation

### From prebuilt releases

Each [`libmoq-v*` release](https://github.com/moq-dev/moq/releases) ships a
`moq-<version>-<target>.tar.gz` bundle with the static library, the dynamic
library, and the generated header. Supported targets:

- `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`
- `aarch64-apple-darwin`

Download and extract a bundle (here `0.2.0` on Linux x86\_64):

```bash
ver=0.2.0
target=x86_64-unknown-linux-gnu
curl -fsSL "https://github.com/moq-dev/moq/releases/download/libmoq-v$ver/moq-$ver-$target.tar.gz" \
  | tar xz
```

That gives you a `moq-$ver-$target/` directory containing `include/moq.h`
and `lib/` (`libmoq.a` plus the dynamic library).

### Compile against it

`libmoq.a` is a static library, so your linker also needs the system libraries it
depends on (media frameworks, the C++ runtime, and so on). The bundle ships a
pkg-config file that lists them, which saves you tracking the set by hand:

```bash
root=moq-$ver-$target
export PKG_CONFIG_PATH="$root/lib/pkgconfig"

cc subscribe.c $(pkg-config --cflags --libs --static moq) -o subscribe
```

Without pkg-config, pass the same flags yourself:

```bash
pkg-config --libs --static moq   # prints exactly what to add
```

The static library (`libmoq.a`) links the whole Rust runtime in, so the result
has no libmoq runtime dependency. To link the dynamic library instead, keep
`lib/` on your loader path (`LD_LIBRARY_PATH` on Linux, `DYLD_LIBRARY_PATH` on
macOS) at runtime.

### From source

If there's no prebuilt bundle for your target, build it yourself. You'll need a
[Rust toolchain](https://rustup.rs/):

```bash
git clone https://github.com/moq-dev/moq
cd moq/rs/libmoq
cargo build --release
```

`libmoq` is part of the Cargo workspace, so the build emits to the workspace
root `target/` (two levels up from `rs/libmoq/`): `../../target/release/libmoq.a`
(static) and `../../target/release/libmoq.{so,dylib,dll}` (dynamic), with the
generated header at `../../target/release/moq.h`.

## Client configuration

`moq_session_connect` dials with the defaults. To pin a protocol version, adjust TLS trust, or tune the transport, build a client config and dial with `moq_client_connect` instead:

```c
int client = moq_client_create();
if (client < 0)
    return client;

int result;

// Pin the handshake to one draft instead of offering every supported version.
struct moq_string version = { "moq-lite-05", 11 };
result = moq_client_set_versions(client, &version, 1);
if (result < 0) {
    moq_client_close(client);
    return result;
}

// Trust one self-signed relay without accepting every certificate.
struct moq_string fingerprint = { hex_sha256, strlen(hex_sha256) };
result = moq_client_set_tls_fingerprints(client, &fingerprint, 1);
if (result < 0) {
    moq_client_close(client);
    return result;
}

result = moq_client_set_quic_congestion_control(client, "delay", 5);
if (result < 0) {
    moq_client_close(client);
    return result;
}

int session = moq_client_connect(url, url_len, client, origin, 0, on_status, user_data);

// The config is copied into the session, so the handle can be released (or reused
// for another dial, or edited in between) right away.
moq_client_close(client);
if (session < 0)
    return session;
```

A `client` of `0` means the defaults, so `moq_client_connect(url, len, 0, ...)` is exactly `moq_session_connect`.

The setters fall into a few groups:

- **Protocol**: `moq_client_set_versions` restricts what's offered during the handshake. Names are spelled the way the CLI spells them (`moq-lite-05`, `moq-transport-19`); `moq_versions` lists what this build offers so a menu can't drift from what the setter accepts. An empty list restores the default of offering everything.
- **TLS**: `moq_client_set_tls_fingerprints` (pin a SHA-256 hex fingerprint, the native equivalent of the browser's `serverCertificateHashes`), `moq_client_set_tls_roots`, `moq_client_set_tls_system_roots`, `moq_client_set_tls_host_name` (SNI override), `moq_client_set_tls_cert` / `moq_client_set_tls_key` (mTLS), and `moq_client_set_tls_disable_verify` (development only: it accepts any certificate, so prefer a fingerprint).
- **Transport**: `moq_client_set_backend`, `moq_client_set_bind`, `moq_client_set_connect_timeout`, `moq_client_set_failover_delay`, and `moq_client_set_websocket_enabled` / `moq_client_set_websocket_delay` for the fallback that gets you through a UDP-blocked network. The QUIC backends are compile-time optional, so use `moq_backends` for the names this build actually accepts rather than assuming all three.
- **Tuning**: `moq_client_set_backoff_initial` / `_multiplier` / `_max` / `_timeout` for reconnect pacing, and `moq_client_set_quic_max_streams` / `_idle_timeout` / `_keep_alive` / `_gso` / `_mtu_discovery` / `_congestion_control` / `_qlog` for the transport. The QUIC ones are ignored by the WebSocket fallback.

Every knob is its own setter, and a knob you never set stays at its default. That is deliberate rather than incidental: the C ABI is stable, so a new knob has to be a new function. Bundling several into one `struct` you pass by pointer would freeze their layout, and adding a field later would break every caller compiled against the old size. The optional strings (`congestion_control`, `qlog`, the TLS paths) are set-or-clear, where NULL or empty puts the knob back to automatic, so a setting can be undone without rebuilding the handle.

Setters for reportable knobs have a matching `moq_client_get_*`, and a knob you never set reads back as its default:

```c
uint32_t client = moq_client_create();
uint64_t idle;
moq_client_get_quic_idle_timeout(client, &idle);   // 30000
```

So a fresh handle *is* the defaults, and a settings UI can read them instead of hardcoding numbers that go stale when a default is retuned. Same reason `moq_versions` exists for the version menu. The getters take an out-parameter and return zero or a negative code, since every value is a valid one and the return channel is for errors.

The knobs whose default depends on the backend (GSO, path MTU discovery, congestion control, the TLS root store) have no getter: there is no single value to report, and they stay on the backend's choice until you set them.

Two capabilities are compile-time optional, so ask before offering them:

- `moq_backends` lists the QUIC backends this build has, the same way `moq_versions` lists the drafts. A name it doesn't report is rejected by `moq_client_set_backend`.
- `moq_qlog_supported` reports whether traces can be captured. `moq_client_set_quic_qlog` stores a directory either way, but a non-empty directory is rejected while the client configuration is initialized, before a connection is created.

Combinations that would quietly drop a setting are rejected at dial rather than resolved by precedence. `moq_client_set_tls_disable_verify` accepts every certificate, so pairing it with a fingerprint or a root fails instead of ignoring the trust material, and the reconnect knobs must leave a non-zero delay or retrying would spin.

Setters validate what they can parse, so a typo'd version or an unparseable bind address fails at the setter with the reason in `moq_error()`, rather than being silently dropped at connect time. A value that parses but the transport can't express (an idle timeout past QUIC's millisecond varint, say) is caught when the connection is dialed instead. Either way `moq_error()` names it.

## Callback lifetime

Any function that registers a callback (`moq_session_connect`, `moq_origin_announced`, `moq_origin_consume_announced`, `moq_origin_request`, `moq_consume_catalog`, `moq_consume_video`, `moq_consume_audio`, `moq_consume_track`, `moq_consume_datagrams`, `moq_consume_video_raw`, `moq_consume_audio_raw`, `moq_consume_json_snapshot`, `moq_consume_json_stream`) takes a `void *user_data` pointer that libmoq passes back to every callback invocation. The status code carries the lifecycle:

- **`> 0`**: a live result you can use: a frame, catalog, or announce ID (or `1` to mean "session connected"). May fire any number of times.
- **`0`**: closed cleanly. **Terminal.**
- **`< 0`**: closed with an error. **Terminal.**

A positive result that is itself a handle must be freed once you're done with it (e.g. a broadcast from `moq_origin_request` via `moq_consume_close`). `moq_origin_announced` is the notable repeat case: it delivers a fresh announce ID for *every* announce / unannounce event, so free each one with `moq_origin_announced_free` after reading it with `moq_origin_announced_info`, or they accumulate for the life of the listener.

Once a callback fires with any non-positive (`<= 0`) code, libmoq will never invoke it again and never touch `user_data` again. Release `user_data` in response to that final callback.

The matching `*_close` function only *requests* shutdown: it returns immediately, does **not** free `user_data`, and does **not** cancel the final callback. The terminal callback still fires (on libmoq's internal thread) once the background task stops, and that is the one safe point to free `user_data`. This means you never have to guess whether an in-flight callback is still running after `close`, and you don't need an external weak-reference or refcount around `user_data`.

Because the terminal callback runs on libmoq's thread, bindings that own thread-affine objects (e.g. a Qt `QObject`) should hop to the owning thread to perform the actual destruction; the `user_data` lifetime contract holds regardless of which thread tears the object down.

## Threading

Every function may be called from any thread, and a handle is not tied to the thread that created it.

The raw publish functions (`moq_publish_video_raw_frame`, `moq_publish_audio_raw_frame`) block the caller until the codec has taken the frame, which is what paces a publisher against its encoder. Calls on the same producer are serialized, but concurrent calls have no defined order, so use one thread when frame, cut, or bitrate order matters. A second producer keeps encoding, and consume callbacks, frees, and shutdown are unaffected.

## Error handling

Functions return a negative code on failure (`0` or a positive handle on success). The code identifies the kind of failure, but the human-readable reason is available separately via `moq_error()`:

```c
int rc = moq_consume_catalog_close(catalog);
if (rc < 0) {
    fprintf(stderr, "close failed: %s\n", moq_error());
}
```

`moq_error()` returns the reason for the most recent failed call **on the calling thread**, including detail the numeric code can't carry (which URL failed to parse, why a decode failed, etc.). The returned pointer is valid until the next libmoq call on that thread, so copy it if you need to keep it. It is only meaningful after a call returned a negative code; check the code first. Errors delivered through status callbacks carry their code directly, so read `moq_error()` from inside the callback if you want the matching reason.

A server can reject the connection on auth grounds: unauthorized (HTTP 401) or forbidden (HTTP 403). Each returns its own distinct negative code (with `moq_error()` reporting `"unauthorized"` / `"forbidden"`). These are terminal, so distinguish them from a transient transport failure and stop rather than reconnecting.

Failed calls are reported only through the return code and `moq_error()`, not logged. To surface libmoq's internal logs (moq-net / QUIC activity), call `moq_log_level("debug")` (or `"trace"`, `"info"`, etc.) to install a tracing subscriber.

## Shared video properties

`moq_publish_video_properties` replaces the catalog properties shared by every video rendition in one update. A false `has_*` flag clears that property, and rotation is normalized to the nearest clockwise quarter turn. Read the same snapshot with `moq_consume_video_properties`:

```c
moq_video_properties properties = {
    .display_width = 1080,
    .display_height = 1920,
    .has_display = true,
    .rotation = 90.0,
    .has_rotation = true,
    .flip = false,
    .has_flip = true,
};

if (moq_publish_video_properties(broadcast, &properties) < 0) {
    fprintf(stderr, "video properties failed: %s\n", moq_error());
}

moq_video_properties snapshot = {0};
if (moq_consume_video_properties(catalog, &snapshot) < 0) {
    fprintf(stderr, "video properties failed: %s\n", moq_error());
}
```

## Raw media

The `moq_publish_media_*` and `moq_consume_video` / `moq_consume_audio` calls carry already-encoded frames, for a caller that brings its own codec. The `_raw` calls carry uncompressed media instead and run the codec inside libmoq, so a C application can publish pixels and PCM without linking one.

`moq_publish_video_raw` opens an encoder and a video track together. Resolution, framerate, and pixel layout are fixed there, so each `moq_video_encoder_frame` carries only pixels and a timestamp:

```c
struct moq_video_encoder_input input = {
    .format = MOQ_VIDEO_PIXEL_FORMAT_RGBA,
    .width = 1280,
    .height = 720,
    .framerate = 30,
};
struct moq_video_encoder_output output = {
    .codec = MOQ_VIDEO_CODEC_H264,
    .kind = MOQ_VIDEO_ENCODER_KIND_AUTO,  // hardware if available, software otherwise
};

int32_t video = moq_publish_video_raw(broadcast, &input, &output);
if (video < 0) {
    fprintf(stderr, "publish video failed: %s\n", moq_error());
}

struct moq_video_encoder_frame frame = {
    .timestamp_us = pts_us,
    .data = rgba,
    .data_size = 1280 * 720 * 4,
};
moq_publish_video_raw_frame(video, &frame);
```

Every zero in the output config means "pick a default": `bitrate` derives one from the resolution and framerate, `gop` uses roughly two seconds. `data` is borrowed only for the call, and must be exactly one picture at the configured resolution: the frame carries no dimensions of its own, so a wrong-sized buffer is rejected rather than reinterpreted. (The decode side's `moq_video_frame` does carry dimensions, since there they are whatever the stream turned out to be.) A hardware encoder pipelines, so a call that puts nothing on the wire is normal rather than an error.

The track is named after the codec (`.avc3` / `.hev1`), and its catalog rendition appears once the first keyframe has been encoded, which is where the resolution and codec string come from. So a subscriber discovers the track through the catalog rather than a name you chose.

Two knobs run the live encoder. `moq_publish_video_raw_bitrate` retunes it without forcing a keyframe, which is cheap enough to drive from a congestion controller; a negative return means this backend can't retune while running, so stop adapting rather than stop publishing. `moq_publish_video_raw_cut` starts a new group at the next frame, which is optional: the encoder keyframes every `gop` frames on its own, and each of those cuts a group, so a subscriber can always join without it. Reach for it only when you want to place the boundaries yourself, aligning groups with something the encoder can't see such as a scene change or a source switch. Call `moq_publish_video_raw_finish` to flush the codec and end the track.

`moq_publish_audio_raw` is the same shape for PCM in and Opus out, and `moq_consume_video_raw` / `moq_consume_audio_raw` are the decode-side mirrors, delivering I420 frames and PCM through the usual callback contract.

## Raw Tracks

Raw tracks carry arbitrary byte payloads without catalog or codec parsing. Use
`moq_publish_track_frame` / `moq_publish_group_frame` to provide presentation
timestamps in microseconds.
libmoq creates raw tracks with a microsecond timescale by default (used when
`moq_track_info.timescale_valid` is false or no info is given), matching the C
ABI's timestamp units.

Use `moq_publish_track_group_at` to create sparse or replayed groups at an
explicit sequence. `moq_publish_track_finish_at` declares the exclusive end
while still permitting lower groups. `moq_publish_track_abort` and
`moq_publish_group_abort` terminate a producer with an application error. Call
`moq_publish_track_finish` after filling the groups below a declared end.

Subscribers receive raw frame handles from `moq_consume_track`; read each one
with `moq_consume_track_frame`. The returned `moq_frame.timestamp_us` carries
the timestamp, and `keyframe` is always false because raw tracks do not parse
codec metadata.

## Raw Track Options

`moq_publish_track` accepts optional publisher-side track properties:
`ordered` controls prioritization only. When true, groups are prioritized in
sequence order. Groups may always arrive out-of-order (or not at all) over the
network.

```c
struct moq_track_info info = {0};
info.priority = 3;
info.ordered = true;
info.latency_max_ms = 1000;
info.latency_max_valid = true;
info.timescale = 1000000;
info.timescale_valid = true;

int track = moq_publish_track(
    broadcast,
    name,
    name_len,
    &info);
```

`moq_consume_track` accepts optional subscriber delivery preferences.
`moq_consume_track_update` changes them while the callback task is running.
Fields ending in `_valid` decide whether the matching optional value is present:

```c
struct moq_subscription sub = {0};
sub.priority = 5;
sub.ordered = true;
sub.latency_max_ms = 25;
sub.group_start = 10;
sub.group_start_valid = true;

int consumer = moq_consume_track(
    broadcast,
    name,
    name_len,
    &sub,
    on_frame,
    user_data);

sub.group_end = 20;
sub.group_end_valid = true;
moq_consume_track_update(consumer, &sub);
```

Pass `NULL` for either options pointer to use the moq-net defaults.

## JSON tracks

For JSON payloads, libmoq frames the values for you. You opt into one of two modes. Snapshot (lossy) carries one value updated over time; a subscriber only sees the latest, via `moq_publish_json_snapshot` / `moq_consume_json_snapshot`. Stream (lossless) is an ordered append-log where every record is preserved, via `moq_publish_json_stream` / `moq_consume_json_stream`. Values are UTF-8 JSON documents.

```c
struct moq_json_snapshot_config config = { .delta_ratio = 8, .compression = true };
int32_t json = moq_publish_json_snapshot(broadcast, "status", strlen("status"), &config);
const char *value = "{\"state\":\"live\"}";
moq_publish_json_snapshot_update(json, value, strlen(value));

// Subscribe: on_value fires with a value ID for each update; read it, then release it.
int32_t task = moq_consume_json_snapshot(consume, "status", strlen("status"), &config, on_value, user_data);
// In on_value: struct moq_json_value v; moq_consume_json_value(id, &v); ... moq_consume_json_value_free(id);
```

`compression` must match on the producer and subscriber. The consumer callback follows the same lifetime contract as every other (see above): release `user_data` on the terminal `<= 0` call.

## Use cases

- **C/C++ applications** integrating MoQ without a Rust toolchain
- **Bindings for other languages** that aren't already covered by [Python](/lib/py/), [Kotlin](/lib/kt/), [Swift](/lib/swift/), or [Go](/lib/go/)
- **Legacy systems** and **embedded** targets where pulling in Rust at build time is impractical

## Source and issues

- Source: [rs/libmoq/](https://github.com/moq-dev/moq/tree/main/rs/libmoq)
- API reference: [docs.rs/libmoq](https://docs.rs/libmoq)
