moqtap-codec 0.5.0

MoQT (Media over QUIC Transport) wire codec — draft-07 through draft-20 message encoding/decoding
Documentation

moqtap-codec

MoQT wire codec — a parser and writer for every MoQT draft from draft-07 through draft-20.

Pure encoding and decoding: no I/O, no async runtime, no network dependencies. It depends only on bytes and thiserror.

What it does

  • Encodes and decodes every MoQT control message type per each supported draft (setup, subscribe, publish, fetch, namespace, track status, goaway)
  • Encodes and decodes data stream headers (subgroup, datagram, fetch, object)
  • Variable-length integers, in whichever encoding the draft uses: RFC 9000 Section 16 through draft-16, and MoQT's own (Section 1.4.1) from draft-17, where the length is the number of leading 1 bits in the first byte. DraftVersion::varint_encoding maps drafts to encodings
  • Key-value parameter (KVP) pairs
  • Core protocol types: TrackNamespace, Location, FilterType, GroupOrder, ObjectStatus
  • Session and request error codes, per-draft
  • Runtime draft dispatch via the dispatch module (AnyControlMessage, AnySubgroupHeader, AnyFetchHeader, AnyDatagramHeader, AnyObjectHeader) — one enum variant per enabled draft feature, decode selects the draft at runtime from a DraftVersion
  • Draft-neutral object framing on the same seam (AnySubgroupObjectReader, AnySubgroupObjectWriter, AnyFetchObjectReader, AnyFetchObjectWriter and the values they hand back), so a caller frames a data stream into individually addressable objects without naming a draftNN type. Subgroup and fetch streams on every draft 07-20
  • Structured parameter values, rather than opaque bytes: the AUTHORIZATION TOKEN structure (auth_token), the subscription filter of drafts 15-19 (subscription_filter), and the Range Filter parameters of drafts 19 and 20 (range_filter). Draft-20 rebuilt the LOCATION_FILTER value — the Filter Type enum is gone and the shape comes from the field count — so it reads its own through draft20::message::decode_location_filter, and reads the nested parameter block of the new FILL_PARAMETERS through draft20::message::decode_fill_parameters

Module layout

Each draft's wire format lives in its own module with an independent implementation. Nothing decides a draft's encoding outside that draft's module, and a build that enables one draft compiles one draft's format.

moqtap_codec::
    varint, kvp, types, version, error   (shared primitives)
    auth_token, subscription_filter,
    range_filter                         (structured parameter values,
                                          each serving a stated draft subset)
    fields                               (draft-neutral rendering types:
                                          FieldValue, FieldMap)
    dispatch, data_dispatch              (per-draft dispatch: runtime Any*
                                          enums and object framing)
    draft07, draft08, ..., draft20       (per-draft wire format)

"No wire-level code is shared across drafts" is what the older wording here said, and it was not true. dispatch and data_dispatch are ~4,900 lines at the crate root that decode and encode object headers, extension blocks, delta-encoded object IDs and stream-type fields across all fourteen drafts — the tree diagram above has listed them since runtime dispatch landed, so the page contradicted itself in twelve lines. fields/params.rs is the sharper counterexample: it decodes wire bytes across drafts, calling VarInt::decode on a length-prefixed parameter value from a table four drafts share, and that is exactly where a remotely-triggerable panic lived — four drafts on one table while their own decoders disagreed about which parameter types are integers.

The rule that is true, and that this crate is actually built on:

  • Every draft's format is decided in that draft's module. dispatch and data_dispatch contain an arm per draft that forwards to it; they choose which decoder runs and never what it does. data_dispatch's macro families are grouped by wire era — legacy_subgroup_glue! for drafts 07-13, modern_subgroup_glue! for 14-20, fetch_glue! for 07-13, plus hand-written fo14..fo20 where no macro shape captured the flags.
  • A crate-root module may be shared only where it serves a stated draft subset, and the subset is a specification fact rather than a convenience: subscription_filter is drafts 15-19, range_filter is 19-20, fields/params.rs is 07-10 because draft-08 removed ROLE and draft-11 keeps its own tables.
  • Anything reachable from a single draft belongs to that draft's module. This is the invariant fields/mod.rs is written under; violating it produced 16 dead-code errors under a single-draft build. Draft-14's parameter handling was moved out of the shared file for exactly this reason.

fields is settled and public: AnyControlMessage::fields() returns a FieldMap. fields/params.rs is pub(crate), gated any(draft07, draft08, draft09, draft10), and holds only what more than one draft shares — one non-SETUP parameter table across 07-10 and the generic kvp_to_json_d07_inner that reads it. Draft-07's SETUP table is draft-07-only; the 08-10 SETUP table, the ROLE-less cohort, is gated to those three. Every other draft defines its own tables in its own draftNN/fields.rs.

The workspace-wide statement of all this — including the two other crates, which span drafts differently — is ARCHITECTURE.md.

Draft selection

Each draft is behind a feature flag. Enable the ones you need. The default is all-drafts.

# every draft (default)
moqtap-codec = "0.5"

# draft-14 only
moqtap-codec = { version = "0.5", default-features = false, features = ["draft14"] }

# draft-07 plus draft-14 for runtime dispatch
moqtap-codec = { version = "0.5", default-features = false, features = ["draft07", "draft14"] }

Draft-20 is supported and is not a default anywhere. It is the newest draft this crate implements; it is not the interop target, and nothing here promotes it to a connection default, an advertised-preferred version or an auto-selected draft.

Usage

Per-draft (compile-time)

use moqtap_codec::draft14::message::{ControlMessage, ClientSetup};
use moqtap_codec::varint::VarInt;

let msg = ControlMessage::ClientSetup(ClientSetup {
    supported_versions: vec![VarInt::from_u64(0xff00000e).unwrap()],
    parameters: vec![],
});
let mut buf = Vec::new();
msg.encode(&mut buf).unwrap();

let mut cursor = &buf[..];
let decoded = ControlMessage::decode(&mut cursor).unwrap();
assert_eq!(msg, decoded);

Runtime dispatch across drafts

use moqtap_codec::dispatch::AnyControlMessage;
use moqtap_codec::version::DraftVersion;

let mut cursor: &[u8] = &wire_bytes;
let msg = AnyControlMessage::decode(DraftVersion::Draft14, &mut cursor)?;
match msg {
    AnyControlMessage::Draft14(inner) => { /* ... */ }
    _ => {}
}

Benchmarks

Criterion benches in benches/codec.rs cover varint, KVP, control-message, data-stream header and subgroup decode paths. They need the draft17 feature, which is on by default.

cargo bench -p moqtap-codec

License

MIT