open-standards/wai/extensions/moq-streaming-format.md
Transaction Science 1fa6a0ba8a
Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 6s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 10s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been skipped
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been skipped
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 3m10s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 2m44s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 2m7s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
wai cross-architecture byte-equality / does this change touch it (pull_request) Successful in 15s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Has been skipped
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Has been skipped
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Has been skipped
WAI conformance / does this change touch it (pull_request) Successful in 18s
WAI energy receipts / does this change touch it (pull_request) Successful in 15s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 11s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Has been skipped
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Has been skipped
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 6s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been skipped
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been skipped
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 6s
wai-pack / wai-pack · linux (pull_request) Has been skipped
wai-pack / wai-pack · linux-arm64 (pull_request) Has been skipped
wai-pack / wai-pack · macos (pull_request) Has been skipped
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been skipped
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been skipped
wai-pack / wai-pack · dependency licences (pull_request) Has been skipped
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m36s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 5s
wai-quantum studio and desktop / studio · browser (pull_request) Has been skipped
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been skipped
wai-web wasm32 / does this change touch it (pull_request) Successful in 8s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been skipped
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m20s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 18s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 5m21s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 11s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 27s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m38s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 9m14s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m56s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 49s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 19m40s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 11m10s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 31m12s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 20m1s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 6m18s
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
Merge origin/main into joule-moq/multi-draft
wai/CHANGELOG.md: both entries kept, this branch's above main's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-21 19:04:02 -04:00

