open-standards/wai/extensions/frame-stream.md
Transaction Science 9067a39b5a
Some checks failed
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 1m50s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m11s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 1m9s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m9s
wai-audit collector and player integration / does this change touch it (push) Successful in 17s
wai-app and Create / wai-app · does this change touch it (push) Successful in 21s
wai-fleet service / does this change touch it (push) Successful in 24s
wai-key-proxy / does this change touch it (push) Successful in 20s
WAI operation catalogue / does this change touch it (push) Successful in 30s
wai-ledger service / does this change touch it (push) Successful in 20s
wai-ops-gpu integer operators on GPUs / does this change touch it (push) Successful in 16s
wai-pack / wai-pack · does this change touch it (push) Successful in 23s
wai-quantum for JavaScript / does this change touch it (push) Successful in 23s
wai-sink-android native sink library / does this change touch it (push) Successful in 21s
wai-sink-swift native sink package / does this change touch it (push) Successful in 15s
wai-quantum studio and desktop / does this change touch it (push) Successful in 29s
wai-quantum studio and desktop / desktop app · windows (push) Waiting to run
WAI Studio / does this change touch it (push) Successful in 18s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 18s
WAI examples / WAI examples · does this change touch them (push) Successful in 15s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 13s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 58s
wai-key-proxy / proxy · linux (push) Successful in 1m35s
wai-fleet service / service · linux (push) Successful in 3m11s
wai-ledger service / service · linux (push) Successful in 2m47s
wai-gstreamer / wai-gstreamer · the plugin on a distribution's GStreamer (push) Successful in 7m7s
wai-audit collector and player integration / collector · linux (push) Successful in 5m33s
wai-pack / wai-pack · linux (push) Successful in 4m16s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 2m11s
wai-ops-gpu integer operators on GPUs / Vulkan GPUs (framework-128gb) (push) Successful in 10m45s
wai-ops-gpu integer operators on GPUs / Vulkan GPUs (jetson-hub) (push) Successful in 11m0s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m58s
wai-quantum studio and desktop / studio · browser (push) Successful in 5m45s
wai-quantum for JavaScript / javascript · linux (push) Successful in 7m20s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 11m46s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 9m58s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (push) Successful in 16m36s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 8m25s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 12m34s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 15m40s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 15m37s
WAI Studio / Studio in WebKit · linux (push) Successful in 34m49s
WAI examples / WAI examples · portable · linux (push) Successful in 33m46s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 47s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m54s
wai-fleet service / service · macos (push) Successful in 35s
wai-ledger service / service · macos (push) Successful in 1m14s
wai-pack / wai-pack · macos (push) Successful in 3m46s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m19s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (push) Failing after 8m27s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m27s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m2s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m18s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 4m13s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m24s
wai-pack / wai-pack · the floor through a media prober (push) Failing after 1m21s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 11m15s
wai-pack / wai-pack · linux-arm64 (push) Successful in 5m42s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m34s
wai-pack / wai-pack · dependency licences (push) Successful in 5m7s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 8m14s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 11m1s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 8m19s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 10m15s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 18m23s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 14m58s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Failing after 27m45s
wai-pack / wai-pack · independent verifier (Python) (push) Successful in 23m51s
Native sinks: the integer frame forms decoded one frame an object, each frame's networks on the dispatched unit
native-sink §13.6 (new): a host opens a stream of its integer decoder for a stream of
wai.video.int_motion.frame, wai.video.int_hyper.frame or wai.video.int_cond.frame
(frame-stream §3) and gives it each object in order. The stream holds frame-stream §5's
reference and makes its checks through the reference decoders (frame_stream::FrameStream over
IntFrames and CondFrames); each frame is dispatched as a whole payload is, and the answer adds
frame {kind, index, content_hash}. A decode run twice (the GPU failed during it) restarts from
the same reference, so the stream advances once.

- wai-rs: sink_decode::frames (IntFrameStream, IntDecoder::decode_frame); IntFrames and
  CondFrames gain Clone and rebind, FrameStream map_codec, so a stream keeps its state while the
  host's set is lent to it per object. C interface: wai_int_frames_open, wai_int_decode_frame,
  wai_int_frames_reset, wai_int_frames_free (include/wai.h regenerated by build.rs).
- wai-sink-swift: WaiIntFrameStream and WaiIntDecoder.decode(frame:stream:); IntDecodeTests
  decodes a stream of each frame form (staged by build-xcframework.sh with its listing).
- tests/sink_decode_frames.rs, in wai-ops-gpu.yml's Metal and Vulkan legs: 21 streams, 95
  objects (70 frames, 25 refusals) of int-payload-conformance/frame-stream and int-cond, each
  frame its committed SHA-256 and each refusal its code with the GPU and on the CPU; every
  stream again with each graph on the GPU and the interpreter, compared; the C interface held
  to the Rust one. Run on 2026-11-11 on Metal (arm64 laptop) and Vulkan (0x1002, Linux): 26
  frames' networks on the GPU, 98 graph runs compared, none differing; clippy clean under
  sink_decode and video_int; the frame_stream and int_cond lib tests, tests/int_cond_video.rs and
  wai_int_payload_vectors verify passed. Swift: desktop 11 tests (7 of 16 frames on the GPU) and
  both simulators passed.
- wai.audio.opus.dred.int is not added: its parameter set is the draft's weights file, which the
  repository does not hold, so no decode of it can be held to the CPU's here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 17:00:35 -05:00

