open-standards/joule-code/joule-code-rs/crates/joule-moq
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science af4b1529f7
All checks were successful
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 23s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 28s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 19s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 38s
WAI Studio / does this change touch it (push) Successful in 16s
wai-app and Create / wai-app · does this change touch it (push) Successful in 57s
wai-fleet service / does this change touch it (push) Successful in 59s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 24s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 5m34s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 53s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 2m12s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 3m2s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 4m19s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m9s
wai-fleet service / service · linux (push) Successful in 2m34s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 7m36s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 9m46s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 20m23s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 8m53s
WAI Studio / Studio in WebKit · linux (push) Successful in 14m30s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 54s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 6m13s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 52s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m0s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m6s
wai-fleet service / service · macos (push) Successful in 42s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m29s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m23s
Relay robustness: denial-of-service limits, HopTime and Sync Support, the DVR store as MoQ files, a load harness with a signed record, and a switch through the relay
The relay is held to draft-englishm-moq-relay-dos-02 consideration by consideration (the
README's table, which also says what is not bounded). New limits: the sessions refused at
SETUP that are held open, in all and by address, the oldest shed first; subscriptions,
published tracks and namespaces a session; requests a second a session, refused
EXCESSIVE_LOAD with a Retry Interval.

What the MoQ timing package left unwired:
- the relay rewrites TEMPO's HopTime as it forwards, for a subscription that carried the
  draft's Sync Support parameter, negotiated by a Setup Option at -23 and -22 (-18 and -16
  carry none; `stamp = "always"` covers them). The draft's code points are unassigned: the
  placeholders are in joule_moq::tempo alone, and [tempo] sets the three property types;
- joule-moq-room's tempo gains a Stamper and a Receiver, and a test publishes stamped
  objects on a session and holds each to the room at a subscriber through the relay;
- the DVR store writes its window as .moq and .dat files, read back and verified against
  the receipts from the files alone, agreeing with a FETCH byte for byte.

A load harness to draft-evens-moq-bench-00's method (joule_moq_relay::bench,
examples/relay_bench.rs): profiles, the draft's messages and metrics, and a signed, dated
measurement record of the host that ran it, used as a regression guard with stated
tolerances; it compares a host only with itself. A subscriber is switched between two
renditions through the relay by joule-moq's switching plan: every frame once, on a Group
boundary.

Planted faults, each caught: a flood of refused SETUPs (and, with the bounds removed, all
held); a session past each count and past its rate (and, with the limit removed, served); a
switch mid-group; timing claims off the instant, past the bound and outside Immutable
Properties; a data file and a .moq file cut short, a data file outside its directory, a
changed payload; a record changed after signing and a run claimed for another host.

Left: the rewind subscription filter (draft-duke-moq-subscribe-rewind-02), probe tracks at
the relay (the library is in joule-moq), TOO_FAR_BEHIND, FEC tracks and V3C publishing.

Co-Authored-By: Claude <[email protected]>
2026-11-10 23:31:46 -05:00
..
examples Track switching and probe tracks as library modules of joule-moq, each with a corpus and a second implementation 2026-11-10 23:31:32 -05:00
src Relay robustness: denial-of-service limits, HopTime and Sync Support, the DVR store as MoQ files, a load harness with a signed record, and a switch through the relay 2026-11-10 23:31:46 -05:00
tests Relay robustness: denial-of-service limits, HopTime and Sync Support, the DVR store as MoQ files, a load harness with a signed record, and a switch through the relay 2026-11-10 23:31:46 -05:00
Cargo.toml The retired communicationOS name leaves the repository 2026-11-07 02:39:44 -05:00
README.md Track switching and probe tracks as library modules of joule-moq, each with a corpus and a second implementation 2026-11-10 23:31:32 -05:00

joule-moq

MoqT (Media over QUIC Transport) for WAI broadcast media, with a JWP receipt extension: every object carries verifiable provenance and its energy figure.

The drafts it implements

draft-ietf-moq-transport-23 (2 November 2026), the current draft, and three earlier drafts for interoperation with deployed relays and other implementations: draft-ietf-moq-transport-22 (1 October 2026), the model this crate is written in, draft-ietf-moq-transport-18 (12 May 2026), the working group's interoperability target, and draft-ietf-moq-transport-16 (13 January 2026). A draft is chosen before the first MOQT byte, by the ALPN moqt-NN on native QUIC or the WT-Available-Protocols value moqt-NN on WebTransport; this crate prefers -23, then -22, -18 and -16 (draft::Draft). Drafts do not interoperate with each other, so a session is one draft from its first byte to its last. MOQT_DRAFT_VERSION is -22's conventional code, 0xff000016, the model's.