554 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: MoQ Streaming Format
> Status: Draft, and deliberately tracking rather than pinning — MOQT is
> pre-Working-Group-Last-Call and its text still moves. Declares WAI as a
> **streaming format** over Media over QUIC Transport, so a WAI envelope is a
> first-class MOQT object rather than an opaque payload inside someone else's
> container. No new transport: WAI defines none by charter, and this extension
> does not change that. Field semantics and rules over them only — never MOQT's
> byte encoding.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Drafts read for this revision: `draft-ietf-moq-transport-22` (MOQT),
> `draft-ietf-moq-msf-01` (MSF) and `draft-ietf-moq-cmsf-01` (CMSF), all
> Internet-Drafts of the IETF MoQ working group, none past Working Group Last Call.
> A section number of MOQT, MSF or CMSF in this text is one of those revisions; §6
> says what happens when they move.
## 1. Why a streaming format
MoQ separates two things that other stacks fuse:
- **MOQT** (`draft-ietf-moq-transport`) moves objects, in groups, on tracks, and
orders them by priority. It does not know what an object means.
- **MSF** (`draft-ietf-moq-msf`) says what objects mean: it fragments a stream
into objects, maps each object to a stream of its own, and declares what a
publisher emits in a **catalog**.
WAI's confidentiality extension seals MoQ objects end to end (secure objects keyed by
MLS; see [confidentiality](confidentiality.md)), and leaves what the objects mean to
this one. This one treats MoQ as what it is — a two-layer design with an open slot at the upper
layer, which is precisely where a container that carries capability-dispatched
objects belongs.
The alignment is structural, not aspirational. MSF fragments a stream into
objects in groups; WAI envelopes are already objects, and
[jwp-receipts](jwp-receipts.md) already commits per object and signs per group.
The two layers agree on the unit without either being changed.
## 2. Precedent: the slot is open, and cheaply
MSF's catalog names a format in its `packaging` field, over a value set MSF's own
text fixes (MSF §5.2.4: `loc`, `mediatimeline`, `eventtimeline`, `moqlog`,
`moqmetrics`; the catalog itself is the track named `catalog`, MSF §5). There is
no central streaming-format registry. Read alone, that is closed.
`draft-ietf-moq-cmsf` shows it is not. CMSF is a sibling Internet-Draft that
declares `packaging` value `cmaf`, stating that it *extends the allowed packaging
values defined in MSF*; inherits MSF wholesale — *all of the specifications,
requirements and terminology defined in MSF apply to implementations of this
extension*; adds two track-level catalog fields; and registers nothing: **this
document has no IANA actions**. LOCMAF is a second instance of the same move.
So the path is a sibling draft with no registry action and no permission sought.
This extension is the WAI-side statement of what such a draft would say.
## 3. The packaging value
A track carrying WAI envelopes **MUST** declare `packaging` value `"wai"`.
- Every object on such a track **MUST** be exactly one WAI envelope (`WAI1`
magic + manifest + payload), whole. An object carrying a fragment of an
envelope, or more than one, is not this packaging.
- The object boundary **MUST** be the envelope boundary. This is what makes the
receipt's per-object commitment (`jwp-receipts` §2.1) and MOQT's object
identity the same unit; if they diverge, every guarantee that composes them is
void.
- A sink that does not recognise `"wai"` **MUST NOT** attempt to interpret the
objects. MSF's tolerant-parser rule covers unknown *fields*; it does not
license guessing at unknown *packaging*.
## 4. Catalog declaration
A WAI track's catalog entry **MUST** declare, in addition to MSF's required
fields, the capabilities a sink needs to dispatch what the track carries:
| field | type | meaning |
|---|---|---|
| `wai_capabilities` | array of string | Capability strings (WAI SPEC.md §4) that objects on this track may dispatch to. |
| `wai_determinism` | string | The weakest tier any object on the track claims — see [determinism-tiers](determinism-tiers.md). Absent means no tier is claimed. |
| `wai_receipts` | boolean | Whether objects carry receipts per §5. |
| `wai_receipt_alias` | integer | The `track_alias` the publisher signs in this track's receipts ([jwp-receipts](jwp-receipts.md) §2.4.1), from 0 to 2^53 − 1. |
| `wai_receipts_track` | string | The name, in this track's namespace, of the track that carries this track's receipts (§4.1). |
| `wai_ai_usage` | object | The usage statement of the track's objects ([ai-usage](ai-usage.md) §3), or of the room record in effect for a room's track. Absent means the catalog states none. |
| `wai_priors` | array of object | OPTIONAL. The parameter sets the track's objects pin (SPEC §3.1), by digest, each with the tier it is offered at (§4.2). Absent means the catalog states none. |
- `wai_receipt_alias` names the track's receipt chain, which a verifier keeps per
publisher key and `track_alias` (jwp-receipts §3 step 3). MOQT's own Track Alias is
chosen for each session by the publisher of that session, a relay included (MOQT
§3.1.3), so past the first hop it is not the number the receipts sign; a sink
finds a track's chain by this member. It is an integer written as one (SPEC §2:
not `8.0`, not `"8"`), compared as a number; a catalog that carries any other value
for it, on any track, carries no stage set with receipts (§4.1, check 6). Two
tracks of one catalog **MUST NOT** carry the same value: each track that declares
receipts has a chain of its own (§4.1). §4.1 requires it, and
`wai_receipts_track`, on the tracks of a stage set that declare receipts; on
another track both are OPTIONAL.
- `wai_capabilities` **MUST** be exhaustive for the track. A publisher that
dispatches to a capability it did not declare has broken the catalog's
purpose, which is letting a sink decide **before** subscribing whether it can
play what it is about to receive.
- A sink **MUST NOT** subscribe on the assumption that an undeclared capability
will not appear, and **MUST** treat an undeclared capability at dispatch as a
protocol error rather than falling back silently.
- `wai_determinism` is the **weakest** tier on the track, not the strongest.
Declaring the strongest would let a track advertise a guarantee most of its
objects do not meet.
- `wai_ai_usage` lets an agent know a track's AI usage preference before it
subscribes. The catalog is unsigned and the objects' own statements are
signed with them: a catalog whose statement differs from an object's is refused
(`catalog_conflicts`), and the object's governs (ai-usage §4.5, §5.3).
These are custom catalog fields under MSF's extension rule (MSF §5: custom field
names "MUST NOT collide" with MSF's, and a parser ignores a field it does not
understand); they are namespaced by their `wai_` prefix and MUST NOT collide with
MSF-defined names.
### 4.1 Staged tracks
A stage set ([staged-delivery](staged-delivery.md)) is carried as **one track per
layer**: a **base track**, which carries the set's bases, and a **layer track** for
each layer index, which carries the layers of that index. Every track keeps §3: each
object is one whole `WAI1` envelope. A subscriber chooses its stage by the tracks it
subscribes to; a relay forwards tracks and never reads an envelope.
**The catalog.** Each track of a set **MUST** carry, in its catalog entry,
```json
"wai_staged": { "set": "cam1", "layer": 1, "lead_ms": 40 }
```
| member | type | meaning |
|---|---|---|
| `set` | non-empty string | The set's name: the same on every track of the set. |
| `layer` | integer, 0–255 | 0 on a base track; *k* on a track that carries layer *k*. |
| `lead_ms` | integer from 0 to 2^53 − 1, or `null` | On a layer track of a live set, the lead of its layer (staged-delivery §8): the time before a unit's presentation by which the layer must arrive to be applied to it. `null` on a base track. A sink reads it only while the set is live: when `isLive` is false it ignores it, as MSF §5.2.8 has a sink ignore `targetLatency`. |
A set's extent is the tracks present: its layers are 0 to the highest layer a track
of the set states, each with at least one track. The catalog states no count of
layers, so an MSF delta update (MSF §5.1.6, §5.3), which adds and removes tracks and
changes none it declared before, changes a set's extent by adding or removing a layer
track alone. More than one track **MAY** carry one layer, as renditions of it. Each
rendition is a track of its own, with a receipt chain of its own: a clone (MSF §5.1.6)
inherits every member of its parent but its name, so a clone of a track that declares
receipts **MUST** override `wai_receipt_alias` and `wai_receipts_track` (*Receipts*,
below).
A layer track's `wai_capabilities` lists the refinement capabilities its envelopes
carry; a sink that does not implement staged delivery finds them unsupported (SPEC
§4) and has no reason to subscribe. A member of `wai_staged` this revision does not
define is ignored, an earlier revision's `layers` among them. A publisher puts on a
track of layer *k* only envelopes whose `refines.layer` is *k*, and on a base track
only envelopes with no `refines`; a sink applies an envelope by its own `refines`
(staged-delivery §4.1, §13), whichever track it came on.
**Names and types.** A track is named by its namespace and its name (MSF §5.2.2,
§5.2.3); one that declares no namespace inherits the catalog track's (MSF §5.2.2). Two
tracks of a catalog **MUST NOT** share one namespace|name pair, and the tracks of a set
**MUST** share one namespace. The check below needs the catalog track's namespace,
which a sink knows, since it subscribed to the catalog track: a checker that does not
know it compares a track that inherits only with tracks that inherit, and refuses a set
that mixes declared and inherited namespaces (`staged-malformed`). On a track of a set, `renderGroup`, where present,
**MUST** be an integer that fits 64 unsigned bits, written as one (SPEC §2), and is
compared as such: `true`, `1.0` and `"1"` are not `1`. `depends`, where present,
**MUST** be an array of strings.
**MSF fields.** The tracks of a set **MUST** have the same `isLive` (MSF §5.2.7) and
**MUST** share one `renderGroup` (MSF §5.2.11), which is present on each. On a live
set, MSF already requires one `targetLatency` across a render group (MSF §5.2.8); it
is the target latency staged-delivery §8 adds to a unit's timing, unless the sink
applies its own. Every entry of a track's `depends` (MSF §5.2.14) **MUST** name a track
of the catalog in the track's own namespace, where MSF resolves it. The `depends` of a
track of layer *k* **MUST** name at least one track of the set at each layer below
*k*, and **MUST NOT** name a track of the set at *k* or above; a base track's **MUST
NOT** name a track of the set. An `altGroup` (MSF §5.2.12) **MAY** group the base
tracks of alternative sets, and **MUST NOT** be set on a layer track: a subscriber
picks one track of an alternate group, and a layer is never an alternative to its base
or to another layer. A layer track **MUST NOT** carry `temporalId` or `spatialId` (MSF
§5.2.16, §5.2.17), which number a codec's own layers; a layer's index is
`wai_staged.layer` here, and `refines.layer` in the envelope.
**Receipts.** Every track of a set that declares `wai_receipts` true **MUST** carry
`wai_receipt_alias` (§4) and `wai_receipts_track`: the name, in its namespace, of its
**receipts track**, of `packaging` `"wai-receipts"`. Each media track has a receipts
track of its own, so each layer has a receipt chain of its own and a subscriber
verifies the tracks it subscribes to without the receipts of those it does not
(jwp-receipts §6.1); and a subscriber subscribes to the receipts tracks of the media
tracks it subscribes to and no others, so the tracks it chooses cap the receipts it
receives as they cap its stage. A track **MUST NOT** carry an alias another track of
the catalog carries, or name a receipts track another track names: a rendition of a
layer carries an alias and names a receipts track of its own.
- Group *g* of a receipts track carries one object: the group's receipts in the binary
form of jwp-receipts §2.7.3 (`WJR1`), the group receipt and the object receipts of
group *g* of its media track. A publisher publishes it no later than that group's
first object. A subscriber reads it by jwp-receipts §2.7.4, and refuses an object
that does not read (`malformed`) as it refuses one whose receipts do not verify.
- A receipt is about one MOQT object, and **MUST** say which: the group receipt in
group *g* of a receipts track has `group_id` *g*, the MOQT Group ID of the media
track's group, and each object receipt in it has that `group_id` and, as
`object_id`, the MOQT Object ID of its object. Its `signer_pubkey` is the
publisher's key and its `track_alias` the media track's `wai_receipt_alias`. A
subscriber refuses a receipt that breaks this (`moqt_mismatch`, jwp-receipts §3.3)
before it verifies the receipt's signature, and holds nothing of it.
- A receipts track has no delivery timeout of either kind, and its publisher priority
is its media track's. A subscriber sets no delivery timeout on its subscription to a
receipts track (MOQT §9.20.3, §9.20.4); a receipt it loses anyway it recovers as
below.
- Its objects are receipts, not envelopes, and §3 does not apply to them. The
`WAI_RECEIPT` property (§5) names an object's group receipt but does not carry it.
**Verifying, recovering and joining.** A subscriber verifies each group receipt as
jwp-receipts §3 does, on the chain of the publisher key and the track's
`wai_receipt_alias`, and admits a group only after its parent (§3 step 3). Each object
of a receipts track travels on a stream of its own (MSF §6), so receipts can arrive out
of order, and a relay can drop one at its discretion (MOQT §5.2). It holds no receipt
of the track's object until it verifies it, and until then applies no layer it carries
(staged-delivery §10.1).
- A subscriber that subscribed to a receipts track from its first group, and that has
received every object of it, holds the receipt of every object of its media track,
delivered or not, and checks one it did not receive by inclusion (jwp-receipts §3,
*Verified by inclusion*). Where a receipts-track object is missing, the receipts after
it on that track wait for it, within the bound below.
- A receipt whose parent the subscriber lacks is held (`parent_not_held`). The
subscriber recovers the parent by a FETCH (MOQT §3.2) of the receipts track's groups
from its chain's head to the held group: group *g* of the receipts track holds the
receipts of group *g* of its media track.
- A subscriber **MUST** state a bound on what it holds for missing parents
(jwp-receipts §3 step 3, *Holding*), each count a finite number: a count of receipts
held live for each chain, and a time from each one's arrival only beside it, never
alone; a count of receipts held as history for each chain; and a count across all its
chains. Past the live bound it refuses the layers whose receipts it holds, their
receipts refused (`parent_not_recovered`) and the layers refused for want of them
(staged-delivery §10.1), and re-anchors the chain at the next validly signed group of
it that it receives (*Re-anchoring*). The layers of the groups the anchor skips are
refused for want of their receipts, unless history later fills them.
- After a re-anchor, a FETCH of the gap (MOQT §3.2) can deliver its groups in any
order, ascending among them: the verifier admits as history each group that links
forward from the last group admitted before the gap or back from the anchor
(jwp-receipts §3 step 3, *History*), never as a fork, and holds as history, under the
history count, one that links to neither yet.
- A subscriber that joins a live track after its first group (MOQT §3.5) declares a
join: its verifier anchors the chain at the first validly signed group of it that it
receives, and holds the history before it as unverified (jwp-receipts §3 step 3,
*Joining*). Joining at an earlier group (MOQT §3.5: a SUBSCRIBE whose
`FILL_PARAMETERS` select it, its objects delivered on a fill fetch stream, §3.4;
earlier revisions of MOQT called this a Joining FETCH) retrieves earlier receipts.
The fill fetch stream delivers them in ascending order, so each waits for the one
after it: the verifier holds them as history, under the history count and outside
the live one, and admits them, back from the anchor, where their signed links lead to
it.
A MOQT relay forwards a receipts track as it forwards any track, its objects opaque
payload (MOQT §7.8): it neither reads nor verifies the receipts in it. It is
therefore not a relay that conforms to jwp-receipts §4, which verifies a group's
signature before fanning its objects out; the subscriber verifies every receipt itself
(jwp-receipts §4.1, *Receipts carried as payload*).
**Checking a catalog.** A sink checks each set's entries in this order, and stops at
the first that fails:
1. `wai_staged` is an object with the three members above, of their types and in their
ranges; the track's `packaging` is `"wai"` and its `isLive` a boolean; its name is a
string, its namespace absent or a string, and the pair is no other track's; its
`renderGroup`, if present, an integer that fits 64 unsigned bits; its `depends`, if
present, an array of strings; and, where the checker does not know the catalog
track's namespace, either every track of the set declares a namespace or none does
(`staged-malformed`). An entry whose `set` cannot be read belongs to no set, and is
reported for the empty name.
2. The set has a track at each layer from 0 to the highest a track of it states
(`staged-set-incomplete`).
3. Every entry of every track's `depends` names a track of the catalog in the track's
namespace; a track of layer *k* names a track of the set at each layer below *k*,
and none at *k* or above (`staged-depends`).
4. The tracks have one namespace, one `isLive` and one `renderGroup`, present on each,
and no layer track carries `altGroup`, `temporalId` or `spatialId`
(`staged-grouping`).
5. On a live set, `lead_ms` is an integer on every layer track and `null` on every
base track (`staged-lead`). A set that is not live is not checked here.
6. Every `wai_receipt_alias` of the catalog, on any track, is an integer from 0 to
2^53 − 1; every track of the set that declares `wai_receipts` true carries an alias
and names, in its namespace, a receipts track of `packaging` `"wai-receipts"`; no
other track of the catalog carries its alias; and no other track names its receipts
track (`staged-receipts`).
A sink treats the layer tracks of a set that fails as absent. Its base tracks are
tracks of packaging `wai` like any other, and the sink **MAY** subscribe to them.
**Groups and objects.**
- Group and object ids of different tracks need not align. `refines` names what a
layer refines, and a sink checks it (staged-delivery §4.1); nothing is read from
where a group falls on another track.
- On a live track each group **MUST** carry exactly one object, which travels in a
subgroup of its own (MOQT §2.2): MSF maps every object to a new stream (MSF §6), so
none travels as a datagram. A group of one object is complete when its object is, so
its group receipt is signed before the object is sent, and the latency the receipt
adds to a layer's arrival (staged-delivery §8) is one signature and its carriage, not
a group's duration.
**Priority.** MOQT schedules first by subscriber priority, the priority a subscriber
gives its request (MOQT §5.1.2, §9.20.7), and only between objects of equal subscriber
priority by publisher priority, the lower number first, 0 the highest (MOQT §5.1.1,
§5.1.2). The base track's publisher priority **MUST** be a number strictly below every
layer track's; the priority of a track of layer *k* **MUST** be no greater than that of
any track of a layer above *k*; and a receipts track's is its media track's. These hold
for each track's `DEFAULT_PUBLISHER_PRIORITY` (MOQT §10.4) and for every subgroup that
sets its own (MOQT §5.1.1). A subscriber's own priorities can still put a layer before
its base: one that wants the publisher's order **SHOULD** give its requests for a set's
tracks one subscriber priority.
**Delivery timeouts.**
- A publisher **SHOULD** set a live layer object's `OBJECT_DELIVERY_TIMEOUT` (MOQT
§5.2, §10.2) to no more than its **slack** at publication: the time from publication
to the presentation of the last unit the object covers, less the layer's `lead_ms`,
less the publisher's estimate of the path. MOQT reads a timeout set as an object property on the first
object of a subgroup (MOQT §5.2, §10.2); here that object is its subgroup's only one,
so the publisher **MAY** set each object's timeout on the object, from its own slack.
A slack of zero or less cannot be set, since a timeout of 0 is no timeout (MOQT
§5.2): such an object cannot arrive in time for any unit it covers, and a publisher
**MAY** withhold it.
- A publisher **MUST NOT** set on a base track a delivery timeout of either kind
shorter than the time from a base's publication to the presentation of its last
unit.
- `OBJECT_DELIVERY_TIMEOUT` runs at each hop, from the time the object's last header
byte reaches that hop, and is checked when the hop passes the object to its transport
(MOQT §5.2). `SUBGROUP_DELIVERY_TIMEOUT` starts instead when the subgroup's FIN
arrives, and runs until all its data is acknowledged (MOQT §5.2): on a subgroup of
one object, it bounds that object's delivery after it was sent. Where both the
publisher and the subscriber set a value, MOQT uses the smaller (MOQT §5.2, §9.20.3,
§9.20.4), so a sink whose own target latency is longer than the one a publisher
assumed cannot extend the publisher's timeout. Either timeout releases, hop by hop,
bandwidth that a late layer would waste; neither is an end-to-end deadline: an object
can pass every hop within its timeouts and still arrive late. The rule of
staged-delivery §8 is the only rule that decides whether a layer is late.
**Capping.** A subscriber caps its stage by subscribing to the base tracks and to the
tracks of layers 1 to *n*, and to their receipts tracks, and to no others. A relay
**MUST NOT** read an envelope to cap layers: it treats every payload as opaque (MOQT
§7.8), and forwards the tracks it is asked for. On a track of packaging `wai` the
prototype header fields of staged-delivery §13 are not read.
**Example.** The entry of the first layer track of a live set, with its base track
`cam1/base`, and the entry of its receipts track:
```json
{ "namespace": "example/cam1", "name": "cam1/L1", "packaging": "wai",
"isLive": true, "targetLatency": 300, "role": "video", "renderGroup": 1,
"depends": ["cam1/base"],
"wai_capabilities": ["wai.video.int_refine"], "wai_determinism": "decode-equivalence",
"wai_receipts": true, "wai_receipt_alias": 8, "wai_receipts_track": "cam1/L1/receipts",
"wai_staged": { "set": "cam1", "layer": 1, "lead_ms": 20 } }
{ "namespace": "example/cam1", "name": "cam1/L1/receipts", "packaging": "wai-receipts",
"isLive": true }
```
### 4.2 Pinned parameter sets
A capability that decodes against a parameter set names that set in each envelope, by a
pin: the SHA-256 of the whole set, in a declared form, with the tier the sender claims
(SPEC §3.1). A track **MAY** also state, in its catalog entry, the pins its objects carry.
A sink can then tell before it subscribes whether it holds the parameter sets the track
needs, and which tier each is offered at:
```json
"wai_priors": [
{ "capability": "wai.neural.int_hyper",
"sha256": "<64 lowercase hexadecimal digits>",
"form": "wai.prior.set", "tier": "decode-equivalence" }
]
```
| member | presence | meaning |
|---|---|---|
| `capability` | REQUIRED, string | A capability the track's `wai_capabilities` lists. |
| `sha256` | REQUIRED, string | A pin's `sha256`: 64 lowercase hexadecimal digits. |
| `form` | OPTIONAL, string | The pin's `form`. Absent, or `null`, where the pin declares none. |
| `tier` | OPTIONAL, string | The tier the publisher offers the set at, from SPEC §3.1's vocabulary. Advisory (below). |
An entry **lists** a pin for a capability when its `capability` is that capability, its
`sha256` is the pin's, and its `form` is the pin's: both absent, or the same string. A
member of an entry that this revision does not define is ignored.
- **Reading.** `wai_priors`, where present and not `null`, is an array of objects, each
with the members above of their types and no two with the same `capability`, `sha256`
and `form`, and each `capability` one the track's `wai_capabilities` lists. A sink that
reads anything else refuses the track (`catalog_priors_malformed`): it does not
subscribe to it. A `tier` this revision does not register is read as `none` (SPEC §3.1).
- **Exhaustive.** Where a track carries `wai_priors`, the publisher **MUST** list in it
every pin its objects carry. An object **breaks the catalog** when:
1. it carries a pin that no entry lists, either for its `capability` or for its
`fallback`; or
2. an entry names its `capability`, or its `fallback` (not `null`), and the object
carries no pin, or carries a pin that no entry lists for that capability.
A publisher whose objects' pin can decode through the fallback (SPEC §4 step 5)
**SHOULD** list the set for the fallback as well, so that a sink that holds only the
fallback can make the same decision before it subscribes.
- **Mismatch.** A sink checks each object of such a track against the track's
`wai_priors` before it dispatches the object (SPEC §4). An object that breaks the
catalog is refused (`catalog_prior_mismatch`). The track is refused with it: the sink
dispatches no further object of the track, and **MAY** unsubscribe from it. The
catalog never stands in for the envelope. A sink verifies an object's pin under SPEC
§3.1 whether or not the catalog lists it. An entry never satisfies a pin, never
supplies a pin the envelope lacks, and never changes which set a decode reads.
- **Tier.** A listed `tier` is advisory. A publisher **MUST NOT** list a tier above the
one the capability's registry class gives (SPEC §3.1). A sink **MUST NOT** claim a tier
on the strength of a listed tier: the tier it claims for an object is the one its own
decode earns, under SPEC §3.1 and §7 and [determinism-tiers](determinism-tiers.md) §3,
whatever the catalog says. A listed tier that differs from the tier an object's pin
states is not a mismatch.
- `wai_determinism` (§4) still states the weakest tier any object on the track claims.
Reference implementation: `staged::moq::CatalogPriors` reads the member
(`CatalogPriors::read`) and checks an object against it (`CatalogPriors::check`,
`CatalogPriors::check_manifest`). `staged-conformance/moq/priors.json` holds a case for
each reason and for each way an object can break the catalog. `wai_staged_vectors`
checks the cases, and so does `staged_verify.py`, which shares no code with `wai-rs`.
The browser sink of `joule-moq-web` dispatches no capability that decodes against a
parameter set, and does not read the member.
## 5. The per-object property
MOQT carries metadata a relay can see as **Properties** of a track or of an object
(MOQT §8.4), registered in a table MOQT creates, with ranges set aside for an
application's own use. MSF §12 defines its properties this way; the one it defines,
`MSF_COMPRESSION`, is at both scopes. CMSF adds track-level catalog fields and
defines **no per-object property**. None of them carries a receipt.
A WAI receipt is already per-object. This extension therefore defines one
object-scope property, `WAI_RECEIPT`, whose value is the object's receipt
commitment:
| field | type | meaning |
|---|---|---|
| `content_hash` | 32 B | `jwp-receipts` §2.1 BLAKE3 over the envelope bytes. |
| `joules_micro` | u64 | Microjoules producing this object; `0` is the unmetered marker, never 0 J ([energy-measurement](energy-measurement.md) §2). |
| `acquisition` | string | Acquisition class per [energy-measurement](energy-measurement.md) §2; empty when `joules_micro` is `0`. |
| `acquisition_evidence` | bytes | For `OnChipCounter` and `CalibratedInstrument`, the evidence that follows the tag byte in `energy-measurement.md` §2.7 (declared uncertainty, then filtering state or calibration reference). Empty for every other class. |
| `group_receipt` | 32 B | The link hash of the group receipt covering this object (`jwp-receipts` §3 step 3). |
On the wire the property is one Key-Value-Pair (MOQT §8.3) of an odd type. Its value is
the five fields in the order of the table, with nothing after them: `content_hash` (32
bytes); `joules_micro` (a MOQT variable-length integer, MOQT §8.1); `acquisition` (a
variable-length integer giving its length, then its UTF-8 bytes); `acquisition_evidence`
(a variable-length integer giving its length, then the bytes); and `group_receipt` (32
bytes). A sink refuses a value that ends early, has bytes left after `group_receipt`, or
whose `acquisition` is not UTF-8 (`property_invalid`). The publisher sets the property
inside the object's Immutable Properties (MOQT §10.7), which no relay may change or
remove. No property type is registered for it. The Internet-Draft source in `wai/ietf/`
requests one from the Specification Required range of MOQT's property registry (MOQT
§16.8). Until one is assigned, a track of packaging `wai` carries the property as type
`0x3A41`, from the range MOQT §8.4 sets aside for an application's own use. Another
application may give that type another meaning, so a sink reads it only on a track of
packaging `wai`.
- A non-zero `joules_micro` **MUST** be accompanied by `acquisition`. A joule
figure without its acquisition class is exactly what `energy-measurement.md`
§2 forbids, and putting it on the wire does not make it acceptable.
- A zero `joules_micro` is the unmetered marker: `acquisition` **MUST** be
empty and `acquisition_evidence` empty, since a class beside a zero figure
claims a measurement that did not occur (`energy-measurement.md` §2). A sink
**MUST NOT** present the marker as a figure of zero joules, and **MUST**
discard a property that breaks this bullet or the one above
(`property_invalid`).
- `acquisition_evidence` **MUST** be present and non-empty for a refined
hardware class (`OnChipCounter`, `CalibratedInstrument`), and empty
otherwise — including for legacy `HwShunt`, which carries no evidence. A
refined class without its declared uncertainty is refused by
`energy-measurement.md` §2 on the same terms as a figure without a class. A
property written before this field existed has no `acquisition_evidence`; a
reader treats its absence as empty, which is valid only for the classes that
carry none.
- No signature covers this property. A sink **MUST NOT** report `acquisition`
or `acquisition_evidence` as signed unless they and `joules_micro` equal a
signed statement it holds of the same object, which is one of:
- the label in the object's Merkle leaf, once the object receipt has been
accepted under `jwp-receipts` §3 against the group receipt
`group_receipt` names — `group_receipt` must then name that group; or
- an `energy` claim about the object that it has accepted under
`jwp-receipts` §3.1. A claim names no group, so `group_receipt` is outside
what it states.
Where such a statement gives another figure, class, evidence or group, the
sink **MUST** discard the property (`property_disagrees`). Where it holds
none, the class is outside every signature, and the sink reads the figure as
unlabelled.
- A class read as signed is the statement of the key that signed it: the group
receipt's `signer_pubkey` for a leaf, the claim's `signer_pubkey` for an
`energy` claim. A sink **MUST** report that key with the class, and **MUST
NOT** report a class a claim signs as the publisher's unless the claim's
signer is a key it holds as the publisher's. Anyone can sign an `energy`
claim, a relay that rewrites this property included.
- A relay **MUST** preserve this property unchanged, per `jwp-receipts` §4.1.
A relay that recomputes it is asserting something it did not measure.
- A relay that does not forward an object, whether because a delivery timeout
passed or at its discretion (MOQT §5.2), **MUST** still forward that object's
receipts to every subscriber that requested receipts for its track (jwp-receipts
§4.1). This property rides on the object and is lost with it; the receipts ride
the track's receipts track (§4.1), which the relay forwards as it forwards any
track, so a subscriber of that receipts track that receives the receipt accounts
for the object as `inclusion-verified` (jwp-receipts §4.4). A receipts-track object
the relay drops in turn is recovered as §4.1 says. A relay that signs a `delivery-gap` claim (jwp-receipts
§2.4.11) does not add it to the publisher's receipts track, whose objects it
forwards unaltered (MOQT §7.8); this extension defines no carriage for such a
claim.
- Telemetry about objects on a WAI track is
[delivery-telemetry](delivery-telemetry.md)'s. It is unsigned and advisory, and it
**MUST NOT** restate this property's figure, class or evidence.
- Absence of the property is not evidence of absence of a receipt; receipts MAY
ride elsewhere (§1 of `jwp-receipts` lists sidecar and trailer carriage). A
sink **MUST NOT** infer that an object without the property is unattested.
## 6. What this extension does not do
Stated so it is not re-argued:
- It does **not** define, profile, or restate MOQT's wire encoding — not the
object header, not the byte layout of properties, not the control messages.
Those are MOQT's, and the property that makes this binding cheap is that WAI
rides a transport instead of defining one. §4.1 paraphrases MOQT's scheduling
order, its delivery timeouts and its joining (MOQT §5.1, §5.2, §3.5) only to say how
a stage set uses them and which values it sets; it defines none of them, and where a
paraphrase and MOQT differ, MOQT holds.
- It does **not** replace CMSF or LOC. A publisher with CMAF-packaged media
should use CMSF; this packaging is for tracks whose objects are WAI envelopes.
- It does **not** make MoQ WAI's transport. WAI remains carrier-agnostic; this
is one binding among the several `jwp-receipts` §1 anticipates.
- It is not the only binding. [http-delivery](http-delivery.md) binds the same
objects to HTTP, admits `WAI2` envelopes with verified component ranges, carries
`WAI_RECEIPT` in a sidecar index, and maps this extension in its Appendix C.
- It does **not** pin a MOQT draft revision. MOQT is pre-Last-Call; an extension
that pinned a moving target would be wrong within a meeting cycle. Where MOQT
and this text disagree, MOQT wins and this text is the thing that must change.
The revisions named in the status note are the ones this text was last read
against: where a later revision renumbers or renames what it cites, the
citation is what changes.
- Nothing in it depends on which MOQT draft carries the objects: an envelope, its
`WAI_RECEIPT` property inside Immutable Properties, a receipts track and its
catalog fields are the same on every draft that has Immutable Properties. The
reference implementation (`joule-moq` and `joule-moq-relay`, in
`joule-code/joule-code-rs/crates/`) speaks `draft-ietf-moq-transport-22`, `-18`
and `-16`, the draft chosen per session by ALPN or `WT-Available-Protocols`, and
carries this extension's objects and receipts unchanged on each. Its README
records the relays and drafts it has carried them through, and when.
## 7. Framing
The delivery industry is converging on MoQ, and the layer above it is where
formats declare themselves. A container that carries instructions, dispatches by
capability, and commits per object has a natural home there — not because it
competes with the transport, but because the transport deliberately left the
question of meaning to someone else.