567 lines
38 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: Frame Streams
> Status: Draft. Carries an inter-frame video capability **one frame an object**: each
> object is one whole `WAI1` envelope holding one frame; a **group** opens with an intra
> frame, which resets the decoder state; and each later frame of the group names its
> predecessor by content hash, so that a sink refuses a lost, reordered or altered frame
> instead of decoding past it against the wrong reference. Registers four frame
> capabilities (§1), the manifest member `frame` (§2), the neural frame payload `WNF1` (§4)
> beside the integer one, `WIF1` ([integer-payloads](integer-payloads.md) §16), and the
> checks a sink makes (§5). The MoQ catalog member that declares a frame stream on a track,
> `wai_frame_stream`, is [moq-streaming-format](moq-streaming-format.md) §4.3. §9 maps a
> neural frame stream to and from a track of the neural video packaging of the MOQT
> Streaming Format (NMSF, `draft-herz-moq-nmsf-01`), byte for byte where both sides state a
> frame. No transport is defined or changed: a frame stream rides any carrier that delivers
> objects in order.
> Reference implementation: `wai-rs` — `frame_stream::{FrameStream, IntFrames, ParseOnly,
> split_wiv1, split_dcvc, join_dcvc, catalog, nmsf}`, `neural::DcvcFrames`. Conformance:
> [`int-payload-conformance/frame-stream/`](../int-payload-conformance/frame-stream/).
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
## 1. Scope
An inter-frame capability's registered payload holds a whole clip: `WIV1` codes every frame
of a clip in one residual stream (integer-payloads §3), and the reference neural
inter-frame payload holds every frame's bitstreams behind one header (§6). A live sender
has one frame at a time, and a carrier that moves objects (MoQ, SPEC §1) moves each frame
as it is made. Each **frame capability** below carries one frame of its **base**'s
bitstream as its payload:
| frame capability | base | payload | parameter set | registry class |
|---|---|---|---|---|
| `wai.video.int_motion.frame` | `wai.video.int_motion` | `WIF1`, keyframe 0 (integer-payloads §16) | none | `StandardDefined` |
| `wai.video.int_hyper.frame` | `wai.video.int_hyper` | `WIF1`, keyframe 1 (integer-payloads §16) | the base's: `entropy.json`, `g_s.whm`, `h_s.whm` | `NeuralIntegerExact` |
| `wai.video.int_cond.frame` | `wai.video.int_cond` | `WCF1` (integer-payloads §17) | the base's: `model.wcb` | `NeuralIntegerExact` |
| `wai.neural.dcvc_rt.frame` | `wai.neural.dcvc_rt` | `WNF1` (§4) | the base's, sink-supplied | `NeuralFloat` |
| `wai.neural.dcvc_fm.frame` | `wai.neural.dcvc_fm` | `WNF1` (§4) | the base's, sink-supplied | `NeuralFloat` |
A frame capability is a capability of its own because it is a different framing of its
base's bitstream, and a registered string's payload format is fixed (SPEC §8);
`wai.audio.opus.packet` is the precedent, a capability of one Opus packet whose decoder
state carries from packet to packet. Each is a payload capability, media class `video`,
with its base's fidelity tier and licence class, and decodes against its base's parameter
set, of its base's shape: a pin on a frame object names the set its base's pin would name.
## 2. Groups and the `frame` member
A **frame stream** is a sequence of objects of one frame capability, in the order a sink
receives them. It is made of **groups**: a group is one intra frame, then zero or more
inter frames, in order. An intra frame's decode reads nothing but its own payload (and the
parameter set); an inter frame's reads the state the frames before it in its group left
(for `WIF1`, the previous decoded frame and its intra frame's table; for `WNF1` and
`WCF1`, the reference feature).
Every envelope of a frame capability carries the manifest member `frame`, an object:
| member | type | meaning |
|---|---|---|
| `index` | integer, 0 to 2^53 − 1 | the frame's position in its group: 0 for its intra frame, the frame before it plus 1 for every other |
| `prev` | string, or `null` | on an inter frame, the content hash ([jwp-receipts](jwp-receipts.md) §2.1) of the envelope of the frame before it in its group, as 64 lowercase hexadecimal digits; `null` on an intra frame |
| `number` | OPTIONAL, integer, 0 to 2^53 − 1 | the frame's sequence number in its stream, as its publisher counts it: on every frame of a group whose intra frame carries one, each the one before it plus 1; on none of a group whose intra frame carries none |
| `capture_ms` | OPTIONAL, integer, 1 to 2^53 − 1 | the wallclock at which the frame was captured, in milliseconds since 1970-01-01T00:00:00Z, as its publisher states it; absent where it states none |
```json
"frame": { "index": 3, "prev": "<64 lowercase hexadecimal digits>", "number": 1203, "capture_ms": 1793491239699 }
```
- A member of `frame` this revision does not define is ignored. `index`, `number` and
`capture_ms` are integers written as one (SPEC §2).
- `number` and `capture_ms` are inside the envelope, so the content hash, the next frame's
`prev` and the object's receipt cover them with the frame. A sink holds a group's
`number`s to their sequence (§5) and reads `capture_ms` as its publisher's statement.
- `prev` names the predecessor's whole envelope, its manifest included, and so its own
`prev`: the hash of a group's last frame commits to every frame of the group, in order,
as a receipt chain does.
- The content hash is over the envelope's bytes as they travel, the bytes an object
receipt names (jwp-receipts §2.1). A sink that receives an encrypted envelope (SPEC §6)
takes the hash of what it received, before it opens the payload.
- An encoder writes `fallback` `null`: no other capability reads a frame's payload.
- Every frame of a group carries the pin its intra frame carries: both have no
`conditioning.prior`, or both have the same value.
`frame` is an OPTIONAL manifest member under SPEC §8's rule for a MINOR revision: an
envelope that does not carry it keeps its bytes and its meaning. It is carried exactly
when the capability is a frame capability. A reader of an earlier revision does not
advertise a frame capability, so it finds such an envelope's capability unsupported and
the envelope inert (SPEC §4).
## 3. The integer frame forms
`wai.video.int_motion.frame` and `wai.video.int_hyper.frame` carry `WIF1`
(integer-payloads §16). A group's intra frame carries the group's residual table, its `q`
and `offset`, and frame 0: with keyframe 0 its residual symbols, predicted from grey; with
keyframe 1 the base's nested `WIH1`, the learned keyframe. Each inter frame carries its
motion vectors, its `mv_precision` and `block`, and its residual symbols, in a stream of its
own. The decode of each frame is `WIV1`'s frame step (integer-payloads §3), through the
same stage operators ([integer-operators](integer-operators.md) §13.5; the keyframe through
`whm1/1`, §13.2), so:
- a frame decodes to the same bytes on every conforming sink, given its chain: each frame
capability has its base's registry class, and its frames' frame-state-equivalence digests
(SPEC §7 *Digests*) are portable;
- a stream split from a clip (§6) decodes, frame by frame, to the clip's frames, byte for
byte, and to the digests the clip's decode gives.
`wai.video.int_cond.frame` carries `WCF1` (integer-payloads §17): one frame of conditional
inter-frame coding, whose latent is coded given the feature the frame before it in its group
left, and which leaves its own feature to the frame after it. Its intra frame reads the zero
feature. Each frame is decoded by integer-payloads §17.4 through its pinned set, so the same
two properties hold: its frames reproduce on every conforming sink given their chain, and a
group split from a `WIC1` clip (integer-payloads §17.2, a copy of the clip's frames) decodes
to the clip's frames, byte for byte.
## 4. `WNF1` — one frame of the reference neural inter-frame payload
`wai.neural.dcvc_rt.frame` and `wai.neural.dcvc_fm.frame` carry `WNF1`: one frame's two
bitstreams, behind the clip header every frame of its group repeats.
| off | size | field | rule |
|---|---|---|---|
| 0 | 4 | magic `WNF1` | |
| 4 | 1 | version | `1` |
| 5 | 1 | `kind` | `0` intra, `1` inter |
| 6 | 2 | reserved | `0` |
| 8 | 4 | `h` | ≥ 1 |
| 12 | 4 | `w` | ≥ 1 |
| 16 | 4 | `fps_milli` | frames per second × 1000 |
| 20 | 2 | `cy` | ≥ 1: the latent's channels |
| 22 | 2 | `cz` | ≥ 1: the hyper-latent's channels |
| 24 | 1 | `sy_log2` | ≤ 15: the latent's side is `sy = 2^sy_log2` |
| 25 | 1 | `sz_log2` | ≤ 15: the hyper-latent's side is `sz = 2^sz_log2` |
| 26 | 2 | `q` | the quality index the entropy tables are read at |
| 28 | 2 | `cf` | ≥ 1: the reference feature's channels |
| 30 | 2 | reserved | `0` |
| 32 | 4 | `lz` | the hyper-latent bitstream's length |
| 36 | 4 | `ly` | the latent bitstream's length |
| 40 | `lz` | the hyper-latent bitstream | |
| … | `ly` | the latent bitstream | |
Nothing follows the latent bitstream. Every multi-byte field is little-endian.
**Checks**, in this order, each with its code from integer-payloads §8: the magic
(`bad_magic`); the version byte, present (`stream_length`) and 1 (`version`); the fixed
header of 40 bytes (`stream_length`); the reserved fields (`reserved_nonzero`); `kind`, then
`h`, `w`, `cy`, `cz` and `cf` at least 1, then `sy_log2` and `sz_log2` at most 15
(`field_range`); the frame `h·w·3`, the latent `cy·sy·sy`, the hyper-latent `cz·sz·sz` and
the reference feature `cf·sy·sy`, each at most `2^26` elements (`too_large`); the two
bitstreams (`stream_length`); nothing after them (`trailing`).
**Decode.** The bitstreams are the capability's own, as its reference decoder reads them
(`neural/video_dcvc.rs`): the latents are entropy-decoded from the two bitstreams against
the base's parameter set at `q`, and the base's temporal network maps the latents and the
reference feature to the frame and the next reference feature. An intra frame starts the
reference feature at zero; an inter frame reads the one the frame before it left, and
carries its intra frame's header, every field from `h` to `cf` (`geometry_mismatch`). A
decode the capability's decoder cannot complete is refused `payload_malformed`, and a pin
that does not resolve with SPEC §3.1's code. The decode runs floating point: its frames
reproduce on one build and not across hardware (`NeuralFloat`, SPEC §7), so these strings
never back `intent = replicate`. The chain of §2 still holds every frame to its
predecessor: two sinks may present different pixels, never different frames.
## 5. What a sink checks
A sink holds, for each frame stream it decodes, its **reference**: the last frame it
decoded, its content hash, its `index` and its pin, and the decoder state the frame left. It
checks each object it receives, in this order, and refuses it at the first rule that fails:
1. The envelope reads (SPEC §2): `envelope_malformed`.
2. Its capability is the stream's: `frame_capability_mismatch`.
3. It carries `frame`, an object: `frame_link_missing`.
4. `frame`'s `index` is an integer from 0 to 2^53 − 1, its `prev` is `null` or 64
lowercase hexadecimal digits, its `number`, where present, is an integer from 0 to
2^53 − 1, and its `capture_ms`, where present, is an integer from 1 to 2^53 − 1:
`frame_link_malformed`.
5. The payload parses alone: integer-payloads §16 for `WIF1`, §17.1 for `WCF1`, §4 for
`WNF1`, each with its
code.
6. `frame` agrees with the payload's kind: an intra frame has `index` 0 and `prev` `null`,
an inter frame an `index` of 1 or more and a `prev`: `frame_link_mismatch`.
An **intra frame** then drops the reference, is decoded (SPEC §3.1's pin checks, then the
decode's codes), and becomes the reference. An **inter frame** is checked against the
reference first:
7. The sink holds a reference: `frame_no_reference`.
8. Its `prev` is the reference's content hash: `frame_gap` when its `index` is past the one
after the reference's (frames are missing), `frame_prev_mismatch` otherwise.
9. Its `index` is the reference's plus 1: `frame_index_mismatch`. Its `number` is the
reference's plus 1 where the reference carries one, and absent where the reference
carries none: `frame_number_mismatch`.
10. Its `conditioning.prior` is the reference's, compared as JSON values (both absent
counts as the same): `frame_prior_changed`.
11. It is decoded against the reference: for `WIF1`, its `h × w` is its group's
(`geometry_mismatch`), then the decode's codes; for `WCF1`, its header, `h` to `cf`, is
its group's (`geometry_mismatch`), then the decode's codes (integer-payloads §17.4); for
`WNF1`, its header is its group's (`geometry_mismatch`), then the decode's.
It then becomes the reference. **After any refusal the sink holds no reference**: every
inter frame it receives is refused (`frame_no_reference`) until an intra frame opens a group.
So a sink never decodes an inter frame against a reference that is not its predecessor's
decode: a lost frame, a frame delivered twice or out of order, and an altered `prev` are
each refused at the first frame they would corrupt, and the stream resumes at the next
group. A sink that joins a stream joins at an intra frame.
| code | when |
|---|---|
| `envelope_malformed` | the object is not a `WAI1` envelope that reads |
| `frame_capability_mismatch` | the object's capability is not the stream's |
| `frame_link_missing` | no `frame` member, or one that is not an object |
| `frame_link_malformed` | an `index` or `prev` outside its type or range |
| `frame_link_mismatch` | an intra frame with a `prev` or an `index` other than 0, or an inter frame with no `prev` or `index` 0 |
| `frame_no_reference` | an inter frame while the sink holds no reference |
| `frame_gap` | an inter frame whose predecessor is not the reference, with frames missing between |
| `frame_prev_mismatch` | an inter frame whose `prev` is not the reference's content hash |
| `frame_index_mismatch` | an inter frame that names the reference but whose `index` is not the next |
| `frame_number_mismatch` | an inter frame whose `number` is not the one after the reference's, or that carries one where the reference carries none, or none where it carries one |
| `frame_prior_changed` | an inter frame whose pin is not its group's |
A payload's refusals are integer-payloads §8's codes; a pin's are SPEC §3.1's.
## 6. Splitting a clip
**`WIV1`.** A `WIV1` clip is one group of `WIF1` frames, frame for frame. Frame 0 is the
intra frame: the clip's table, `q` and `offset`, and, with keyframe 0, frame 0's residual
symbols; with keyframe 1, the clip's nested `WIH1`. Each frame `f ≥ 1` is an inter frame:
frame `f`'s motion vectors, the clip's `mv_precision` and `block`, and frame `f`'s residual
symbols. Each frame's symbols are the ones the clip's residual stream decodes to for that
frame, coded into a stream of their own by the rANS coder integer-payloads §2 decodes (its
state starting at `2^31`, so each stream ends canonically). The group decodes, frame by
frame, to the clip's frames, byte for byte (§3). The split refuses a clip whose residual
stream does not decode: `stream_exhausted`, `stream_noncanonical`, and the parse's codes.
A stream of several groups is the split of several clips, one after another.
**The reference neural payload.** The reference decoder of `wai.neural.dcvc_rt` and
`wai.neural.dcvc_fm` reads a whole clip as: `h`, `w`, `n_frames`, `fps_milli` (`u32`),
`cy`, `cz` (`u16`), `sy_log2`, `sz_log2` (`u8`), `q`, `cf` (`u16`), 26 bytes; then
`n_frames` pairs of `u32` lengths `(lz, ly)`; then each frame's hyper-latent and latent
bitstreams, in order. Its split is one group of `WNF1` frames, each with the clip's header
and its own two bitstreams, copied; frame 0 intra, every other inter. The split reads the
clip strictly: its header as `WNF1`'s checks read it, at least one frame (`field_range`),
every bitstream inside it (`stream_length`) and nothing after the last (`trailing`). The
**join** is its inverse: one group of `WNF1` frames, the first intra and the others inter
(`field_range`), each with the first's header (`geometry_mismatch`), back to the clip. The
split and the join give each other's input back, byte for byte, and the decode of the
frames is the decode of the clip: the reference decoder's whole-clip decode runs the same
step a frame (`neural/video_dcvc.rs`, `Temporal::step`).
## 7. Carriage
A frame stream rides any carrier that delivers its objects in order; this extension
defines no transport. Each object keeps every rule of the carrier for a `WAI1` object, and
its receipt is an object receipt like any other ([jwp-receipts](jwp-receipts.md)).
- **MoQ.** One track carries one frame stream, its catalog entry declaring it
(`wai_frame_stream`, [moq-streaming-format](moq-streaming-format.md) §4.3). Each MOQT group
of the track is one group of the stream, and the object of Object ID *k* in it is the frame
of `index` *k*. A subscriber joins at a group's first object (MOQT §3.5). A frame a relay
drops is refused as a gap at the frame after it, never decoded past; the group's receipts
still account for it (moq-streaming-format §5). A frame stream of a neural frame
capability maps to and from a track of packaging `nvc` (§9).
- **HTTP and files.** A sequence of envelopes, in order: a playlist's objects
([http-delivery](http-delivery.md)), or a file of envelopes one after another.
A frame object is complete when its frame is coded, so a live sender publishes each frame
as it is made, and a sink presents it when it is decoded.
## 8. Reference implementation and conformance
- `wai-rs` (feature `video_int`): `frame_stream::FrameStream` holds a stream's reference and
makes §5's checks in order, decoding through a `FrameCodec`: `IntFrames`, the decoder of
the integer frame forms (`int_video::decode_unit_typed`, the clip decode's frame step, and,
for a keyframe, `int_payload::decode_keyframe` through the capability's set as its pin
resolves it); and `ParseOnly`, which parses each frame and checks its geometry against its
group's intra frame without decoding. `frame_stream::split_wiv1`, `split_dcvc` and
`join_dcvc` are §6's; `frame_stream::envelope` and `wrap_group` write frame objects;
`frame_stream::catalog` reads `wai_frame_stream` and checks an object against it;
`container::check_int_payload_framing` parses `WIF1`, `WCF1` and `WNF1` on every emit
path. `int_cond_video::CondFrames` decodes `wai.video.int_cond.frame`, keeping the reference
feature from frame to frame (integer-payloads §17.5); its corpus is
[`int-payload-conformance/int-cond/`](../int-payload-conformance/int-cond/).
`neural::DcvcFrames` (features `neural` and `video_int`) decodes the neural frame forms
through the reference decoder's `Temporal::step`. A native sink that decodes in its own
process takes the integer frame forms through `sink_decode::frames` (feature `sink_decode`),
each frame's networks on the device's GPU or its CPU ([native-sink](native-sink.md) §13.6).
- [`int-payload-conformance/frame-stream/`](../int-payload-conformance/frame-stream/), written
by `wai_int_payload_vectors gen` and checked by `wai_int_payload_vectors verify`: the
committed `WIV1` clips `int_video`, `video_real` (half-pel) and `video_nk` (a learned
keyframe, pinned) split into frame objects, each frame decoding to its clip's frame byte
for byte; two clips as one stream of two groups; a stream with each of §5's faults planted;
`WIF1` and `WNF1` payloads each changed in one place; a synthetic neural clip, its split,
its join and their refusals; and catalog entries with objects checked against them. The
neural clip's bitstreams are generated bytes, not an encode: the corpus holds its framing
and its chain, and no decode, which needs the sink-supplied networks.
- `int_payload_verify.py`, which shares no code with wai-rs, checks the same corpus from
this text: it splits each clip again with its own rANS coder and requires the committed
payloads, decodes every keyframe-0 frame and requires each frame's SHA-256, makes §5's
checks over every stream with its own BLAKE3, and checks every payload, split, join and
catalog case. It does not decode a `WIH1` keyframe; those streams it checks to §5 and the
frames' parse, and wai-rs checks their decode.
## 9. NMSF tracks
`draft-herz-moq-nmsf-01` (NMSF) packages the frames of a neural inter-frame codec for the
MOQT Streaming Format as packaging `nvc`: one frame an object, a group of pictures a MOQT
group that opens with a neural keyframe (NMSF §3.5), the model's weights installed on both
sides and named, not sent (NMSF §8). A frame stream of `wai.neural.dcvc_rt.frame` or
`wai.neural.dcvc_fm.frame` carries the same bitstreams, so a publisher maps one to the
other: an NMSF frame becomes one `WNF1` frame object (a **rewrap**, §9.4), and a frame object
becomes one NMSF object, or two (an **unwrap**, §9.5). Neither changes a bitstream; an
unwrap of a rewrap gives the NMSF objects back byte for byte; and a relay forwards both
tracks as it forwards any track. A section number of NMSF here is one of that revision's;
where a later revision renumbers or changes what this section cites, this section is what
changes.
### 9.1 What maps
An NMSF object is a 26-byte header and a payload (NMSF §3.2). This section reads the header
from the field definitions, which give each field's size and sum to the 26 bytes the section
states; the figure before them draws `pts_ms` one byte short. Every field is big-endian:
| off | size | field |
|---|---|---|
| 0 | 1 | `frame_type`: `0x00` Intra, `0x01` Inter, `0x02` to `0xFF` reserved (NMSF §3.3) |
| 1 | 1 | `qp`: 0 to 63; 64 to 255 reserved |
| 2 | 4 | `frame_number` |
| 6 | 8 | `pts_ms` |
| 14 | 4 | `width` |
| 18 | 4 | `height` |
| 22 | 4 | `payload_len` |
| 26 | `payload_len` | the payload |
The payload is **components** (NMSF §3.6), each `channels`, `height`, `width` and
`data_len` (`u32`, big-endian) and then `data_len` bytes of an entropy-coded tensor. In
single-track mode an object's payload is the hyperprior's component and then the latent's;
in two-track mode the hyperprior track's object holds the hyperprior's component and the
latent track's object the latent's, the two objects of a frame with the same header (NMSF
§3.4). A frame object holds the same frame as:
| NMSF | frame object | where it has no form |
|---|---|---|
| `codec` `dcvc-rt`, `dcvc-fm` (NMSF §5) | the capability `wai.neural.dcvc_rt.frame`, `wai.neural.dcvc_fm.frame`; `conditioning.kind` `dcvc_rt`, `dcvc_fm` | any other codec: `nmsf_codec_unmapped`. `dcvc-dc` is SPEC §5's candidate `wai.neural.dcvc_dc`, which pins no bitstream and has no frame form; `ssf`, `fvc`, `rlvc` and `elfvc` WAI does not register |
| a MOQT group: an Intra frame, then Inter frames | a group of the stream (§2); the object of Object ID *k* is the frame of `index` *k* (moq-streaming-format §4.3) | |
| `frame_type` Intra, Inter | `WNF1` `kind` 0 (`index` 0, `prev` `null`), 1 (`index` 1 or more, `prev` the frame before) | a reserved type: `nmsf_reserved` |
| `qp` | `WNF1` `q` | a reserved `qp`: `nmsf_reserved`; a `q` past 63 (§9.5): `nmsf_value_unmappable` |
| `frame_number` | `frame.number` (§2) | a frame with no `number`, or one past 2^32 − 1 (§9.5): `nmsf_value_unmappable` |
| `pts_ms`; 0 where it states none | `frame.capture_ms` (§2); absent where it states none | a `pts_ms` past 2^53 − 1: `nmsf_value_unmappable` |
| `width`, `height` | `WNF1` `w`, `h` | |
| the hyperprior's component | `WNF1` `cz`, `sz_log2` (the side is `2^sz_log2`) and the hyper-latent bitstream | a tensor that is not square, whose side is not a power of two at most 2^15, or of channels outside 1 to 65 535 (§4): `nmsf_shape_unmappable` |
| the latent's component | `WNF1` `cy`, `sy_log2` and the latent bitstream | as the hyperprior's |
| the catalog's `framerate` | `WNF1` `fps_milli` | a `framerate` that is not a whole number of thousandths (§9.2), or a frame of another `fps_milli` (§9.5): `nmsf_value_unmappable` |
| — | `WNF1` `cf`, the reference feature's channels, from the model binding (§9.3) | |
| `codec` and `nvc.modelVersion`, the model by name | `conditioning.prior` and `wai_priors`, its parameter set by SHA-256 (SPEC §3.1), through the model binding | |
### 9.2 The catalog member `wai_nmsf`
A WAI track whose frames map to an NMSF track keeps that track's catalog entries in the
member `wai_nmsf`: an array of one entry (single-track mode) or two (two-track mode, a
`hyperprior` and a `latent` track, NMSF §3.1), each a JSON object as NMSF §3.7 and §3.8 state
it, in the order given. The frame objects do not carry `colorspace`, `gopSize`, `priority`,
`nvcRole`, `depends` or the `nvc` object, and `wai_nmsf` keeps them, so the entries an unwrap
publishes are the entries the rewrap read, member for member. A rewrap publishes the entry:
```json
{ "name": "video/wai", "packaging": "wai", "isLive": true, "role": "video",
"width": 64, "height": 64, "framerate": 30,
"wai_capabilities": ["wai.neural.dcvc_rt.frame"],
"wai_frame_stream": { "capability": "wai.neural.dcvc_rt.frame" },
"wai_priors": [ { "capability": "wai.neural.dcvc_rt.frame",
"sha256": "<64 lowercase hexadecimal digits>",
"form": "wai.prior.set", "tier": "none" } ],
"wai_nmsf": [ { "name": "video", "packaging": "nvc", "isLive": true, "codec": "dcvc-rt",
"width": 64, "height": 64, "framerate": 30, "colorspace": "ycbcr-bt709",
"gopSize": 4, "nvc": { "modelVersion": "cvpr2025" } } ] }
```
Its `name` is the publisher's; `isLive`, `width`, `height` and `framerate` are the first NMSF
entry's; it declares the frame stream (moq-streaming-format §4.3); `wai_priors` (§4.2) lists
the binding's pin, with its `form` and `tier` where the pin states them, and is absent where
the binding has no pin; `wai_nmsf` holds the NMSF entries.
**Reading NMSF entries.** A publisher reads them, in this order, and refuses the track at
the first rule that fails:
1. Each entry is an object with `name` (a non-empty string), `packaging` `"nvc"`, `isLive`
(a boolean), `codec` and `colorspace` (strings), `width`, `height` and `gopSize`
(integers of at least 1) and `framerate` (a number above 0); `priority`, where present,
a number; `nvcRole` and `depends`, where present, strings; and `nvc`, where present, an
object whose `modelVersion`, where present, is a string. There is one entry, with no
`nvcRole`, or two: one of `nvcRole` `"hyperprior"` and one of `"latent"`, whose `depends`
is the hyperprior entry's `name`, the two names different. Otherwise
`nmsf_catalog_malformed`.
2. Two entries agree on `codec`, `isLive`, `width`, `height`, `framerate` (as numbers),
`colorspace`, `gopSize` and `nvc.modelVersion` (both absent counts as the same):
`nmsf_track_mismatch`.
3. The codec has a frame capability (§9.1): `nmsf_codec_unmapped`.
4. `fps_milli` is the integer *m* nearest `framerate` × 1000, from 1 to 2^32 − 1, where *m*
÷ 1000 is `framerate` in binary64 arithmetic: 29.97 is 29 970, and 30 000 ÷ 1 001 has
none (`nmsf_value_unmappable`).
**Reading a WAI entry.** An unwrap reads the entry's frame stream (moq-streaming-format
§4.3, `catalog_frame_stream_malformed`; a track that declares none has no NMSF form,
`nmsf_no_form`); its `wai_nmsf` (absent or `null`: `nmsf_no_form`; not an array:
`nmsf_catalog_malformed`), whose entries read as above and whose codec's frame capability is
the declared one (`nmsf_catalog_malformed`); and the pin `wai_priors` lists for that
capability: each of its entries an object, and one for the capability with a `sha256` of 64
lowercase hexadecimal digits and a `form` that is absent, `null` or a string
(`nmsf_catalog_malformed`). It lists none, and the frames carry no pin; or one, and they
carry it; or several, and the track has no NMSF form, which names one model
(`nmsf_no_form`). A publisher that unwraps a frame stream it wrote itself states its NMSF
entries in `wai_nmsf`: the colorspace and GOP size its frames do not carry are its to state.
### 9.3 The model binding
NMSF names a model by `codec` and `nvc.modelVersion` and leaves the distribution of its
weights to the deployment (NMSF §8). A frame object names its parameter set by SHA-256 in its
pin, which a sink checks before it decodes (SPEC §3.1). A **model binding** is the
publisher's statement of the set a name stands for, and of what `WNF1` needs of the model
that an NMSF object does not carry:
```json
{ "codec": "dcvc-rt", "modelVersion": "cvpr2025", "cf": 64,
"prior": { "id": "dcvc-rt/cvpr2025", "sha256": "<64 lowercase hexadecimal digits>",
"determinism": "float", "form": "wai.prior.set", "tier": "none" } }
```
| member | presence | meaning |
|---|---|---|
| `codec` | REQUIRED, string | the NMSF codec identifier |
| `modelVersion` | string, `null` or absent | the `nvc.modelVersion` it binds; `null` or absent binds a track that names none |
| `cf` | REQUIRED, integer, 1 to 65 535 | the channels of the model's reference feature, `WNF1`'s `cf` (§4) |
| `prior` | a pin (SPEC §3.1), `null` or absent | the parameter set every frame object carries as its pin; `null` or absent, the frames carry none |
A binding that does not read so is `nmsf_binding_malformed`. A rewrap maps a track through
the first binding whose `codec` and `modelVersion` are the track's: none is
`nmsf_model_unbound`. The neural frame forms are `NeuralFloat`, so a binding's pin claims no
tier above `none` (SPEC §3.1). A binding is the publisher's configuration and is not carried:
in the catalog a rewrap publishes, its name is in `wai_nmsf` and its pin in `wai_priors`, so an
unwrap reads both from there.
### 9.4 Rewrap: NMSF to frame objects
A publisher maps an NMSF track's frames in the order it receives them: each frame one object,
or in two-track mode the hyperprior track's object and the latent track's object of the
frame (NMSF §4.2), with the MOQT Object ID where the carrier gives one. It holds the
**group** it is mapping: its last frame's `index`, `frame_number`, `WNF1` header and frame
object's content hash. It checks each frame in this order, and refuses it at the first rule
that fails:
1. The frame is one object in single-track mode and two in two-track mode:
`nmsf_track_mismatch`.
2. Each object, in order, is at least 26 bytes and its `payload_len` is the length of the
bytes after its header (`nmsf_object_malformed`), and its `frame_type` and `qp` are not
reserved (`nmsf_reserved`).
3. The two objects of a frame have the same header, `payload_len` aside:
`nmsf_track_mismatch`.
4. Components fill each payload: two in single-track mode, one in each object in two-track
mode: `nmsf_object_malformed`.
5. An Intra frame drops the group, and is at Object ID 0 where it has one. An Inter frame
follows a group, its `frame_number` the group's last plus 1 and its Object ID, where it
has one, the group's last `index` plus 1. Otherwise `nmsf_group_order`.
6. Its `pts_ms` is at most 2^53 − 1: `nmsf_value_unmappable`.
7. Each component has a `WNF1` form (§9.1), and the `WNF1` header the frame gives passes §4's
checks: `nmsf_shape_unmappable`.
8. An Inter frame's `WNF1` header is its group's, so its `qp`, `width`, `height` and tensor
shapes are its Intra frame's: `nmsf_group_header_changed`.
It then writes the frame object: a `WAI1` envelope of the codec's frame capability, `media`
`video`, `intent` `perceptual`, `conditioning.kind` the codec's (§9.1) and `conditioning.prior`
the binding's pin, with no fallback (§2). Its payload is the `WNF1` frame of `kind`
`frame_type`, `h` `height`, `w` `width`, `fps_milli` the track's (§9.2), `cy`, `cz`,
`sy_log2` and `sz_log2` the components', `q` `qp`, `cf` the binding's, and the components'
data as its two bitstreams. Its `frame` is `index`, `prev`, `number` `frame_number` and
`capture_ms` `pts_ms`, absent where `pts_ms` is 0. The frame object becomes the group's
last.
After any refusal the publisher holds no group: it refuses every Inter frame
(`nmsf_group_order`) until an Intra frame opens one. A lost or damaged NMSF object therefore
yields no frame object for any frame its loss would have changed. A single-track frame and the
two objects of the same frame in two-track mode give the same frame object, which a sink
admits as any frame (§5).
### 9.5 Unwrap: frame objects to NMSF
A publisher whose WAI entry reads (§9.2) maps each frame object in order, and refuses it at
the first rule that fails:
1. It is admitted as a sink admits a frame (§5), its payload parsed and held to its group's
header as `ParseOnly` holds it: §5's codes and §4's.
2. An intra frame's pin is the one the entry lists: the same `sha256` and `form`, or no pin
where the entry lists none: `nmsf_model_unbound`. §5 holds each inter frame's pin to its
group's.
3. Its `frame.number` is present and at most 2^32 − 1: `nmsf_value_unmappable`.
4. Its `WNF1` `q` is at most 63 and its `fps_milli` is the track's: `nmsf_value_unmappable`.
It then writes the NMSF object, or the two: the header `frame_type` `kind`, `qp` `q`,
`frame_number` `number`, `pts_ms` `capture_ms` (0 where absent), `width` `w`, `height` `h`;
the hyperprior's component (`cz` channels, a side of `2^sz_log2` both ways, the hyper-latent
bitstream) and the latent's (`cy`, `2^sy_log2`, the latent bitstream). In single-track mode
one object holds both components; in two-track mode the hyperprior track's object holds the
first and the latent track's object the second, the same header on both. The frame's `cf` is
not carried. After any refusal the publisher holds no reference (§5), and refuses every
inter frame until an intra frame opens a group.
### 9.6 What each side carries, and the codes
A frame object carries what an NMSF object does not:
- the link to the frame before it (§2): a sink refuses a lost, reordered or altered frame at
the first frame it would change, and resumes at the next group (§5);
- a receipt per object ([jwp-receipts](jwp-receipts.md)), which covers the frame with its
`number` and `capture_ms`;
- the parameter set by SHA-256 (SPEC §3.1), which a sink checks against the set it holds;
- a catalog from which a sink chooses, before it subscribes, a track it can decode: a frame
stream, or beside it a track of a floor or recommended video capability (SPEC §5) for a
sink that holds no networks;
- the integer frame forms (§3), whose frames reproduce on every machine; NMSF registers no
codec for them, so a frame stream of `wai.video.int_motion.frame` or
`wai.video.int_hyper.frame` has no NMSF form.
An NMSF track carries what a frame stream does not:
- two-track delivery, the hyperprior on a track of its own at a priority of its own (NMSF
§3.1, §3.8). A frame object holds both bitstreams; an unwrap to two-track mode gives the
NMSF subscriber both tracks;
- `colorspace`, `gopSize`, `priority` and the `nvc` object, which `wai_nmsf` keeps (§9.2);
- a `qp` that changes inside a group, which `WNF1` holds once a group
(`nmsf_group_header_changed`); tensors of any shape, where `WNF1` holds square tensors whose
side is a power of two, as the reference decoder takes them (§4, `nmsf_shape_unmappable`);
and frame rates that are not whole thousandths (`nmsf_value_unmappable`).
| code | when |
|---|---|
| `nmsf_catalog_malformed` | an NMSF entry, or `wai_nmsf` or `wai_priors` in a WAI entry, does not read (§9.2) |
| `nmsf_track_mismatch` | two NMSF entries disagree (§9.2), or the two objects of a frame are not two, or differ in their headers (§9.4) |
| `nmsf_codec_unmapped` | an NMSF codec with no frame capability |
| `nmsf_model_unbound` | no model binding for the track's codec and model version (§9.3), or an intra frame whose pin is not the one the WAI entry lists (§9.5) |
| `nmsf_binding_malformed` | a model binding that does not read (§9.3) |
| `nmsf_object_malformed` | an object shorter than its header, whose `payload_len` is not its payload's length, or whose payload is not its components (§9.4) |
| `nmsf_reserved` | a reserved `frame_type` or `qp` |
| `nmsf_group_order` | an Intra frame past Object ID 0, an Inter frame with no group, or not the next `frame_number` or Object ID |
| `nmsf_value_unmappable` | a `pts_ms`, `framerate`, `number`, `q` or `fps_milli` the other side cannot state |
| `nmsf_shape_unmappable` | a tensor or frame `WNF1` cannot state (§9.1) |
| `nmsf_group_header_changed` | an Inter frame whose `qp`, size or tensor shapes are not its group's |
| `nmsf_no_form` | a WAI track with no frame stream, no `wai_nmsf`, or several pins |
### 9.7 Reference implementation and conformance
- `wai-rs` (feature `video_int`): `frame_stream::nmsf` — `parse_object`, `write_object`,
`parse_components` and `write_component` (§9.1); `read_nmsf`, `Track::bind`,
`Track::wai_entry` and `read_wai` (§9.2); `Binding` (§9.3); `Rewrap` (§9.4) and `Unwrap`
(§9.5), which runs `FrameStream` over `ParseOnly`. `frame_stream::Stamp` reads `number` and
`capture_ms` (§2), and `FrameStream` makes §5's checks of them.
- [`int-payload-conformance/frame-stream/nmsf/`](../int-payload-conformance/frame-stream/nmsf/),
written by `wai_int_payload_vectors gen` and checked by `wai_int_payload_vectors verify`: a
64 × 64 stream of two groups as NMSF objects in single-track and two-track mode and the
frame objects each maps to, the same frame objects for both modes; NMSF objects changed in
one place each, for every rule of §9.4; NMSF entries and WAI entries for every rule of §9.2;
model bindings that do not read; frame objects an unwrap refuses, among them the reference
neural stream of §8, whose frames carry no `number`; and streams whose `number` or
`capture_ms` a sink refuses (§5). The objects' bitstreams are generated bytes, not an
encode: the corpus holds the framing and the mapping, and no decode. Every stream's frame
objects unwrap back to its NMSF objects byte for byte, and every code of §9.6 is given by
a case.
- `int_payload_verify.py`, which shares no code with wai-rs, checks the same corpus from
this text: it reads every NMSF object and entry with its own parser, maps every stream
and requires each committed frame object's payload and manifest, admits the frame objects
with its own §5 checks and BLAKE3, unwraps them and requires the NMSF objects byte for
byte, and checks every catalog, binding, unwrap and links case.