Each draft is a codec module of its own: draft23, draft22, draft18, draft16. The rest of the crate (tracks, objects, receipts, the session's state) is written once, in -22's model; draft23::bridge, draft18::bridge and draft16::bridge map every message and object of the other drafts to and from it, and name what one draft can say and the other cannot. joule-moq-relay's transport::bridge runs a whole session that way, so the relay and its client speak all four drafts with one implementation.

Where -23 differs from -22

-23 has -22's session shape, integer, Location, Key-Value-Pairs, names, Token, subgroup streams, datagrams and padding; draft23 uses -22's code for those and has its own for the rest. Its change log (§A.1) lists each change; each is implemented as follows:

-23 (issue) draft23 the bridge to -22
control message Length a varint (#2017); MAX_CONTROL_MESSAGE_SIZE, Setup Option 0x09, at least and by default 65535 control: framed with a varint, a Length past the limit refused before its body is waited for (FIELD_LENGTH_EXCEEDED); the option read and checked the frame rewritten; the option kept, as an unknown one is at -22
FETCH_OK gone, 0x18 reserved; FETCH answered by REQUEST_OK with EXPIRES and LARGEST_OBJECT (#2009) control: 0x18 an unknown message (UNEXPECTED_MESSAGE); RequestOk::check_for(Fetch) -23 to -22: End Location from LARGEST_OBJECT inside the requested range, else the requested end; -22 to -23: LARGEST_OBJECT where the End Location falls short; End Of Track becomes End of Track (0x81) after the last object on the fetch stream
a relative Location filter in FETCH is INVALID_RANGE (#1960) read; the publisher's to refuse refused with INVALID_RANGE by the bridge, both ways
FORWARD (0x10, 1 forward) renamed PAUSED (0x11, 1 paused) (#2002) params::Parameter::Paused; 0x10 unknown (INVALID_PARAMETER) inverted, both ways
REQUEST_UPDATEs processed and answered in order; FILL_PARAMETERS and TRACK_NAMESPACE_PREFIX updates not coalesced (#1938, #2042) RequestUpdate::may_coalesce the relay answers updates in order, one by one
LARGEST_OBJECT a relay computes (#1978) read and written the relay's: the largest of what its upstream publisher stated (SUBSCRIBE_OK, PUBLISH, PUBLISH_STATE_NOTIFY) and the objects it received on its upstream subscriptions, all unpaused and unfiltered; an object counts once whole, not from its first byte (joule-moq-relay's README)
overlapping SUBSCRIBE_NAMESPACE and SUBSCRIBE_TRACKS allowed; PREFIX_OVERLAP gone; SUBSCRIBE_TRACKS_ID (0x36) (#2018) codes, params the relay refuses an overlap only at -22 and earlier; SUBSCRIBE_TRACKS_ID dropped (the relay refuses SUBSCRIBE_TRACKS)
updating a namespace subscription's prefix (#2019) read the relay's
SUBSCRIBE_TRACKS lists its parameters (#1995); FILL_PARAMETERS also in SUBSCRIBE_TRACKS and PUBLISH (#2007) params::ctx FILL_PARAMETERS in PUBLISH refused (NOT_SUPPORTED): -22 has no fill for a PUBLISH
duplicate PUBLISH_NAMESPACE allowed (#2021) the relay keeps each, the namespace published until the last is withdrawn
NAMESPACE carries Parameters (#1858) control::Namespace; none is defined dropped by name if any
FILL_PARAMETERS a count of parameters, not a length (#2015) params the inner parameters mapped
EXPIRES in every OK (#1994) params::ctx dropped where -22's message has no place for it
specific session codes in place of PROTOCOL_VIOLATION (#1773) error: the code each fault gets, by plane a -23 session is closed with -23's code
REQUEST_ERROR's Error Details: REDIRECT (0x01), UNSUPPORTED_PROPERTY (0x02) (#2010, #2011) control::ErrorDetail; REDIRECT without its detail is MALFORMED_MESSAGE the Redirect both ways; other details dropped by name
unknown Mandatory Track Properties in TRACK_STATUS_OK (#2016) data::unknown_mandatory the relay's
EXPIRED_AUTH_TOKEN for PUBLISH_DONE (#1970), UNAVAILABLE for REQUEST_ERROR (#2041) codes numbers travel; on a -23 session the relay ends a subscription with PUBLISH_DONE EXPIRED_AUTH_TOKEN when the grant it was held by expires, and on the older drafts' sessions, whose PUBLISH_DONE has no such status, the session
GOAWAY on a request stream from its receiver only (#1966) request::RequestStream a -23 requester's GOAWAY closes the session (PROTOCOL_VIOLATION); a -22 requester's is not sent
a receiver need not detect every duplicate Request ID (#1993) -22's tracker, which detects every one
Track Names not empty (#1954) refused (PROTOCOL_VIOLATION), except a Redirect's for a namespace request an empty -22 name refused (NOT_SUPPORTED)
NEW_GROUP_REQUEST before DYNAMIC_GROUPS is known (#2008) read
a Gone object state, End of Gone Range (0x40C) (#2022) data::RangeEnd::Gone sent to -22 as End of Unknown Range
End of Range delta-coded (#2031); Object ID Delta + 2 within a group (#2006); End of Group (0x80) and End of Track (0x81) (#2013) data::FetchCodec every fetch stream re-encoded, both ways; End of Group dropped at -22, and an End of Range inside the prior entry's group, which -22 (as read here) cannot say, left as a gap
UNKNOWN_OBJECT_STATUS reset code gone (#2004) codes
a supported property outside its scope, or repeated, makes the track malformed (#2039, #2044) data::check_properties a subgroup stream's bytes are -22's and pass the bridge as they are: the relay's reader applies the rule to a -23 session's (draft22::data::UniStreamDecoder::objects_at_23)
SUBGROUP_HEADER PROPERTIES bit for later objects (#1915) the sender's
grease values are not Mandatory Track Properties (#2037) data::is_mandatory, Properties::check_object_as the relay's reader takes -23's reading on a -23 session's streams and keeps -22's on a -22 session's

Where -23 leaves an encoding open

  • MAX_CONTROL_MESSAGE_SIZE's type, 0x09, is odd, so as a Key-Value-Pair its value is length-prefixed bytes, while §9.1.8 encodes the size as a variable-length integer: the bytes hold exactly one varint. A value below 65535, or bytes that are not one varint, do not match the option's definition (MALFORMED_KEY_VALUE).
  • §9.1 forbids repeating a Setup Option but names no code: PROTOCOL_VIOLATION.
  • End of Group and End of Track close the group of the prior entry, which may be an End of Range; the encoder writes them only right after an object.
  • A REDIRECT Error Detail whose value does not read is a Key-Value-Pair that does not match its type (§8.3, MALFORMED_KEY_VALUE); a REDIRECT beside a code other than REDIRECT, like UNSUPPORTED_PROPERTY beside one other than UNSUPPORTED_EXTENSION, is kept as an unknown detail and ignored (§9.4.2).
  • A fault whose rule names no code takes, on a control or request stream, MALFORMED_MESSAGE when it is cut short or has bytes left over and INVALID_FIELD for a value out of range; on a data stream TRUNCATED_OBJECT for a FIN inside an object; in a datagram MALFORMED_OBJECT.

Where -18 differs from -22

-18 has -22's integer, Key-Value-Pairs, names, Token structure, message frame, a pair of unidirectional control streams and a bidirectional stream per request, and -22's objects on the wire; draft18 uses -22's code for those and has its own for the rest:

-18 -22 the bridge
filter parameter 0x21 SUBSCRIPTION_FILTER, length-prefixed: Next Group Start, Largest Object, AbsoluteStart, AbsoluteRange (End Group Delta) LOCATION_FILTER, six forms Relative Start 0, Next Object, Absolute Start, Absolute Start Group End; -22's other forms are refused
FILL_PARAMETERS, Range Filters, INCLUDE_PROPERTIES none defined refused (NOT_SUPPORTED)
PUBLISH_OK's parameters the subscription's (filter, forward, priority, group order, timeouts) EXPIRES only; the rest by REQUEST_UPDATE left out, by name
GOAWAY a Request ID, on the control stream none supplied from the peer's Request IDs seen
FETCH Fetch Type: Standalone (End exclusive, {g, 0} the whole group) or Joining namespace, name, LOCATION_FILTER (inclusive); joining by fill exclusive to inclusive; Joining refused
FETCH_OK End Location exclusive inclusive {g, o} to {g, o-1}, {g, 0} to {g, 2^64-1}
PUBLISH_BLOCKED / PUBLISH_SKIPPED 0x0F 0x0F renamed
PUBLISH_STATE_NOTIFY none 0x22 left out
PUBLISH_DONE Stream Count, unknown 2^62-1 2^64-1 mapped
End of Timed-Out Range (0x20C) none defined sent as End of Unknown Range
Setup Options 0x06, 0x08 unknown MAX_FILTER_RANGES, MAX_REQUEST_UPDATES left out both ways
codes VERSION_NEGOTIATION_FAILED, DUPLICATE_SUBSCRIPTION, INVALID_JOINING_REQUEST_ID, SUBSCRIPTION_ENDED TOO_MANY_REQUEST_UPDATES, CONFLICTING_FILTERS, INVALID_FILTER by number; an unknown code reads as INTERNAL_ERROR
a requester's FIN on PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE, SUBSCRIBE_TRACKS cancels it (§6.1: "closing the stream with either a FIN or RESET_STREAM"; §6.2: withdrawn by cancelling) not a cancellation (§6.4.2.2) an -18 FIN reaches -22 as a cancellation; a -22 requester's FIN on these is not sent

-18 itself leaves two things open, read here as follows: Table 5 lists 0x1E PUBLISH_OK pointing to REQUEST_OK, while §10.5 and the change log make PUBLISH_OK the name of the REQUEST_OK that answers a PUBLISH; this crate sends 0x07 and reads either. And §2.5 names 0x38-0x3F as the one-byte application property range where its §15.8 (and -22) give 0x78-0x7F; this crate's own properties are in 0x3800-0x3FFF, which both give.

Where -16 differs from -22

-16 writes every integer as QUIC's (RFC 9000 §16: 1, 2, 4 or 8 bytes, at most 2^62-1), and has another session shape, so draft16 is its own throughout:

-16 -22
control one bidirectional stream the client opens: CLIENT_SETUP (0x20), SERVER_SETUP (0x21), then every request and response by Request ID SETUP (0x2F00) on each end's unidirectional stream; a stream per request
Request IDs the next in sequence (§9.1), within MAX_REQUEST_ID (setup parameter 0x02, message 0x15), REQUESTS_BLOCKED (0x1A) parity and uniqueness
cancelling UNSUBSCRIBE, FETCH_CANCEL, PUBLISH_NAMESPACE_DONE, PUBLISH_NAMESPACE_CANCEL resetting the request's stream
Message Parameters Key-Value-Pairs: priority, group order and forward as integers, LARGEST_OBJECT length-prefixed; one DELIVERY_TIMEOUT (> 0) each in its own form; two delivery timeouts
SUBSCRIPTION_FILTER AbsoluteRange End Group absolute (-18) a delta
SUBSCRIBE_NAMESPACE (0x11) Subscribe Options: PUBLISH, NAMESPACE or both SUBSCRIBE_NAMESPACE and SUBSCRIBE_TRACKS
SUBGROUP_HEADER no FIRST_OBJECT bit FIRST_OBJECT 0x40
fetch objects Group and Object IDs absolute delta-coded in the Group Order
padding none streams and datagrams
a Track Namespace 1 to 32 fields 0 to 32
PUBLISH_DONE codes EXPIRED 0x5, TOO_FAR_BEHIND 0x6 the reverse
stream reset UNKNOWN_OBJECT_STATUS 0x4 0x6 (0x4 is GOING_AWAY)

joule-moq-relay's bridge renumbers this end's Request IDs into -16's sequence, grants the peer requests ahead of it, routes each request and its responses to a stream of its own, turns a cancelled stream into -16's cancel message, and re-encodes every object (the pairs inside Immutable Extensions included). A request that finds no Request ID left under the peer's MAX_REQUEST_ID for ten seconds is refused locally, with EXCESSIVE_LOAD. -16's SUBSCRIBE_NAMESPACE rides a bidirectional stream of its own (§9.25) while every other request rides the control stream, and QUIC does not order one stream against another, so a peer's request can arrive ahead of an earlier one still in flight: the bridge admits a new ID of the peer's parity within the grant, leaving the gap, and closes the session with INVALID_REQUEST_ID for an ID seen before, where §9.1 would close it for any ID out of sequence.

Mappings of other MoQ drafts

Two modules map a draft onto this crate's objects; neither is on a session's path.

  • file: draft-jennings-moq-file-05. A track's objects as a .moq file (one JSON member an object) beside a .dat file of their payloads, read back byte for byte, properties and all, so a track and its receipts track verify from the files alone. The draft is written against -14; the module writes -23's objects, and -22's, -18's and -16's, which carry the same. WAI extensions/live-recording.md §8.7; corpus wai/moqt-conformance/moq-file/.
  • v3c: draft-yuyou-moq-v3c-00. A wai.pc.vpcc payload split into the draft's component tracks and joined back. WAI extensions/moq-streaming-format.md §4.5; corpus wai/moqt-conformance/v3c/. ISO/IEC 23090-5 was not read, and the corpus holds no bitstream an encoder wrote.

cargo run -p joule-moq --example oneway_vectors -- gen writes both corpora, and tests/oneway_vectors.rs holds the crate to them.

Track switching and probe tracks

Two modules are what an adaptive subscriber and the relay that serves it compute; neither opens a stream, and neither is on a session's path until a caller puts it there.

  • switching: draft-ugur-moq-fast-switching-00, read with draft-gurel-moq-track-switching-01. From an MSF catalog's altGroup and keyframeSpacing it reads a Switch Set, its GoP Ladders and its Alignment Unit, and refuses a set in which a spacing is not a multiple of that unit. It gives a Location's Frame Number, a target's nearest Switch Point and its Distance, the track to switch to under the draft's full policy or its simpler one, and the End Group expected of the current track. A SwitchPlan states the switch as the two requests draft-ietf-moq-transport-23 has: a SUBSCRIBE of the target with an absolute-start Location Filter at the Switch Point, and a REQUEST_UPDATE that ends the current subscription at the End Group. SWITCH_FROM, altTrackGroup and Switch Track Alias are not in -23 and are not written; the module's text says what they would add. A ReleaseBuffer takes both tracks' objects through the overlap and presents each Frame Number once, in order. A switch that would start inside a Group, or at a Group the target never publishes, is refused, each with its own code, and so is a plan whose End Group would leave frames unsent. Corpus wai/moqt-conformance/switching/.
  • probe: draft-gurel-moq-probing-00. Dynamic Probe Names parsed by the draft's grammar with every invalid case of its §7.1 under its own reason; preset names; the resolution of its Figure 4 in exact integer arithmetic; a probe's objects, each with its Location, its payload size and its release time, at the lowest publisher priority, with LOC's Timestamp property and a payload of zeros; the per-probe, session and relay-wide limits of its §7.2 with Table 5's defaults as a configuration, nothing clamped; the error code of each outcome in each direction and its Retry Interval; the probe catalog, which has no member a limit could be carried in; the session-scoped namespaces and the empty-namespace form; and a receiver's measurement of throughput and of its arrival gaps against the send gaps. The relay that serves probes calls this module. Corpus wai/moqt-conformance/probe/.

cargo run -p joule-moq --example adaptation_vectors -- gen writes both corpora, and tests/adaptation_vectors.rs holds the crate to them.

What is implemented

The codec (draft22), every decoder bounded (it reads no byte that is not there and allocates nothing from a length before the bytes behind it exist) and every refusal a typed WireError carrying the draft's consequence: close the session with a code, reject the request, or a malformed track.

-22 module
§8.1 variable-length integer: 1 to 9 bytes, leading ones, longer forms accepted draft22::varint
§8.2 Location, §8.7 Track Namespace (at most 32 fields, none empty, 4,096 bytes for a Full Track Name), §8.8 rendering draft22::names
§8.3 Key-Value-Pairs (delta types, values up to 2^16-1 bytes), §8.4 and §10 Properties with Immutable Properties, Prior Group and Object ID Gaps and Mandatory Track Properties, §9.1 Setup Options draft22::kvp
§8.9 Authorization Token structure draft22::token
§9 every control message of Table 5, framed by a 16-bit Length draft22::control
§9.20 every Message Parameter, the list each message allows, the rules on repetition; §8.6 Range Filters; §9.20.9 Location Filters; FILL_PARAMETERS draft22::params
§6.3 and §6.4.2 control and request streams, Request IDs by end, §9.1.7 MAX_REQUEST_UPDATES credit, the order of messages on each request stream draft22::request
§11 subgroup streams, object datagrams, fetch streams with End of Range, padding; a whole-stream decoder and an incremental one draft22::data
§12 and §16.11 session, request, PUBLISH_DONE and stream reset codes; §13 grease draft22::codes

On it:

  • session: the draft by ALPN, SETUP options, Request IDs, the receipt extension declared by a Setup Option.
  • transport_quic (feature quic): native QUIC on quinn. A pair of unidirectional control streams, each opened by SETUP; a bidirectional stream per request; subgroup, fetch and padding streams; object datagrams.
  • mock: an in-memory fabric and relay whose objects, receipts and requests cross the -22 codec.
  • carriage: how this crate's objects and receipts ride -22 objects (below).

An extension parameter: RELAY_PATH

-22 §9.20 has every Message Parameter "defined in the negotiated version of MOQT or negotiated via Setup Options". joule-moq-relay's cascading negotiates one: each end of a session between two relays declares the Setup Option RELAY_ID (0x3A4F, odd, so its value is bytes: the relay's 32-byte id), and with both declared every request one relay sends the other may carry RELAY_PATH (0x3A51): length-prefixed, 1 to 16 relay ids of 32 bytes each, concatenated, the sender last. The codec reads it in SUBSCRIBE, FETCH, TRACK_STATUS, PUBLISH_NAMESPACE and SUBSCRIBE_NAMESPACE (Parameter::RelayPath, Parameters::relay_path) and in no other message; a length that is not a multiple of 32, or past 16 ids, does not read. Whether it was negotiated is the session's to know: a receiver that did not negotiate it holds it unknown and closes the session with PROTOCOL_VIOLATION, as §9.20 requires. At -23 it is negotiated the same way and has the same encoding and the same messages (draft23::params::Parameter::RelayPath; -23 §9.19 admits a parameter "negotiated via Setup Options" as -22 §9.20 does), draft23::bridge carries it both ways, and an unnegotiated one, one in another message, or one that is not 1 to 16 ids is INVALID_PARAMETER there; the -23 corpus holds it (two messages that carry it, four refused). The -18 and -16 bridges have no form for it and refuse it by name.

Where -22 leaves an encoding open

The codec takes one reading of each, and the vector checker takes the same:

  • FILL_TIMEOUT (0x0A) and RENDEZVOUS_TIMEOUT (0x04) state no encoding; both are durations in milliseconds and are read as varints, as the draft encodes its other durations.
  • FILL_PARAMETERS is length-prefixed and holds "a sequence of Parameters": read up to its length, with no count before them, type deltas starting from 0.
  • End of Range (§11.4.1.2) says "the Group ID and Object ID fields are present"; the only such fields of a fetch object are the Group ID Delta and Object ID Delta, which bits 0x08 and 0x04 of 0x8C, 0x10C and 0x20C mark. They are read as an object's are: absolute on the first entry, and after it a Group ID that moves one or more groups in the request's Group Order, with an absolute Object ID.
  • An unknown message, stream or datagram type closes the session; -22 names no code, and this codec gives PROTOCOL_VIOLATION.

Carriage of this crate's objects and receipts

Object Properties in the range -22 sets aside for an application's own use (§8.4), inside the object's Immutable Properties, since a relay must not change them:

type name value
0x3A40 MRL_LAYER varint: the object's layer, when the header states layers
0x3A41 WAI_RECEIPT bytes: WAI extensions/moq-streaming-format.md §5's property, laid out below
0x3A42 MRL_TOTAL_LAYERS varint, when non-zero
0x3A43 JWP_OBJECT_RECEIPT bytes: the object receipt standing alone, in the binary form of WAI extensions/jwp-receipts.md §2.7.2
0x3A44 WAI_ENVELOPE varint 1, when the payload is a WAI envelope

A layer cap on a SUBSCRIBE is an OBJECT_PROPERTY_FILTER on MRL_LAYER (messages::mrl_cap_filter), which needs the peer's MAX_FILTER_RANGES above 0. An object's delivery timeout rides as its OBJECT_DELIVERY_TIMEOUT property (§10.2).

The WAI_RECEIPT value: content_hash (32 bytes), joules_micro (a -22 varint), acquisition (a varint length, then UTF-8), acquisition_evidence (a varint length, then bytes), group_receipt (32 bytes), and nothing after.

A group's receipts ride its receipts track (moq-streaming-format §4.1): group g carries one object, the group receipt and the object receipts of group g of the media track in the binary form of WAI jwp-receipts §2.7.3: WJR1, the group receipt's canonical bytes (its signing payload in the form signed, then its signature, and for a legacy group its parent flag and parent), a u32 count, and each object receipt's body. ReceiptBundle::read gives the group's link hash computed over the bytes received; read_receipts, GroupReceipt::from_binary and ObjectReceipt::from_binary refuse what does not read with the §3 step jwp-receipts §2.7.4 names. tests/wai_jwp_corpus.rs reads and writes every binary vector of WAI's corpus.

Room grants (WAI extensions/live-rooms.md §10.5, grant_proof): a grant is public, so it is presented with a WRP1 proof of its principal's key over a challenge the verifier chose. Over MOQT the relay reads the client's SETUP before it sends its own (draft-ietf-moq-privacy-pass-auth-04 §3.5.1): every SETUP of a relay carries a challenge (GRANT_CHALLENGE, 0x3A47) good for one later SETUP there, and the client's carries the grant as an AUTHORIZATION TOKEN beside the proof (GRANT_PROOF, 0x3A49) over a challenge an earlier SETUP gave it, with the context SETUP //<authority><path>. A SETUP refused says so (GRANT_REFUSED, 0x3A53, the reason's code; add_refusal, refusal_of), and refusal_asks_for_proof says whether a proof over the challenge beside it answers it. check_proof is §10.5.4's checks, given the grant's digest and principal by whatever read the grant; over HTTP the same record rides the WAI-Room-Proof header and the challenge WAI-Room-Challenge.

LOC objects (WAI extensions/loc-carriage.md, loc): a WAI envelope whose payload is one WebCodecs chunk (an Opus packet, an AVC or HEVC access unit in Annex B, an ADTS frame) rides a track of packaging loc (draft-ietf-moq-loc-05) as one object: the payload is the LOC Payload, and the Immutable Properties hold the LOC properties (Timestamp 0x10, Timescale 0x08, Video Frame Marking 0x09, Audio Level 0x0C, Video Config 0x0D, Audio Config 0x0F, and -05's Codec String 0x11 where the publisher states one), the envelope's manifest as WAI_MANIFEST (0x3A61, bytes) and WAI_RECEIPT. loc::to_loc writes one and loc::from_loc reads it back into the envelope, byte for byte, its properties and its receipt property, refusing with the codes of the extension's §6. tests/loc_vectors.rs regenerates WAI's corpus live-conformance/loc/ (examples/loc_vectors.rs gen) and checks every vector in it.

The receipt extension is declared by the Setup Option 0x3A46, a varint holding the extension's version. -22 has no unregistered range for Setup Options and asks for a provisional registration; until one is made the code point is this crate's choice, and a peer that does not know it ignores it (§9.1).

Not yet implemented

  • draft-ietf-moq-transport-14 (ALPN moq-00, the version negotiated inside the setup messages) and the drafts between -16 and -22 other than -18. A relay that offers only -14 is not reached.
  • WebTransport (MOQT over HTTP/3, §6.2.1), a relay with priority fan-out and per-object receipts, and the authorization token cache (§8.9) are in joule-moq-relay, built on this codec; this crate's own session keeps no token cache. The browser client is joule-moq-web, over this crate built without its default feature runtime (the codec, the carriage and the receipts, for wasm32-unknown-unknown).
  • Evaluating filters at a publisher (§3.3), and holding a peer to its MAX_FILTER_RANGES: the codec reads and writes every filter and counts their ranges.
  • GOAWAY, delivery-timeout enforcement on real streams and §5.1's scheduling are joule-moq-relay's; this crate's QUIC transport has none of them, and its mock relay applies delivery timeouts on a caller-driven clock.
  • draft-ietf-moq-loc and draft-ietf-moq-msf semantics (catalogs, LOC properties): their properties ride as any property does.

The vector corpus

tests/vectors/draft-22/vectors.json holds -22 vectors: varints, every control message type, control, subgroup, fetch and padding streams, datagrams, and the WAI_RECEIPT value; and negative vectors, each with the refusal it must meet. tests/draft22_vectors.rs generates the file deterministically from typed values and holds the crate to it; JOULE_MOQ_WRITE_VECTORS=1 rewrites it. tests/vectors/draft-22/moqt22_check.py decodes the corpus independently, from the draft's text, standard library only:

python3 -I tests/vectors/draft-22/moqt22_check.py

Each vector has a name, a kind, its bytes as hex, canonical (whether this crate's encoder makes exactly those bytes) and expect; a uni-stream vector also has context.group_order (1 Ascending, 2 Descending), the Group Order of the request a fetch stream answers.

kind bytes reading
varint one varint {"value", "length"}
control one framed message the message (below)
uni-stream a whole stream to its FIN a list of events (below)
datagram one datagram {"datagram": {…}} or {"padding": n}
wai-receipt the WAI_RECEIPT value its five fields

The JSON view: byte strings are lowercase hex; a Track Namespace is a list of its fields in hex; a Location is [group, object]; a Key-Value-Pair is {"type", "value"} for an even type and {"type", "bytes"} for an odd one, with Immutable Properties as raw bytes. A message is an object with "message" (its name in Table 5) and its fields by their names in the draft, lowercased with underscores. A parameter is {"type": NAME} with "value" (a varint or a uint8), "token", "location", "filter" (a Location Filter's filter_type and the fields it has; a Range Filter's set_id, property_type for the property filters, and ranges as [start, end or null]; null for Length 0), "parameters" (FILL_PARAMETERS) or "namespace". Stream events are {"header": "subgroup" | "fetch" | "control" | "padding", …}, {"control": message}, {"object": …} (subgroup), {"fetch_object": …}, {"end_of_range": {"kind", "location"}} and {"padding": n}.

A refusal is {"error": kind, "consequence": c}, with c one of session:<code>, request:<code> or malformed-track (and property_invalid for the WAI_RECEIPT value, which a sink discards). The kinds are WireError::kind: truncated, trailing-bytes, unknown-message-type, unknown-stream-type, invalid-flags, unknown-parameter, duplicate, parameter-not-allowed, duplicate-filter, invalid-filter, type-overflow, length-exceeds-limit, too-many-namespace-fields, empty-namespace-field, invalid-value, overflow, properties-on-non-normal, unknown-object-status, no-prior-object, empty-datagram-properties, malformed-token, unexpected-message, malformed-track, and session-rule (a session error with a code of its own, such as KEY_VALUE_FORMATTING_ERROR).

The -23 corpus

tests/vectors/draft-23/vectors.json holds -23's vectors in the same form, generated by tests/draft23_vectors.rs and decoded independently by tests/vectors/draft-23/moqt23_check.py, written from -23's text: every control message and every parameter each may carry, the forms -23 changed (a varint Length, MAX_CONTROL_MESSAGE_SIZE, REQUEST_OK answering FETCH, Error Details, NAMESPACE's parameters, PAUSED, SUBSCRIBE_TRACKS_ID, counted FILL_PARAMETERS, fetch streams with each indicator in both Group Orders), and negative vectors, the planted faults, each with the session code -23 gives it. Among them each -22 form a -23 endpoint meets: FETCH_OK (0x18), FORWARD, a 16-bit Length, a length-prefixed FILL_PARAMETERS, NAMESPACE without its parameter count. The -23 view of a refusal names -23's code (session:UNEXPECTED_MESSAGE, session:INVALID_PARAMETER, ...); NAMESPACE has parameters; REQUEST_ERROR has details; a fetch stream has {"end_of_range": {"kind": "gone", ...}}, {"end_of_group": true} and {"end_of_track": true}.

The -18 and -16 corpora

tests/vectors/draft-18/vectors.json and tests/vectors/draft-16/vectors.json hold each earlier draft's vectors in the same form, generated by tests/draft18_vectors.rs and tests/draft16_vectors.rs (JOULE_MOQ_WRITE_VECTORS=1 rewrites them) and decoded independently by tests/vectors/draft-18/moqt18_check.py and tests/vectors/draft-16/moqt16_check.py, each written from its draft's text:

  • every control message of the draft, every parameter it may carry, each subscription filter, each FETCH type, and the setup messages;
  • the drafts' own examples: -18 §1.4.1 Table 2's integers; RFC 9000 Appendix A.1's for -16's integer; and the first subgroup of -18 §11.6 and -16 §10.5. Their second example cannot be reproduced: it gives a Properties Length of 33 and a Length of 21 for a 14-byte value. 33 is what -16's integer would take for a 21-byte value, not -18's. The working group publishes no vector corpus of its own;
  • control, subgroup (Immutable Properties, End of Group), fetch (both orders at -18; absolute IDs at -16) and padding streams, and datagrams;
  • negative vectors for what each draft refuses that a later one reads (a -22 form in an -18 message, End of Timed-Out Range, -22's SETUP in -16, LARGEST_OBJECT without its length, a DELIVERY_TIMEOUT of 0, FIRST_OBJECT at -16, and more), each with the draft's consequence.

The -18 view differs from -22's in its filter ({"filter_type", "start", "end_group_delta"}), FETCH (fetch_type and the Standalone or Joining fields) and GOAWAY (request_id, null when absent); the -16 view adds each response's request_id, calls Properties extensions, and writes an AbsoluteRange's end_group.

Tests

  • cargo test -p joule-moq --features quic --all-targets runs the codecs' unit tests (each draft's, and each bridge's mappings both ways), the four corpora (tests/draft23_vectors.rs, tests/draft22_vectors.rs, tests/draft18_vectors.rs, tests/draft16_vectors.rs), generated round trips and hostile input (tests/draft22_roundtrip.rs), the QUIC transport over loopback, the mock and relay, the staged MoQ trace and WAI's JWP corpus.
  • The independent decoders above run over their corpora in CI (.github/workflows/joule-code-conformance.yml).
  • joule-moq-relay's tests/draft23.rs and tests/drafts.rs run sessions at -23, -18 and -16 through the relay (see that crate), and joule-moq-interop runs this crate against relays (below).

Interoperability

Our own implementations, at each draft

Runs of 2026-11-10, on loopback (joule-moq-interop's scripts/local.sh, the relay on a port the system chose): joule-moq-interop (this crate's client) against joule-moq-relay; the relay's own sessions (joule-moq-relay's tests/draft23.rs, tests/drafts.rs); the ingest gateway's sessions to the relay (joule-moq-ingest's tests/ertmp.rs); and the browser client (joule-moq-web's e2e/room.e2e.mjs, scenarios webtransport23 and websocket23).

implementation transport -23 -22 -18 -16
joule-moq-interop: the runner's eight cases QUIC pass pass pass pass (no RENDEZVOUS_TIMEOUT at -16)
joule-moq-interop: wai-receipts QUIC pass pass pass pass
joule-moq-interop: wai-receipts WebTransport pass pass pass pass
joule-moq-interop: wai-receipts, publisher and subscriber at different drafts QUIC 23 to 22, 22 to 23, 23 to 18, 16 to 23: pass 16 to 22, 22 to 16: pass 18 to 16, 16 to 18: pass
joule-moq-interop: --tamper payload, receipt, in flight (must fail) QUIC fails, as it must fails, as it must
joule-moq-relay: two publishers, three subscribers, every object verified QUIC, WebTransport, WebSocket pass pass pass pass
joule-moq-relay: -23 and -22 clients through one relay, both directions QUIC, WebTransport, WebSocket pass pass
joule-moq-relay: FETCH from the cache QUIC, WebTransport pass pass pass pass
joule-moq-ingest: Enhanced RTMP into the room, the gateway's link at the draft, a -22 watcher QUIC pass pass pass pass
joule-moq-web: the room page in a browser (three publishing pages and a late joiner by FETCH, every object verified, the relay's summaries naming the draft) WebTransport, WebSocket pass pass

Other implementations

Runs of 2026-10-21, from the build host, with joule-moq-interop (in this workspace) and joule-moq-relay at this revision. Relays and clients are named by what they offer; the interoperability runner's registry (its implementations.json at commit e8d544e9, of 2026-10-20) lists their endpoints, and scripts/public.py reads it.

Two scenarios per relay, draft and transport:

  • the runner's test cases: setup-only, announce-only, publish-namespace-done, subscribe-namespace-lifecycle, subscribe-error, rendezvous-timeout (none at -16, which has no RENDEZVOUS_TIMEOUT), announce-subscribe, subscribe-before-announce;
  • WAI receipts end to end (wai-receipts): one session publishes a catalog, a WAI track (ten groups of three objects, each a WAI envelope carrying its receipt in Immutable Properties) and its receipts track through the relay; a second session subscribes and verifies every object against its receipt: inclusion, the signed bundle, the receipts chain, and the receipt and envelope the object carries. Where it does not pass, it runs again by PUBLISH instead of the namespace route, and with no per-object properties, to find which part the relay does not carry.

Pass is every case, both scenarios, on every transport tried; partial names what did not pass and why; fail is a scenario the relay does not carry.

Relay Draft QUIC WebTransport Result Cause of what did not pass
A public MOQT relay network's draft-18 interop endpoint -18 yes yes pass
The same network's production endpoint -16 not covered it needs an operator's token
The same network's draft-14 endpoint -14 not covered this crate has no -14
An open interop relay, drafts -14 to -22 on one port -22, -18, -16 yes yes pass
A research relay at -18 -18 yes yes pass
The same research relay at -22 -22 yes yes pass
A relay at -22 -22 yes yes pass
The same project's relay at -18 -18 yes yes fail PUBLISH_NAMESPACE is accepted, but a SUBSCRIBE for a track in it gets no answer, nor does one after PUBLISH: announce-subscribe and receipts fail; no NAMESPACE for a subscribed prefix; rendezvous-timeout: DOES_NOT_EXIST.
A relay at -16 and -18 -18, -16 yes yes pass
A relay at -14 to -22 -22, -18, -16 yes yes pass
A media server's relay at -22 -22 yes yes partial rendezvous-timeout: DOES_NOT_EXIST at once, not TIMEOUT after the 500 ms asked. Receipts pass.
A relay library's relay, -16 to -22 -22, -18, -16 yes yes partial Receipts pass at every draft. A namespace a closed session published stays registered, so the next session's PUBLISH_NAMESPACE of it is refused ("Namespace already published"): announce-only, publish-namespace-done, announce-subscribe, subscribe-before-announce over QUIC at -22 and -18, publish-namespace-done over WebTransport.
An open relay network's anonymous endpoint, -14 to -20 -18, -16 yes yes partial Receipts: every object delivered and verified against the receipts track, but the relay drops Object Properties, so no object carries its own receipt or envelope; it refuses PUBLISH (NOT_SUPPORTED). rendezvous-timeout at -18: DOES_NOT_EXIST ("unroutable"). Once, over WebTransport, an internal transport error answered a SUBSCRIBE. Every runner case passes at -16.
A relay at -18 to -22 -18 yes yes partial As the row above: Object Properties dropped, PUBLISH refused, rendezvous-timeout DOES_NOT_EXIST.
The same relay at -22 -22 yes yes fail It refuses the publisher's SUBSCRIBE_OK with MALFORMED_TRACK ("short buffer"), so no track crosses it: announce-subscribe and receipts fail. The same exchange passes there at -18, and at -22 with the relays above.
A relay at -18 with a self-signed certificate -18 yes yes partial Objects that carry Object Properties are not forwarded: no media object arrives, while the receipts track (no properties) does and verifies; with no per-object properties every object verifies. SUBSCRIBE_OK for a track no one publishes (subscribe-error, rendezvous-timeout).
A relay at -18 over WebTransport only -18 yes fail Not reached from the build host: no answer to the QUIC handshake.

Other implementations' interoperability test clients against joule-moq-relay on loopback (scripts/clients.sh), three rounds each, over QUIC and WebTransport:

Client Draft Result Cause
The public relay network's -18 test client (the runner's cases and its data-plane cases) -18 pass
An open relay network's test client, its draft pinned -18, -16 pass
The same client at its default draft -22 partial Its SUBSCRIBE carries Message Parameter 0x1, which -22 does not define, so the relay closes the session (-22 §9.20: PROTOCOL_VIOLATION): announce-subscribe. It withdraws a namespace by a FIN, which at -22 is not a cancellation (§6.4.2.2): subscribe-namespace-lifecycle.

These runs found five faults in joule-moq-relay, fixed here (its README states the rules now): a PUBLISH_DONE that arrives before the streams it counts; a track published again after TRACK_ENDED; a publisher that closes its control stream before its last objects; an -18 requester's FIN on a namespace request; a -16 Request ID that arrives ahead of one in flight on another stream. They also found one in the client: it stopped reading after a count of streams, not of objects. scripts/local.sh runs the client against the relay at each draft and across drafts, and checks that --tamper (a payload, a receipt, or a byte of an object in flight) fails. None of these runs is in CI: they reach the public internet or run other implementations' code.