Native
Build native MoQ clients in Rust for desktop, mobile, and embedded platforms. This guide covers connecting to a relay, discovering broadcasts, subscribing to media tracks, and decoding frames.
Dependencies
The key crates:
- moq-native: Configures QUIC (via quinn by default, with noq available through the
noqfeature) and TLS (via rustls) for you. - moq-net — The core networking layer. Can be used directly with any
web_transport_trait::Sessionimplementation if you need full control over the QUIC endpoint. - hang — Media-specific catalog and container format on top of
moq-net.
Connecting
Create a ClientConfig and connect to a relay:
let client = moq_native::ClientConfig::default().init()?;
let url = url::Url::parse("https://cdn.moq.dev/anon/my-broadcast")?;
let session = client.connect(url).await?;The default configuration uses system TLS roots, enables WebSocket fallback, and gives QUIC a 200ms head-start.
URL Schemes
The client supports several URL schemes:
https://— WebTransport over HTTP/3 (recommended for browsers and native)http://— Local development with self-signed certs (fetches the certificate fingerprint automatically)moqt://— Raw QUIC with the MoQ IETF ALPN (no WebTransport overhead)moql://— Raw QUIC with the moq-lite ALPN
The URL path and query mean the same thing on every scheme. Raw QUIC has no request URI to put them in, so the client sends them in the MoQ SETUP instead; the server sees the same request path either way.
Transport Racing
client.connect() automatically races QUIC and WebSocket connections. QUIC gets a configurable head-start (default 200ms); if it fails, WebSocket takes over. Once WebSocket wins for a given server, future connections skip the delay. This is transparent to your application.
Authentication
Pass JWT tokens via URL query parameters:
let url = Url::parse(&format!(
"https://relay.example.com/room/123?jwt={}", token
))?;
let session = client.connect(url).await?;See the Authentication guide for how to generate tokens.
Publishing
The video example demonstrates publishing end-to-end.
The connected Session exposes a publisher() origin::Producer you publish broadcasts into:
let session = client.connect(url).await?;
let route = moq_net::broadcast::Route::new().with_announce(true);
let mut broadcast = session.publisher().create_broadcast("", route)?;
// ... add catalog and tracks to the broadcast ...See the full video.rs example for catalog setup, track creation, and frame encoding.
Subscribing
The subscribe example demonstrates subscribing end-to-end.
The session also exposes a consumer() origin::Consumer for receiving announcements:
let session = client.connect(url).await?;
let mut announced = session.consumer().announced();
// Wait for broadcasts to be announced.
while let Some((path, broadcast)) = announced.next().await {
let Some(broadcast) = broadcast else {
tracing::info!(%path, "broadcast ended");
continue;
};
// Subscribe to tracks on this broadcast...
}If you already know the broadcast path, you can subscribe directly:
let broadcast = session.consumer().get_broadcast("my-stream")
.expect("broadcast not found");Reading the Catalog
The hang catalog describes available media tracks. Subscribe to it using CatalogConsumer:
let catalog_track = broadcast
.consume_track(hang::Catalog::DEFAULT_NAME)
.subscribe(hang::Catalog::default_subscription())
.await?;
let mut catalog = hang::CatalogConsumer::new(catalog_track);
let info = catalog.next().await?.expect("no catalog");The catalog is live-updated — call catalog.next().await again to receive updates when tracks change.
See the full subscribe.rs example for iterating renditions and selecting a track.
Reading Frames
Subscribe to a media track and read frames using OrderedConsumer:
let track_consumer = broadcast
.consume_track(&track_name)
.subscribe(moq_net::Subscription::default().with_priority(1))
.await?;
let mut ordered = hang::container::OrderedConsumer::new(
track_consumer,
Duration::from_millis(500), // max latency before skipping groups
);
while let Some(frame) = ordered.read().await? {
// frame.timestamp, frame.keyframe, frame.payload
}OrderedConsumer handles group ordering and latency management automatically. Groups that fall too far behind are skipped to maintain real-time playback.
Platform Decoders
The frame payload contains the raw codec bitstream. You need a platform decoder to turn it into pixels or audio samples.
Video
- macOS/iOS — VideoToolbox (
VTDecompressionSession). Feed H.264 NALs wrapped inCMSampleBuffer. - Android —
MediaCodecvia NDK. Feed NAL units directly. - Linux — VA-API via
libva, or GStreamer for a higher-level API. - Cross-platform — FFmpeg via the
ffmpeg-nextcrate works everywhere.
Audio
For AAC-LC audio, symphonia decodes to PCM samples and cpal handles platform audio output. For Opus, symphonia also supports decoding, or use the opus crate directly.
Use a ring buffer between the decoder and audio output to absorb network jitter.
Common Pitfalls
description Field in the Catalog
Both VideoConfig and AudioConfig have a description field that provides out-of-band codec initialization data. If present, it contains codec-specific configuration as a hex-encoded byte string.
Video examples:
- H.264 — SPS/PPS in AVCC format. NAL units in the payload are length-prefixed.
- H.265 — VPS/SPS/PPS in HVCC format.
Audio examples:
- AAC —
AudioSpecificConfigbytes. - Opus — Typically
None; configuration is in-band.
When description is None, codec parameters are delivered in-band (e.g. Annex B start codes 00 00 00 01 or 00 00 01 for H.264/H.265). Your decoder must handle whichever format the publisher uses. See the hang format docs for details.
Container Format
Check the container field for each rendition:
legacy— Each frame is a varint timestamp (microseconds) followed by the codec payload. This is the common case.cmaf— Each frame is amoof+mdatpair (fragmented MP4). Used for HLS compatibility.loc— Low Overhead Container: each frame is a small property block followed by the codec payload.
OrderedConsumer decodes legacy timestamps for you automatically.
Anything else decodes as Container::Unknown, which preserves the original JSON so you can republish the catalog unchanged. Skip those renditions: their frames can't be parsed.
Next Steps
- hang format — Catalog schema and container details
- moq-net docs — Core networking API reference
- moq-native docs — Client configuration options
- Relay HTTP endpoints — HTTP fetch for debugging and late-join
- video.rs — Complete publishing example
- subscribe.rs — Complete subscribing example