open-standards/wai/extensions/packaging.md
Transaction Science cd6dcf41bb SPEC §7 Signatures: the one Ed25519 rule, cited by the receipt extensions
Every Ed25519 signature WAI defines is verified strictly, in order: a canonical
key, a key not of small order, S < L, R not of small order, and the cofactorless
equation with R compared byte for byte. OER/2's chain signature keeps the rule
oer-receipts §5 settles. jwp-receipts §3.1 step 3, http-delivery's playlist
receipt and packaging's receipt cite it instead of restating it; the worlds,
avatars, feeds, spatial-audio, haptics, volumetric, score, film, provenance,
ternary and quantum receipt texts cite it where they name their signature.
CHANGELOG records the sweep.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 14:39:04 -04:00

998 lines
64 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: Packaging
> Status: Draft. A packager turns sources into WAI objects under a declared policy;
> this extension fixes what a verifier recomputes. This revision defines the policy,
> its digest and what it may ask a packager to emit (§3), how a source is cut into
> pieces (§4), the objects a packager writes (§5), the stage index (§6), the floor
> (§7), what each class is offered over HTTP and MoQ (§8), the package receipt (§9),
> and the package and its verification (§11). Reference impl: `wai-pack` — `policy`,
> `capcheck`, `source`, `scenecut`, `classify`, `encode`, `floor`, `cmaf`, `catalog`,
> `pack`, `verify`; in `wai-rs`, the encoder `int_video_enc`, the stage index
> `stage_index` and the receipt `package`; the corpus
> [`pack-conformance/`](../pack-conformance/). Keywords **MUST**, **MUST NOT**,
> **SHOULD**, **MAY** are RFC 2119/8174.
## 1. Scope
A **packager** turns one or more sources into WAI objects, stage indexes, catalogs
and a package receipt, under a declared **policy**. This extension fixes what a
package contains and what a verifier recomputes. It fixes an encoder's choices only
where it names them.
## 2. Terms
- **Component**: one media stream of a package (for example video, or audio).
- **Piece**: a span of a component that decodes with no reference outside itself.
- **Stage**: one object of a piece in staged order: the base is layer 0, refinement
*k* is layer *k* ([staged-delivery](staged-delivery.md)).
- **Rung**: one entry of the policy's ladder: a capability a component's pieces are
coded in, with its role (base, refinement or floor) and parameters.
- **Class**: a delivery profile the policy names.
- **Moment**: a declared span of presentation time with tags.
- **Floor**: a rendition in a standard codec, for sinks that lack the primary.
- **Timebase** of a component: `num / den` units per second. For video one unit is
one frame; for audio, one sample.
## 3. Policy
**3.1** A package is made under exactly one policy, a TOML 1.0 document of schema
`wai.pack.policy/1`, in UTF-8 with no byte-order mark.
**3.2** The **effective policy** is the policy with every default written in (§3.3)
and every override the packager applies (such as a command line forcing
`royalty_free_only`). An override only ever forces `royalty_free_only` on; it never
turns off what the policy asks for. Its JSON form is §3.3's members, with an optional member that
has no default omitted when absent, and arrays of tables in document order. Its
**policy digest** is
BLAKE3("wai:pack-policy\x01" ‖ JCS(effective policy as JSON))
where JCS is RFC 8785. Two spellings of one policy (member order, inline or standard
tables, dotted keys, string or integer notation, comments, an omitted default or the
default written out) therefore have one digest; the order of an array of tables is
part of the policy.
A packager **MUST** refuse with `policy-toml`, before anything else, a policy whose
bytes are not UTF-8, that begins with a byte-order mark, that is not TOML 1.0 (an
integer outside TOML's 64-bit range included), or that nests arrays and tables more
than 32 deep: the document's own table is at depth 1, and an array or a table
directly inside one at depth *k* is at depth *k* + 1. Then, before it reads any key, it
**MUST** refuse one holding anywhere a float (`policy-float`), a date or time
(`policy-datetime`), a negative integer (`policy-negative`), or an integer above
`2^53 − 1` (`policy-integer-range`), visiting members in byte order of their keys and
arrays in order.
**3.3** *Schema.* Within each table, a key this section does not define is refused
first (`policy-unknown-key`, the first in byte order), then each member in the order
listed: a required member that is absent (`policy-missing-key`), a member of another
type (`policy-type`), a value outside its range or vocabulary (`policy-value`). An
absent table reads as empty.
| Member | Type | Default | Range |
|---|---|---|---|
| `schema` | string | required | `wai.pack.policy/1` (else `policy-schema`) |
| `asset_id` | string | required | 1 to 255 bytes |
| `royalty_free_only` | boolean | `false` | |
| `lead` | string | required | 1 to 255 bytes |
| `component` | array of tables | required | |
| `component.id` | string | required | 1 to 255 bytes |
| `component.media` | string | required | `video`, `image`, `audio`, `world`, `feed`, `avatar`, `film` |
| `component.source` | string | absent | |
| `component.timebase` | table | required | members `num`, `den`, each 1 to `2^32 − 1`, required |
| `segmentation.target_ms` | integer | 2000 | |
| `segmentation.min_ms` | integer | 1000 | |
| `segmentation.max_ms` | integer | 4000 | |
| `segmentation.scene_cut` | string | `wai.pack.scenecut/1` | `wai.pack.scenecut/1` |
| `segmentation.cut_abs_permille` | integer | 300 | 1 to 1000 |
| `segmentation.cut_rel_permille` | integer | 3000 | 0 to 1000000 |
| `segmentation.cut_window` | integer | 8 | 3 to 64 (a window of fewer than 3 scores never applies `cut_rel_permille`, §4.3) |
| `segmentation.lookahead_frames` | integer | 6 | 0 to 64 |
| `segmentation.closed` | boolean | `true` | |
| `ladder` | array of tables | empty | |
| `ladder.id` | string | required | 1 to 255 bytes |
| `ladder.component` | string | required | 1 to 255 bytes |
| `ladder.capability` | string | required | |
| `ladder.role` | string | required | `base`, `refinement`, `floor` |
| `ladder.refines` | string | absent | |
| `ladder.params` | table | empty | by capability, below |
| `floor.supplied_listing` | string | absent | 64 lowercase hexadecimal digits |
| `floor.extra_keyframes` | boolean | `false` | |
| `live.layer_lag_ms` | array of integers | one 0 per layer | §3.4 |
| `class` | array of tables | empty | |
| `class.id` | string | required | 1 to 255 bytes |
| `class.max_layer` | integer | 0 | 0 to 255 |
| `class.max_kbps` | integer | absent | 1 to `2^32 − 1` |
| `class.floor_only` | boolean | `false` | |
| `moment` | array of tables | empty | |
| `moment.id` | string | required | 1 to 255 bytes |
| `moment.from_ms`, `moment.to_ms` | integer | required | `to_ms` above `from_ms` |
| `moment.tags` | array of strings | empty | |
| `rule` | array of tables | empty | |
| `rule.id` | string | required | 1 to 255 bytes |
| `rule.when.piece_kind` | array of strings | absent | `natural`, `static`, `synthetic`, `motion_high` |
| `rule.when.moment_tag` | array of strings | absent | |
| `rule.then.ladder` | array of strings | absent | |
| `rule.then.max_layer` | integer | absent | 0 to 255 |
| `rule.then.deadline_ms` | integer | absent | 1 to `2^32 − 1` |
| `defaults.deadline_ms` | integer | 1500 | 1 to `2^32 − 1` |
| `classify.static_permille` | integer | 5 | 0 to 1000 |
| `classify.static_mad_milli` | integer | 300 | |
| `classify.synthetic_bins` | integer | 12 | 0 to 64 |
| `classify.motion_high_milli` | integer | 9000 | |
A rung's `params` are those of its capability; every other capability takes none.
| Capability | Parameter | Default | Range |
|---|---|---|---|
| `wai.video.int_motion` | `block` | 16 | 4, 8, 16, 32 |
| | `search` | 7 | 0 to 32 |
| | `mv_precision` | 2 | 1, 2 |
| | `q_candidates` | `[8]` | a non-empty array, each 1 to 255 |
| | `max_sse_px_milli` | 0 | |
| `wai.video.int_refine` | `q` | required | 1 to 255 |
| `wai.video.av1`, `wai.video.av1.lossless` | `source` | `supplied` | `supplied`, `encode` |
| `wai.audio.int_refine` | `to_rate`, `q` | required | 1 to 384000; 1 to 65535 |
| `wai.audio.flac` | `source` | `encode` | `encode`, `supplied` |
`wai.audio.int_layered` is reserved for the layered integer audio base: it is not
registered yet, and its parameters are defined when it is.
**3.4** *Constraints.* A policy that passes §3.3 is checked in this order, and the
first that fails is the refusal:
1. 1 to 8 components (`policy-too-many-components`); at most 64 rungs
(`policy-too-many-rungs`), 32 classes (`policy-too-many-classes`), 32 rules
(`policy-too-many-rules`) and 32 moments (`policy-too-many-moments`).
2. Ids unique among components, rungs, classes, moments and rules taken together, so
a reference never names two things (`policy-duplicate-id`).
3. `lead` names a component (`policy-lead`).
4. `segmentation.closed` is true: every piece is closed, §4.1 (`policy-open-gop`).
5. `1 ≤ min_ms ≤ target_ms ≤ max_ms` (`policy-piece-bounds`).
6. Every rung names a component (`policy-ladder-component`).
7. A rung carries `refines` exactly when its role is `refinement`
(`policy-refines-role`); `refines` names a rung (`policy-dangling-refines`); no
chain of `refines` returns to a rung it passed (`policy-refines-cycle`); every
chain stays in its component and ends at a base (`policy-refines-base`); no rung is
refined twice (`policy-refines-branch`).
8. Every component has a base rung (`policy-no-base`).
9. A rung's **layer** is 0 for a base or a floor, and one more than the rung it
refines for a refinement; the policy has one more layer than its highest rung
layer. No class's and no rule's `max_layer` exceeds the highest
(`policy-class-layer`).
10. A rule's `then.ladder` names known rungs, all of one component, at least one of
them a base (`policy-rule-ladder`).
11. `live.layer_lag_ms` has one entry per layer, the first 0, the entries
non-decreasing (`policy-layer-lag`).
12. The deadline values (`defaults.deadline_ms` and every rule's `then.deadline_ms`)
number at most 4 (`policy-too-many-deadlines`), and for every refinement layer
*k* and every deadline value *d*, `d − layer_lag_ms[k] ≥ 1`
(`policy-deadline-unreachable`).
Rules fold in document order: every matching rule applies, and a later `then` member
overrides an earlier one.
**3.5** A package receipt carries the policy digest. A verifier holding the effective
policy recomputes it; it cannot check that the packager followed the policy, except
by packing again.
**3.6** *What a packager may emit.* A packager **MUST** refuse a policy with a rung it
may not emit, reading SPEC §5's registry, never the policy. For each rung in ladder
order the first rule that holds is its refusal, and a policy's refusal is its first
rung's:
1. SPEC §5 does not register the capability: `capability-unknown:<cap>`.
2. SPEC §5 registers it as a record or a derivation operation, which names no payload
format: `capability-not-payload:<cap>`.
3. It is a candidate, whose payload format no revision has pinned (SPEC §5,
*Candidate capabilities*): `capability-candidate:<cap>`.
4. A base or a floor whose capability is not standalone media, or a refinement whose
capability is not refinement-only: `capability-role:<cap>`.
5. Under `royalty_free_only`, a capability the registry gives no licence class:
`license-unclassified:<cap>`; one of any class other than royalty-free:
`license-refused:<cap>:<label>`, with the class's SPEC §5 label. The class is the
registry's, "licensed royalty-free by its standards body"; it is not an opinion on
freedom to operate, and a package states nothing about patents.
## 4. Pieces
**4.1** Every piece **MUST** be closed: its first frame, sample or tick is coded with
no reference outside the piece, and no later object of the package references into
it.
**4.2** Piece boundaries are a pure function of the source frames and the effective
policy. Where the policy names `wai.pack.scenecut/1`, the boundaries **MUST** be those
§4.3 computes. The list of piece starts, in timebase units, is the **plan**, and its
digest is
BLAKE3("wai:pack-plan\x01" ‖ u64 LE n ‖ n × u64 LE start)
**4.3** `wai.pack.scenecut/1`. All arithmetic is integer; `>>` is a shift and every
division rounds down. For each RGB8 frame *f* of `w × h` pixels:
1. Luma `Y = (77R + 150G + 29B + 128) >> 8`, and the histogram `H_f` of 64 bins,
`Y >> 2`.
2. `D(a, b) = (Σ_i |H_a[i] − H_b[i]|) · 1000 / (2wh)`: the share of pixels, in
thousandths, whose bin differs. The score of frame *f* ≥ 1 is `s_f = D(f, f − 1)`.
3. The current piece starts at frame *p*, and `len = f − p`. Its window `W_f` is the
scores `s_g` for `p < g < f`, the last `cut_window` of them; `m_f` is their lower
median (sorted ascending, the element at index `(|W_f| − 1) / 2`).
4. **Hard cut** at *f*: `len ≥ min`, `s_f ≥ cut_abs`, and either `|W_f| < 3` or
`s_f · 1000 ≥ cut_rel · m_f`.
5. **Soft cut** at *f*: `len ≥ target` and `2 · s_f ≥ cut_abs`.
6. **Return clause.** A cut at *f* is rejected when `D(f + k, f − 1) < cut_abs` for
some *k* in `1..=min(L, last − f)`: the content came back within *L* frames, so *f*
starts a flash.
7. **Revisit clause.** A cut at *f* is rejected when `D(f, f − 1 − j) < cut_abs` for
some *j* in `1..=min(L, f − 1)`: *f* returns to content seen within *L* frames, so it
ends a flash. Frames before *p* count.
8. A hard or soft cut that clauses 6 and 7 do not reject starts a piece at *f*.
Otherwise, when `len ≥ max`, a piece starts at *f*.
Frame 0 starts the first piece. *L* is `lookahead_frames`, *last* the index of the
source's last frame, `cut_abs` and `cut_rel` the policy's permille values, and `min`,
`target` and `max` the policy's milliseconds on the component's timebase (§4.4). A
decision about frame *f* reads no frame after `f + L`, so a live segmenter decides it
once frame `f + L` has arrived, and a live and an on-demand segmenter with equal *L*
produce equal boundaries. A piece that follows a cut is flagged a scene cut; one
started at `max`, forced.
**4.4** A piece's duration is an integer number of timebase units, and lies between
`min` and `max` except the last piece of a source, which may be shorter than `min`.
A duration in milliseconds converts to timebase units as
`floor(ms · num / (1000 · den))`; a result of 0 becomes 1, and one above `2^64 − 1`
becomes `2^64 − 1`.
**4.5** *Components.* Every component of a package shares one presentation timeline
that starts at 0. The policy names one **lead** component; every other component cuts
its pieces at the lead's boundary instants, each converted to its own timebase as
`floor(start_lead · num_c · den_lead / (den_c · num_lead))` (above `2^64 − 1`,
`2^64 − 1`). A sink presents piece *p* of every component from that start.
**4.6** *Classification.* Each video piece has a kind, which rules match
(`rule.when.piece_kind`). Over the piece's consecutive frame pairs, with `Y` the luma
of §4.3:
- `mad_milli = Σ |Y_f − Y_{f−1}| · 1000 / (wh · (n − 1))`, and 0 when the piece has
one frame (*n* = 1);
- `changed_permille` = the pixels whose luma differs, `· 1000 / (wh · (n − 1))`, and 0
when *n* = 1;
- `occupancy` = the non-empty histogram bins of the piece's first frame.
The kind is the first that holds of: **static** (`mad_milli ≤ static_mad_milli` and
`changed_permille ≤ static_permille`); **synthetic** (`occupancy ≤ synthetic_bins`);
**motion_high** (`mad_milli ≥ motion_high_milli`); **natural**.
**4.7** *Sources.*
- **Y4M**: 8-bit 4:2:0 (`C420jpeg`, `C420paldv`, `C420mpeg2`, `C420`, or no `C`
parameter) or 4:4:4 (`C444`), progressive. A header that is not `YUV4MPEG2` with a
width and a height is refused `y4m-header`; an interlaced stream or frame,
`y4m-interlaced`; another colour space, `y4m-colorspace`; a `FRAME` line that sets
the width, height or colour space, `y4m-size-change`; a frame that ends early or a
line that is not `FRAME`, `y4m-truncated`. Each pixel converts to RGB8 by BT.709
limited range, with 4:2:0 chroma taken at `(y >> 1, x >> 1)`:
- `R = clip((298(Y−16) + 459(Cr−128) + 128) >> 8)`
- `G = clip((298(Y−16) − 55(Cb−128) − 136(Cr−128) + 128) >> 8)`
- `B = clip((298(Y−16) + 541(Cb−128) + 128) >> 8)`
where `>>` is the arithmetic shift and `clip` clamps to 0..=255.
- **WAV**: RIFF/WAVE with a PCM `fmt ` chunk (format tag 1) of 16-bit samples, block
alignment `2 · channels`. Anything else is refused `wav-format`; a chunk that ends
early, `wav-truncated`.
## 5. Objects
**5.1** *One envelope.* Each object is exactly one `WAI1` or `WAI2` envelope and
nothing after it ([http-delivery](http-delivery.md) §5.1).
**5.2** *Position independence.* An object's bytes **MUST NOT** depend on its position
in a package or on the package it belongs to: no member of its manifest names a piece,
a time, an asset, a class or a component, so two pieces with identical content are one
object. The reference packager writes a base's manifest as SPEC §3 orders it, with
`media` the component's media, `intent` `replicate`, `model_requirement` the rung's
capability with no `fallback` member, `conditioning.kind` the capability's last
dot-separated segment (`int_motion`, `av1`), and `target` `{"frames": n, "h": h, "w": w}`
(the piece's frame count and dimensions; for a floor, its sequence header's maximum
frame height and width); a floor's `derived_from` (§7.2) comes after `conditioning`.
**5.3** *Name.* An object is named by its content hash ([jwp-receipts](jwp-receipts.md)
§2.1), `BLAKE3("moq-jwp:object\x01" ‖ envelope)`, as `objects/<64 lowercase hex>.wai`:
the same value is its stage-index entry (§6), its receipt leaf, its MoQ content hash and
its HTTP strong `ETag` ([http-delivery](http-delivery.md) §5.2).
**5.4** *Refinements.* A refinement object carries `refines`
([staged-delivery](staged-delivery.md) §3): `base` the content hash of its piece's base,
`layer` its layer, and as producers in `prev` only objects of the layers below it, the
object its entry refines (§6.2) among them. The reference packager does not write
refinements yet.
**5.5** *Integer video.* An object of `wai.video.int_motion` carries a `WIV1` payload
with keyframe 0, as SPEC §5 pins it ([integer-payloads](integer-payloads.md) §3); this
extension defines no other layout for it, and its decode is that section's. The
reference packager writes these with one encoder (`wai-rs` `int_video_enc`) at the step
§5.6 chooses. The encoder's choices
make a payload a function of the source frames, the rung's `block`, `search` and
`mv_precision` (§3.3), and the step `q`:
- **Motion search** (frames 1 on): blocks in raster order; for each, every vector
`(dy, dx)` with both components in `−R..=R`, `R = search · mv_precision` (at most
127), `dy` outer and `dx` inner, each ascending. Its cost is the sum, over the
block's pixels inside the frame and the three channels, of `|source − prediction|`,
where the prediction is the decode's (integer-payloads §3) from the previous frame
as decoded. The least cost wins; a tie goes to the smaller `|dy| + |dx|`, then to the
earlier vector.
- **Residual**: each frame, each pixel in raster order, each of R, G, B: the
prediction (128 for frame 0) is subtracted from the source and divided by `q`,
rounding halves away from zero; the frame as decoded is `clamp(prediction + r·q, 0,
255)`, and the next frame predicts from it.
- **Table**: one residual table for the clip, with `offset` the least residual, `top`
the greatest, and `h_i` the count of residual `offset + i`, for
`n = min(top − offset + 1, 4095)` symbols. With `R = 2^16 − n − 1` and `H = Σ h_i`, symbol *i* gets frequency
`1 + ⌊R·h_i / H⌋`; the `R − Σ⌊R·h_i / H⌋` left over go one each to the symbols with
the largest `R·h_i mod H`, the lower symbol first on a tie; the escape gets 1. A
residual past the table's span is coded through the bypass escape
(integer-payloads §2).
- A clip of one frame is written with `block` 1 and `mv_precision` 1, as the format
requires.
The encoder also states the SSE of its reconstruction against the source: the sum of
squared differences over every sample. It codes in two passes (the first finds the
vectors, the reconstruction and the residuals' counts; the second derives each
residual again and codes it last first), so beyond its input it holds the
reconstruction, the vectors, the stream and the residual table: at most
`n·h·w·3 + 24n + 12·v + 3·(2·n·h·w·3 + 16) + T` bytes for `n` frames and `v` motion
vectors (`int_video_enc::working_bytes`). `T` is the widest table's, of `S = 2·255 + 1`
symbols: its fit, `16·(S + 1) + 2·8·S + 4·(S + 2)` bytes (a frequency for each symbol
and the escape, an index for each symbol and the sort's scratch, the table), and the
table's three copies and the payload's header, `3·4·(S + 2) + 64`. For a clip past the
format's caps, which the encoder refuses before it allocates, the figure is `2^64 − 1`.
**5.6** *Choosing the step.* For each piece, the base encode of a `wai.video.int_motion`
rung chooses its step from `q_candidates`, read as a set (a value listed twice is one
candidate). It encodes the piece at each candidate, from the largest down, and chooses
the first whose `floor(SSE · 1000 / (3·w·h·n)) ≤ max_sse_px_milli`, where `SSE` is the
encoder's (§5.5) and *n* the piece's frames: the largest step that meets the bound, so
candidates that meet it alike are decided by their step. Where no candidate meets it,
it chooses the smallest. The function the reference packager's base encode calls is
`wai_pack::encode::choose_q`.
## 6. Stage index
**6.1** A package carries, per component, one stage index, the file `index-<id>.wsix`
(`WSIX`), per window: one window for on-demand content, a chain of windows for live
(§9.2). A window holds whole pieces. Integers are little-endian.
**6.2** *Layout.* A 160-byte header, then `n_pieces` piece records of 32 bytes,
`n_entries` entry records of 128 bytes, `n_classes` class records of 16 bytes, the class
plan, and the string table. Nothing follows the string table. Reserved and padding
bytes are zero.
| Offset | Size | Header field |
|---|---|---|
| 0 | 4 | `"WSIX"` |
| 4 | 2 | version, 1 |
| 6 | 2 | `media_class`: 0 video, 1 image, 2 audio, 3 world, 4 feed, 5 avatar, 6 film |
| 8 | 4 | `tb_num` |
| 12 | 4 | `tb_den` |
| 16 | 4 | `n_pieces` |
| 20 | 4 | `n_entries` |
| 24 | 4 | `n_strings` |
| 28 | 4 | `n_classes` |
| 32 | 32 | `policy_digest` (§3.2) |
| 64 | 32 | `prev_index_digest`: the previous window's index digest; zero for on-demand content and a first window |
| 96 | 8 | `first_piece`: the absolute number of the window's first piece |
| 104 | 8 | `first_entry`: the absolute ordinal of the window's first entry across the chain |
| 112 | 32 | `plan_digest` (§4.2) of the pieces' starts |
| 144 | 2 | `component`: the component's id, as a string index |
| 146 | 14 | reserved |
| Offset | Size | Piece record field |
|---|---|---|
| 0 | 8 | `start`, in timebase units |
| 8 | 4 | `duration`, in timebase units |
| 12 | 4 | `first_entry`, window-relative |
| 16 | 2 | `n_entries` |
| 18 | 1 | `kind`: 0 natural, 1 static, 2 synthetic, 3 motion_high (§4.6), 4 world segment, 5 audio |
| 19 | 1 | flags: bit 0 scene cut, bit 1 forced at `max` (§4.3), bit 2 started by a cue, bit 3 aligned to the lead (§4.5) |
| 20 | 4 | `rules`: bit *i* set when rule *i* matched the piece (§8.1) |
| 24 | 4 | `moments`: bit *i* set when the piece lies in moment *i* (§8.1) |
| 28 | 4 | reserved |
| Offset | Size | Entry record field |
|---|---|---|
| 0 | 32 | `content_hash` (§5.3) |
| 32 | 8 | `byte_len`: the whole envelope's length |
| 40 | 8 | `piece`: the absolute piece number |
| 48 | 4 | `refines`: the window-relative index of the entry this one refines; `0xFFFFFFFF` for none |
| 52 | 2 | `capability`, as a string index |
| 54 | 1 | `role`: 0 base, 1 refinement, 2 floor, 3 snapshot, 4 ops, 5 receipt, 6 asset |
| 55 | 1 | `layer` |
| 56 | 1 | `prefix_tier`: 0 none, 1 entropy-consistency, 2 decode-equivalence (§6.4) |
| 57 | 1 | flags: bit 0 deferrable, bit 1 duplicate (an earlier entry of the index has the content hash), bit 2 the manifest carries `derived_from` |
| 58 | 2 | reserved |
| 60 | 4 | `deadline_ms`: 0 for none (§8.6) |
| 64 | 32 | `prefix_digest` (§6.5) |
| 96 | 8 | `sse` (§6.6); `2^64 − 1` when not computed |
| 104 | 4 | `manifest_len`: the envelope's `man_len` |
| 108 | 4 | reserved |
| 112 | 8 | `payload_len`: the envelope's payload bytes (for a `WAI2`, every component's) |
| 120 | 8 | reserved |
`byte_len`, `manifest_len` and `payload_len` are the envelope's own lengths; for an
object whose payload format WAI checks they are its counted bytes
([staged-measurement](staged-measurement.md) §3), so a verifier also requires each such
payload to parse as its format, after the other object rules of §6.3
(`object-lengths`).
A class record is `u16` name (a string index), `u8 max_layer`, `u8` flags (bit 0
floor only), `u32 max_kbps` (0 for none) and 8 reserved bytes, in the policy's class
order. The class plan is `n_classes` rows of `n_pieces` bytes, each the highest layer
the class lists for the piece or `0xFF` for the floor only (§8.1), zero-padded to a
multiple of 8. The string table is `n_strings` strings, each a `u16` length and that
many bytes of UTF-8, at least one, zero-padded to a multiple of 8. It is the component's
id, then the class ids in policy order, then each capability at its first use, and a
package's verifier holds an index to that (§11.3).
index_digest = BLAKE3("wai:pack-stage-index\x01" ‖ file)
leaf_i = BLAKE3("wai:pack-stage\x01" ‖ entry record i)
stage_root = the jwp-receipts §2.2 Merkle root over the leaves
A reader **MUST** refuse a file that is not this layout, the first that applies, in
this order:
1. its first four bytes are not `WSIX`, a file of fewer included (`index-magic`);
2. a file of fewer than six bytes (`index-truncated`), or a version other than 1
(`index-version`);
3. a file shorter than the header, or than the tables its counts give: the piece, entry
and class records, the class plan with its padding, each string's length and bytes,
and the string table's padding, every size taken from its count before anything is
allocated (`index-truncated`);
4. a reserved or padding byte that is not zero (`index-reserved`);
5. a media class with no meaning (`index-media-class`); then, entry by entry, a role,
then a tier, with none (`index-role`, `index-tier`);
6. a string that is empty or not UTF-8, then a string index past the table
(`index-string`);
7. bytes after the string table's padding (`index-trailing`).
**6.3** *Rules.* An index lists every WAI object of its component, in piece order.
Files that are not WAI envelopes (CMAF initialization segments and fragments, FLAC
files) **MUST NOT** appear in it. A verifier **MUST** refuse an index that breaks one
of these, checked in this order:
1. A timebase of zero (`index-timebase`). An index with no previous window
(`prev_index_digest` zero: on-demand content, or a chain's first window) whose
`first_piece` or `first_entry` is not 0, or whose first piece does not start at 0, the
start of the presentation timeline (§4.5) (`window-start`). Then piece by piece: a
piece that lasts no time or does not start where the one before it ended
(`piece-order`); whose
`first_entry` is not the number of entries before it, or that has none
(`piece-entries`); of a kind or with a flag that has no meaning (`piece-kind`). Then
pieces whose entries are not the index's entries (`piece-entries`).
2. Entry by entry, in order: an entry that does not name its piece's absolute number
(`piece-entries`); a flag with no meaning (`entry-flags`); within its piece, a role
before one that comes earlier in the order base, refinement, floor, any other, or a
refinement at a lower layer than the refinement before it (`entry-order`); an entry
that is not a refinement above layer 0, a refinement at layer 0, or `refines` on an
entry that is not a refinement or missing from one (`layer-role`); a
refinement whose `refines` is outside the window (`refines-outside-window`), is not
an earlier entry of its own piece (`refines-mismatch`) or is not one layer below it
(`layer-gap`); its tier (§6.4: `tier-entropy-passthrough`, `tier-weakest`, then
`tier-mismatch`); a non-zero `prefix_digest` with a tier other than
decode-equivalence (`digest-without-tier`); a deadline on an entry that is not a
refinement (`deadline-role`); a floor without flag bit 2 (`floor-derived-from`); a
content hash an earlier entry has at another length (`dup-length`); the duplicate
flag other than set exactly when an earlier entry has the content hash (`dup-flag`).
3. Class by class: a flag with no meaning (`class-flags`); a plan value other than
`0xFF` for a floor-only class, or other than `0xFF` or at most both the class's
`max_layer` and the piece's highest layer otherwise (`class-plan`).
With the objects in hand, a verifier **MUST** also refuse, entry by entry: bytes of
another length (`object-length`) or that do not hash to the entry's content hash
(`object-hash`); bytes that are not one `WAI1` or `WAI2` envelope and nothing else
(`non-envelope-entry`); a `man_len` or a payload length other than the entry's
(`object-lengths`); a capability other than the entry's (`object-capability`; for a
`WAI2`, its first entry's); a refinement whose object is not a `WAI1` whose `refines`
names its piece's base, its layer and producers of the layers below among which the
entry it refines (§5.4, `object-refines`); and `derived_from` (SPEC §7.1) present
otherwise than bit 2 says, or not well formed (`object-derived-from`). A member whose
value is `null` is absent; for a `WAI2`, `derived_from` is present when any entry
carries it, and well formed when every entry that carries it is. A manifest that does
not parse, or names a member twice, is not an envelope (`non-envelope-entry`).
**6.4** *Tier.* The tier a capability's own decode holds is decode-equivalence for a
capability whose registry class reconstructs exactly (`StandardDefined`, and
`NeuralIntegerExact`, whose pin a sink verifies before it decodes),
entropy-consistency for `NeuralEntropyExact`, and none otherwise (SPEC §5, §7).
- An entry at layer 0 states its capability's tier, except a floor, which states none
(§7.4).
- A prefix with one or more refinements holds decode-equivalence only if every object
along it does, and none otherwise ([derived-tracks](derived-tracks.md) §3, rules 1
and 2): a refinement's tier is decode-equivalence exactly when the entry it refines
states decode-equivalence and its own capability's tier is decode-equivalence.
- An entry with `layer` > 0 **MUST NOT** state entropy-consistency
(`tier-entropy-passthrough`); one with `layer` > 0 that states decode-equivalence over
a prefix that does not hold it is refused `tier-weakest`; any entry stating another
tier than the one this section gives, `tier-mismatch`.
**6.5** `prefix_digest` is the SHA-256 decoded-output digest (SPEC §7, *Digests*) of the
prefix the entry completes when `prefix_tier` is decode-equivalence, and all zero
otherwise: for video, the clip digest over RGB8 frames; for an image, the RGB8 frame
digest; for audio, the digest of the s16le interleaved samples; for world, feed, avatar
and film entries, zero (their state hashes live in their receipts). A verifier decodes
the prefix and compares.
**6.6** `sse` is the integer sum, over every output sample of the prefix, of the
squared difference from the source sample the packager coded (RGB8 channels, or s16
samples). It is the producer's statement: a verifier holding the source recomputes it,
and one without the source cannot. A floor's `sse` is `2^64 − 1`.
## 7. Floor and `derived_from`
**7.1** Every piece of a video, image or audio component **MUST** have a floor rendition
in a capability of SPEC §5's mandatory floor or recommended set; a packager refuses a
video component with no floor rung (`floor-missing`). A film or world package SHOULD
have one. This revision packs a supplied AV1 floor (`wai.video.av1` or
`wai.video.av1.lossless`, `source = "supplied"`).
**7.2** *Floor objects.* Each piece's floor is one object of the floor rung's capability
(§5.2), whose manifest carries `derived_from` (SPEC §7.1, *Derived renditions*) naming
the piece's base: `motif` the base's content hash, `motif_capability` its capability,
`renderer` the floor's maker, and `settings` `BLAKE3("wai:render-settings\x01" ‖
JCS(settings))` (§3.6). For a supplied floor, `renderer` is `supplied@<the supplied
listing's SHA-256>` and the settings are `{"source": "supplied", "supplied_listing":
"<that SHA-256>"}`, so neither names a position. This profile fixes these members: they
are not the producer's statement here (SPEC §7.1), and a verifier holds every floor
object to them, to its framing header and to its target (§11.3). An AV1 floor object's payload is the
length-prefixed framing the reference sink decodes (`wai-rs` `codecs::video`): `n`,
`h`, `w`, `fps_num`, `fps_den` as `u32` little-endian (the piece's temporal units, the
sequence header's maximum frame height and width, the component's timebase `num` and
`den`), then each temporal unit as a `u32` little-endian length and its bytes as the
stream holds them.
**7.3** *A supplied floor* is a directory holding, per video component, `<id>.obu`: an
AV1 stream in the low-overhead OBU format (every OBU with its size field), one temporal
unit per source frame. Its listing (SPEC §3.1) of every file is pinned by the policy's
`floor.supplied_listing`. A packager **MUST** refuse, before it writes anything:
- a directory whose listing does not hash to `floor.supplied_listing`
(`floor-listing-mismatch`), and a component with no stream in it
(`floor-stream-missing`);
- a stream that does not split into temporal units each holding a frame, whose first
unit holds no sequence header, with a sequence header whose bytes differ from the
first's, or with a unit that starts at a shown key frame (below) without a sequence
header (`floor-av1-malformed`);
- another number of temporal units than source frames (`floor-frame-count`);
- a unit that does not start at a shown key frame at a piece's first frame, or one that
does elsewhere unless `floor.extra_keyframes` is true (`floor-keyframe-mismatch`). A
unit starts at a shown key frame when its first frame header has `frame_type`
`KEY_FRAME` and `show_frame` 1, or the sequence uses the reduced still-picture header;
a unit that shows an existing frame does not.
The plan a supplied floor is made against is the **keyframe plan**, the file
`plan-<id>.txt`: each piece's first frame in decimal, one per line, each line ended by a
line feed. Its pieces' starts are the plan of §4.2, and `wai-pack plan` writes it from the
source and the policy before any floor is encoded.
**7.4** The floor is presentation. Its output is never compared with the primary's, and
no tier or `replicate` claim covers it.
**7.5** A floor produced by an encoder that is not a pure function of its input (§11)
**MUST** name that encoder and its version in `derived_from.renderer`.
**7.6** *CMAF.* The floor of a video component is also written as CMAF files, which no
stage index lists: `floor/<id>/init.mp4`, and one fragment per piece,
`floor/<id>/<SHA-256 of the fragment>.m4s`. Box layouts are ISO/IEC 14496-12's, with the
AV1 binding's sample entry; every integer is big-endian and every time 0:
- `init.mp4`: `ftyp` (major brand `cmfc`, minor version 0, compatible `cmfc`, `iso6`,
`av01`), then `moov` holding `mvhd` (version 0, timescale `tb_num`, rate 1, volume 1,
the unity matrix, next track 2), `trak` (`tkhd` version 0 with flags 3, track 1,
layer, alternate group and volume 0, the unity matrix, the width and height in 16.16; `mdia`: `mdhd` version 0, timescale
`tb_num`, language `und`; `hdlr` `vide` named `video` with its terminating zero byte;
`minf`: `vmhd` with flags 1,
`dinf` with one self-contained `url `, and `stbl`: `stsd` with one `av01` sample entry
of the width and height, data reference 1, resolution 72 dpi, frame count 1, an empty
compressor name, depth 24 and pre-defined −1, holding `av1C`, then empty `stts`,
`stsc`, `stsz` and `stco`), and `mvex` with `trex` (track 1, sample description 1).
`av1C` is marker and version 1, then the sequence header's profile, level, tier,
bit depth, monochrome and chroma fields, no initial presentation delay, then the
sequence header OBU as the stream holds it. The width and height are the sequence
header's maximum frame width and height.
- A fragment: `moof` holding `mfhd` (sequence number piece + 1) and `traf` (`tfhd` with
default-base-is-moof, track 1; `tfdt` version 1, the piece's start × `tb_den`; `trun`
version 0 with data offset, sample duration, size and flags, the offset pointing at
the `mdat` payload), then `mdat`. Each sample is a temporal unit without its temporal
delimiter, lasting `tb_den`; its flags are `0x02000000` at a shown key frame and
`0x01010000` otherwise.
The codecs string of the floor is the AV1 binding's short form,
`av01.<profile>.<level, two digits><M or H>.<bit depth, two digits>`.
## 8. Classes, catalogs and delivery
**8.1** *What a class lists.* The stages listed for a class are a pure function of the
class, the piece and the effective policy, so every viewer of a class receives the same
catalog bytes.
- A piece that starts at unit `start` and lasts `duration` units spans the
milliseconds from `s_ms` up to `e_ms`, the floors of `start · den · 1000 / num` and of
`(start + duration) · den · 1000 / num`; it lies in moment *i* when `s_ms < to_ms` and
`e_ms > from_ms`. Rule *i* matches it when each condition the rule states holds: its
kind is in `when.piece_kind`, and a tag of a moment it lies in is in
`when.moment_tag`. Matching rules fold in document order (§3.4).
- A floor-only class lists the floor (`0xFF`). Otherwise the class lists
`min(max_layer, the rules' then.max_layer, the piece's highest layer)`; under
`max_kbps`, while the listed prefix needs more than `max_kbps`, that is
`ceil(8 · bytes · num / (1000 · duration · den))` kilobits per second over the bytes of
the listed bases and refinements, the top layer is dropped, and a base that alone
needs more makes the piece floor-only for the class.
**8.2** A catalog **MUST NOT** list a refinement without the stages below it.
**8.3** *HTTP.* A package is offered in [http-delivery](http-delivery.md)'s Mode N
(§11.1 there): the classical presentation is the floor alone, and WAI objects are
offered in playlists of their own.
- A WAI object is none of the media segment formats of RFC 8216 §3. A WAI playlist is
offered only to a sink that recognizes its codecs value from its own capability list,
and a packager **MUST** write a classical master that names nothing of WAI.
- A browser's media-capabilities query answers only for the browser's own decoders,
and **MUST NOT** be used to decide whether a `wai1.*` value can be decoded
([http-delivery](http-delivery.md) §4.3).
- Each class's WAI playlist **MUST** name its class and its stage view.
Durations: a piece of *d* units lasts `floor(d · den · 1000 / num)` milliseconds,
written `S.mmm` (whole seconds, a full stop, three digits). A target duration is the
largest `ceil(d · den / num)`. A rate is `ceil(8 · bytes · num / (d · den))` bits per
second; a variant's `BANDWIDTH` is the largest over the pieces it lists and
`AVERAGE-BANDWIDTH` the rate of all their bytes over all their durations. A floor
variant's bytes per piece are its fragment's; a WAI variant's are the listed bases' and
refinements', over the pieces the class does not take as floor only. Every playlist
line ends with a line feed. For component *c*, of frame size `W×H`, whose floor has
codecs string *F* and maximum frame size `FW×FH`:
- `hls/classic/master.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`,
`#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-STREAM-INF:BANDWIDTH=<b>,AVERAGE-BANDWIDTH=<a>,CODECS="F",RESOLUTION=FWxFH`,
`../classic/floor-<c>.m3u8`: a URI that names the same playlist from every class's
directory, so each class's master differs from this one only by the session-data
line, as [http-delivery](http-delivery.md) §11.1 and §11.8 require.
- `hls/classic/floor-<c>.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`,
`#EXT-X-TARGETDURATION:<t>`, `#EXT-X-MEDIA-SEQUENCE:0`, `#EXT-X-PLAYLIST-TYPE:VOD`,
`#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-MAP:URI="../../floor/<c>/init.mp4"`, then per
piece `#EXTINF:<S.mmm>,` and `../../floor/<c>/<sha256>.m4s`, then `#EXT-X-ENDLIST`.
- `hls/<class>/master.m3u8`: the classical master, byte for byte, with, when the class
lists a WAI object, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8"`
after `#EXT-X-INDEPENDENT-SEGMENTS`, and no other change.
- `hls/<class>/wai.m3u8`, when the class lists a WAI object: `#EXTM3U`,
`#EXT-X-VERSION:7`, `#EXT-X-INDEPENDENT-SEGMENTS`,
`#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="<class>"`,
`#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/<class>.json"`,
`#EXT-X-STREAM-INF:BANDWIDTH=<b>,AVERAGE-BANDWIDTH=<a>,CODECS="<union>",X-WAI-FLOOR="<floor>",RESOLUTION=WxH`,
`wai-<c>.m3u8`. The union and the floor are [http-delivery](http-delivery.md) §4.2's
over the objects the class lists.
- `hls/<class>/wai-<c>.m3u8`: the floor playlist's lines without `#EXT-X-MAP`, adjacent
to it ([http-delivery](http-delivery.md) §11.2); per piece, `#EXT-X-GAP` when the
class takes it as floor only, `#EXTINF:<S.mmm>,`, each refinement the class lists,
in entry order, as `#WAI-LAYER:N=<layer>,URI="../../objects/<hash>.wai#n=<byte_len>"`,
then its base as `../../objects/<hash>.wai#n=<byte_len>`: a layer line follows its
segment's `EXTINF` ([http-delivery](http-delivery.md) §10.4).
Every HLS file above and every MPD below is a rendering of
[http-delivery](http-delivery.md)'s (Appendix G for a media playlist): the reference
packager writes them with the binding's one HLS and one DASH renderer.
DASH, the same as two MPDs ([http-delivery](http-delivery.md) §11.1), each
`<?xml version="1.0" encoding="UTF-8"?>`, then
`<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:full:2011" type="static" mediaPresentationDuration="PT<S.mmm>S" minBufferTime="PT<t>S">`
over the component's whole duration and its target duration, `<Period id="p0" start="PT0S">`,
one `AdaptationSet`, `</Period>` and `</MPD>`, indented by two spaces a level:
- `dash/classic.mpd`: `<AdaptationSet id="1" contentType="video" mimeType="video/mp4" codecs="F" width="FW" height="FH" segmentAlignment="true" startWithSAP="1">`
holding `<Representation id="<c>-floor" bandwidth="<b>">` with
`<SegmentList timescale="<num>">`: `<Initialization sourceURL="../floor/<c>/init.mp4"/>`,
a `SegmentTimeline` of one `<S t="<(start − first start)·den>" d="<d·den>"/>` per
piece, and one `<SegmentURL media="../floor/<c>/<sha256>.m4s"/>` per piece.
- `dash/<class>.mpd`, when the class lists a WAI object:
`<AdaptationSet id="1" contentType="video" mimeType="application/wai" codecs="<union>" width="W" height="H">`
holding `<EssentialProperty schemeIdUri="tag:transaction.science,2026:wai/http-delivery"/>`,
`<SupplementalProperty>`s of schemes `tag:transaction.science,2026:wai/floor` (value the
floor), `tag:transaction.science,2026:wai/class` (the class) and
`tag:transaction.science,2026:wai/stages` (`../stages/<class>.json`), then
per layer *k* up to the highest the class lists, a `Representation` with attributes
`id="<c>-l<k>"`, for *k* > 0 `dependencyId="<c>-l<k−1>"`, and `bandwidth` the peak rate of its own
layer's objects over the pieces it lists, whose
`SegmentList` has a timeline entry and a `SegmentURL media="../objects/<hash>.wai"`
for each piece the class lists to layer *k* (a gap where it does not).
The media type `application/wai` is proposed, not registered
([http-delivery](http-delivery.md) §3). This revision writes the catalogs of a package
of one component; how the variants and groups of several components are written is
defined with the packing of several components.
**8.4** *MoQ.* `moq/catalog.json` is RFC 8785 canonical JSON and a line feed:
`{"tracks": [...], "version": 1}`, the tracks by layer, then by deadline ascending,
then the floor. Each (component, layer, deadline) is one track of
`packaging` `"wai"`: name `<c>/l<k>`, or `<c>/l<k>@<deadline>` for a refinement whose
deadline is not `defaults.deadline_ms`; `renderGroup` 1; `priority` §8.5's;
`wai_capabilities` its entries' capabilities by first appearance; `wai_layer` *k*;
`wai_receipts` false (no object of this revision carries a `WAI_RECEIPT` property);
`wai_determinism` the weakest tier of its entries, absent when that is none. The base
track has `altGroup` 1 when the component has a floor. MSF's catalog defines `depends`
as the tracks on which a track is dependent, and this section has a sink receive every
one of them (below): a dependency is all of its tracks, not any one. So a refinement track
`depends` on every track that carries an entry its own entries refine, in track order —
the layer below at the same deadline, where a piece's layers share its deadline as the
reference packager writes them — and never on a track the catalog does not hold. It
carries `wai_delivery_timeout_ms` (§8.6). The floor is a track
`<c>/floor` of `packaging` `"cmaf"` with `altGroup` 1, `renderGroup` 1, `codec`,
`width` and `height`. A sink **MUST NOT** subscribe to a track unless it also receives
every track that track depends on, so a sink that selects the floor subscribes to no
layer track. `wai_layer` and `wai_delivery_timeout_ms` are catalog fields this
extension defines under the `wai_` prefix of
[moq-streaming-format](moq-streaming-format.md) §4; a later revision of that extension
may define them there instead.
**8.5** *Priority.* The base track's publisher priority is 64 and layer *k*'s
`64 + 32·k`: a refinement track's value is strictly greater than that of the track it
depends on, so it is delivered after it (MOQT: a lower value is delivered first).
**8.6** *Deadline.* An entry's `deadline_ms` is the longest a refinement may arrive
after the base of its piece was published and still be presented; the rules' or
`defaults.deadline_ms`. A refinement track carries DELIVERY_TIMEOUT
`deadline_ms − live.layer_lag_ms[k]` (§3.4 has refused a policy where that is below 1).
Entries of one layer with different deadlines travel on different tracks. A relay may
drop a late refinement (MOQT §5.2), so a refinement never delays the base.
**8.7** *Presentation.* A sink presents piece *p* at the highest prefix whose objects
all arrived and verified by *p*'s presentation instant, and **MUST NOT** delay the base
to wait for a refinement. What the receipts of a dropped object say is
[jwp-receipts](jwp-receipts.md)' to define, not this extension's.
**8.8** *Stage views.* `stages/<class>.json` is RFC 8785 canonical JSON and a line feed:
`{"class": "<class>", "components": {"<c>": {"media_playlist":
"../hls/<class>/wai-<c>.m3u8", "pieces": [...]}}, "policy_digest": "<hex>"}`, with
`media_playlist` only when the class lists a WAI object, and each piece
`{"dur", "p", "start", "stages": [...]}` with `"floor_only": true` when the class takes
it as floor only (and `stages` empty), and each listed stage `{"bytes", "h", "layer"}`,
a refinement's with `"refines"` (the content hash of the entry it refines) and
`"deadline_ms"`.
## 9. Package receipt
**9.1** A version-2 package receipt binds the asset id; the policy digest; its parent
kind and parent receipt hash; per component its id, media class, index digest and stage
root; the entry, distinct-object, referenced-byte and stored-byte counts, which a
verifier recomputes from the indexes; the packager `builtin:<crate>@<version>`; the
energy figure with its class, or the unmetered form; the grant; and the signer. Its
signing payload, big-endian:
labelled_domain("wai:pack-receipt\x01", label)
u32 len ‖ asset_id ‖ policy_digest[32]
u8 parent_kind (0 none, 1 live-window, 2 re-pack) ‖ [parent_receipt_hash[32] if not 0]
u32 n ‖ n × ( u32 len ‖ id ‖ u8 media_class ‖ index_digest[32] ‖ stage_root[32] )
u64 total_entries ‖ u64 unique_objects ‖ u64 total_bytes ‖ u64 stored_bytes ‖ u64 work_units
u32 len ‖ packager
u64 joules_micro ‖ write_label(label)
grant_hash[32] ‖ u32 len ‖ capability ‖ u64 joule_ceiling_micro ‖ u8 0 | u8 1 ‖ u64 funds_ceiling
signer_pubkey[32] ‖ u32 len ‖ signer_id
receipt_hash = BLAKE3("wai:video-package-receipt-id\x01" ‖ payload ‖ sig)
`labelled_domain` and `write_label` are [energy-measurement](energy-measurement.md)
§2.8's: unmetered, the domain as written and no label; metered, its final byte `\x02`
and the class's signed encoding. `total_entries` counts every entry; `unique_objects`
the distinct content hashes over every component; `total_bytes` the entries'
`byte_len`; `stored_bytes` the `byte_len` of each content hash's first entry;
`work_units` equals `total_entries`. A receipt whose content hash comes at two lengths
has no counts. The signature is Ed25519 by `signer_pubkey`, verified as SPEC §7
*Signatures* requires; a signature or a key that rule refuses is `receipt-signature`.
Its JSON form, `package.receipt.json`, is RFC 8785 canonical, hexadecimal lowercase,
integers as numbers no greater than `2^53 − 1` (a packager refuses to seal anything
else, `receipt-json`): `asset_id`; `components`, each `{"id", "index":
"index-<id>.wsix", "index_digest", "media_class", "stage_root"}`; `grant`
`{"capability", "funds_ceiling", "grant_hash", "joule_ceiling_micro"}` with `null` for
no ceiling (a joule ceiling of `2^64 − 1`); `joules_micro`; `kind` `"package"`;
`packager`; `parent_kind` (`none`, `live-window` or `re-pack`); `parent_receipt_hash`
(`null` for none); `policy_digest`; `receipt_hash`; `sig`; `signer_id`;
`signer_pubkey`; `stored_bytes`; `total_bytes`; `total_entries`; `unique_objects`;
`version` 2; `work_units`; `energy_provenance`, the class's label (energy-measurement
§2.7) or `null` when unmetered; and the class's evidence members exactly as its class
carries them. Every member is present, with `null` where it has no value
(`parent_receipt_hash`, `energy_provenance`, a grant's ceilings) and nowhere else.
A reader refuses, before anything else, JSON that is not RFC 8259 or that names a member
twice in one object, at any depth (`receipt-json`). It then reads `"version"`: absent or
1 is a version-1 receipt (§9.3), 2 a version-2 one, anything else `receipt-version`. It
then refuses (`receipt-json`) a member it does not define or one missing, `null` where
the member has a value, a value of another form (a number that is not an integer of its
range, `-0` among them; a string that is not Unicode scalar values), evidence other than
exactly what the class carries, a class label energy-measurement §2.7 refuses, a parent
kind and hash that disagree, an `index` other than `index-<id>.wsix`, and a
`receipt_hash` that is not the receipt's. A reader does not require the canonical form;
a writer writes it, and a package's `package.receipt.json` must be it (§11.3).
A verifier of a receipt against its indexes, in its components' order, refuses, the
first that applies: no component, or another number of indexes than components, a live
window's receipt included (`receipt-index-count`); an index that breaks §6 (its
reason); one that does not hash to its `index_digest` (`receipt-index-digest`), has
another stage root (`receipt-stage-root`), names another component or media class
(`receipt-component`) or another policy digest (`receipt-policy-digest`); two components
of one id (`receipt-component`); other counts, or a content hash at two lengths in any of the
indexes (`receipt-counts`); a parent kind that does not
match the indexes' `prev_index_digest`, which is zero in every index for none and a
re-pack and non-zero in every one for a live window (`receipt-parent-kind`); an energy
figure and class that do not cohere, or a legacy label (`receipt-energy`); a signature
that does not verify (`receipt-signature`).
**9.2** Live windows chain by the parent link with parent kind `live-window`. A re-pack
of an earlier package **MAY** name it with parent kind `re-pack`.
**9.3** A version-1 receipt (`wai.video.package`'s segment map) keeps its bytes and
still verifies. Its JSON form is RFC 8785 canonical with `version` 1: `asset_id`,
`energy_provenance` (`null` when unmetered) with its evidence, `grant`, `joules_micro`,
`kind` `"package"`, `parent_receipt_hash`, `receipt_hash`, `segment_map_root`,
`segments` (each `{"byte_len", "content_hash", "duration_ms", "index"}`), `sig`,
`signer_id`, `signer_pubkey`, `stored_bytes`, `total_bytes`, `total_segments`,
`unique_segments`, `work_segment_ops`. A reader reads one without `version` as version 1,
and, because version-1 receipts were issued before this form, keeps them verifying:
`energy_provenance` may be absent (read as unmetered), an evidence member written `null`
is absent, and an on-chip counter's filtering, absent, reads as unknown. It refuses a
member named twice as version 2's reader does.
Its signing payload, big-endian, is
"wai:video-package-receipt\x01"
u32 len ‖ asset_id ‖ segment_map_root[32]
u64 total_segments ‖ u64 unique_segments ‖ u64 total_bytes ‖ u64 stored_bytes ‖ u64 work_segment_ops
u64 joules_micro ‖ class (u8 0 when unmetered, else the class's signed encoding)
the grant as §9.1 writes it
u8 0 | u8 1 ‖ parent_receipt_hash[32]
u64 n ‖ n × BLAKE3("wai:video-package-seg\x01" ‖ u64 index ‖ content_hash[32] ‖ u64 duration_ms ‖ u64 byte_len)
signer_pubkey[32] ‖ u32 len ‖ signer_id
`segment_map_root` is the jwp-receipts §2.2 Merkle root over those segment leaves, the
counts are the segments' (the bytes of one copy per content hash for `stored_bytes`), and
`receipt_hash` is version 2's.
Both versions share the identity domain, so the figure profile
`wai:video-package-receipt-id` ([energy-measurement](energy-measurement.md) §6.1)
covers both; a version-2 receipt's figure is read only with the indexes it verifies
against.
**9.4** Where the energy figure is not the unmetered form, the packager **MUST** publish
a `measurement` claim about it ([jwp-receipts](jwp-receipts.md) §2.4.8,
[energy-measurement](energy-measurement.md) §6) as
`claims/<receipt_hash>.measurement.json`: subject the receipt, figure profile
`wai:video-package-receipt-id`, signed with the package key and checked against the
receipt before it is written. A verifier refuses a metered package without it
(`measurement-claim-missing`) or with one that does not verify and check against the
receipt (`measurement-claim-invalid`).
## 10. Royalty-free packaging
A packager run with `royalty_free_only = true` **MUST** refuse to emit any object whose
capability the registry does not classify as royalty-free, including one it does not
classify at all (§3.6, rule 5). A package receipt says nothing about patents.
`royalty_free_only` records the registry class the packager read, "licensed
royalty-free by its standards body"; it is not a freedom-to-operate opinion.
## 11. Package, re-pack equivalence and verification
**11.1** *The package* is a directory of these files and no other file or directory
(every directory below it holds one of them):
policy.toml policy.json plan-<c>.txt index-<c>.wsix package.receipt.json
objects/<content hash>.wai …
floor/<c>/init.mp4 floor/<c>/<sha256>.m4s …
hls/classic/… hls/<class>/… dash/… moq/catalog.json stages/<class>.json
claims/<receipt_hash>.measurement.json (a metered package)
LISTING
`policy.toml` is the policy as given; `policy.json` its effective canonical JSON
(§3.2). `LISTING` is the listing (SPEC §3.1) of every other file, and its SHA-256 is the
**pack digest**. A component or class id that cannot be one path segment of a listing,
two class ids equal once `A`–`Z` are mapped to `a`–`z`, and a class named `classic` in
any case (ids name paths, and paths fold case) are refused (`pack-id`).
**11.2** *Re-pack equivalence.* Given the same source bytes, effective policy, supplied
floor, signing key and packager identity, a conforming reference packager **MUST**
produce byte-identical package files on every machine, except the energy figure and
measurement claim of a metered package and floor objects §7.5 covers. A verifier
**MAY** pack again and compare listings. A packager whose identity differs is not held
to this.
**11.3** *Verification.* A verifier of a package, without its sources, applies these
checks in this order and refuses at the first that fails; a check that reads files a
failed check names is not applied:
1. Every file below the package's directory, read strictly — a name beginning with `.`
and a `*.sha256` file included, as a listing (SPEC §3.1) would not skip them — is a
regular file whose path a listing can carry; a symbolic link, a directory that
cannot be read or holds nothing, or anything else is refused (`unexpected-file`).
2. `LISTING` is present (`listing-missing`) and is the listing of every other file
(`listing-mismatch`).
3. `package.receipt.json` reads as a version-2 receipt (§9.1: `receipt-json`,
`receipt-version`) and is that receipt's canonical form, byte for byte
(`receipt-json`).
4. `policy.json` hashes to the receipt's policy digest (`policy-digest-mismatch`);
`policy.toml` is a policy (its own reason, §3.2–§3.4); `policy.json` is its
effective canonical JSON, `royalty_free_only` forced on where `policy.json` says
`true` (`policy-json-mismatch`; an override never turns it off, §3.2, so a
`policy.json` that says `false` over a `policy.toml` that says `true` is refused
here); its ids can name files (`pack-id`, §11.1); and under that effective policy it
asks for nothing a packager may not emit (§3.6's reason; under `royalty_free_only`, a
patent-pool floor is `license-refused`).
5. The receipt's components are the policy's, by id, in order, each of the policy's
media (`policy-component-mismatch`), so a package is never a receipt over fewer
components than its policy has; each index is present (`index-missing`); the receipt
verifies against them (§9.1, the index rules of §6 among its reasons).
6. Per component, in order:
- the index against the policy (`index-policy-mismatch`): its timebase and media
class are the component's; its string table is the component's id, the class ids
in policy order, then each capability at its first use; its class records are the
policy's classes, in order, with their `max_layer`, floor-only flag and `max_kbps`;
every entry's role, capability and layer are a rung's of its component (a base, a
refinement at the rung's layer, or a floor); every piece's kind is one a video piece
has, and its rules, moments and class plan are §8.1's from its kind, span and
entries;
- the plan file holds the index's starts and its digest is the header's
(`plan-mismatch`);
- every entry's object is present (`object-missing`) and keeps the object rules of
§6.3, its counted bytes included;
- every decode-equivalent prefix decodes to its `prefix_digest` (`prefix-digest`);
- when all of that holds, the floor: per piece, one floor object of the floor rung's
capability whose renderer, settings, framing header, unit count and target are
§7.2's (`floor-object-mismatch`) and which presents the piece's base
(`floor-motif-mismatch`); its units each one temporal unit holding a frame, the
first of the first piece carrying a sequence header, every sequence header the
first's bytes, every shown key frame carrying one (`floor-av1-malformed`); each
piece opening on a shown key frame, and no other unit being one unless the policy
allows extra key frames (`floor-keyframe-mismatch`); and the CMAF files are the
ones its temporal units give (§7.6, `floor-cmaf-mismatch`);
- then the catalogs are the ones the index, the objects and the floor give, and an
index no catalog can be written for (a start past `2^53 − 1`) is refused
(`catalog-mismatch`).
7. A metered package's measurement claim (§9.4: `measurement-claim-missing`,
`measurement-claim-invalid`); an object file no entry names (`object-unlisted`); a
catalog file past the ones given (`catalog-unexpected`); any other file
(`unexpected-file`).
**11.4** *What this revision packs.* The reference packager packs one video component
with a `wai.video.int_motion` base (§5.5, §5.6) and a supplied AV1 floor (§7.3). It
refuses by name, before writing anything, what it does not pack yet: another medium
(`pack-media-unsupported`), a refinement rung (`pack-refinement-unsupported`), a rule's
ladder subset (`pack-rule-ladder-unsupported`), a floor to encode
(`pack-floor-encode-unsupported`), and a rung of a capability it does not encode
(`pack-capability-unsupported:<cap>`, every one naming the capability of the rung it
does not pack); and a component with no source (`pack-source-missing`) or whose source's
frame rate is not its timebase (`pack-source-rate`). Where its own work fails it
refuses by name too, and writes nothing: an encode the encoder refuses, such as a piece
past the samples it holds, or an object it wrote that does not count (`pack-encode`); a
piece past `2^32 − 1` units (`pack-piece-length`); catalogs it cannot write, the case
§11.3 refuses as `catalog-mismatch` (`pack-catalog`); a meter that was asked for and
gives no figure, a span too short to reach a microjoule or a figure past `2^64 − 1`
(`pack-meter`, never an unmetered package in its place); a measurement claim it cannot
write (`pack-measurement-claim`); and a listing it cannot write (`pack-listing`). It
writes a package only into a directory that is new or holds nothing, and refuses any
other before it packs (`pack-out-not-empty`): written among another package's files, it
would leave behind the ones it does not write.
## 12. Reasons
| Reason | Where |
|---|---|
| `policy-toml` (not UTF-8, a byte-order mark, not TOML 1.0, nesting past 32), `policy-float`, `policy-datetime`, `policy-negative`, `policy-integer-range` | §3.2 |
| `policy-unknown-key`, `policy-missing-key`, `policy-type`, `policy-value`, `policy-schema` | §3.3 |
| `policy-too-many-components`, `policy-too-many-rungs`, `policy-too-many-classes`, `policy-too-many-rules`, `policy-too-many-moments`, `policy-duplicate-id`, `policy-lead`, `policy-open-gop`, `policy-piece-bounds`, `policy-ladder-component`, `policy-refines-role`, `policy-dangling-refines`, `policy-refines-cycle`, `policy-refines-base`, `policy-refines-branch`, `policy-no-base`, `policy-class-layer`, `policy-rule-ladder`, `policy-layer-lag`, `policy-too-many-deadlines`, `policy-deadline-unreachable` | §3.4 |
| `capability-unknown`, `capability-not-payload`, `capability-candidate`, `capability-role`, `license-unclassified`, `license-refused`, each naming the capability (and the licence label) | §3.6 |
| `y4m-header`, `y4m-interlaced`, `y4m-colorspace`, `y4m-size-change`, `y4m-truncated`, `wav-format`, `wav-truncated` | §4.7 |
| `index-magic`, `index-version`, `index-truncated`, `index-reserved`, `index-media-class`, `index-role`, `index-tier`, `index-string`, `index-trailing` | §6.2 |
| `index-timebase`, `window-start`, `piece-order`, `piece-entries`, `piece-kind`, `entry-flags`, `entry-order`, `layer-role`, `refines-outside-window`, `refines-mismatch`, `layer-gap`, `tier-entropy-passthrough`, `tier-weakest`, `tier-mismatch`, `digest-without-tier`, `deadline-role`, `floor-derived-from`, `dup-length`, `dup-flag`, `class-plan`, `class-flags` | §6.3, §6.4 |
| `object-length`, `object-hash`, `non-envelope-entry`, `object-lengths`, `object-capability`, `object-refines`, `object-derived-from` | §6.3 |
| `floor-missing`, `floor-listing-mismatch`, `floor-stream-missing`, `floor-av1-malformed`, `floor-frame-count`, `floor-keyframe-mismatch` | §7.1, §7.3 |
| `floor-object-mismatch`, `floor-motif-mismatch`, `floor-av1-malformed`, `floor-keyframe-mismatch`, `floor-cmaf-mismatch`, against a package | §11.3 |
| `receipt-json`, `receipt-version`, `receipt-index-count`, `receipt-index-digest`, `receipt-stage-root`, `receipt-component`, `receipt-policy-digest`, `receipt-counts`, `receipt-parent-kind`, `receipt-energy`, `receipt-signature` | §9.1 |
| `measurement-claim-missing`, `measurement-claim-invalid` | §9.4 |
| `unexpected-file`, `listing-missing`, `listing-mismatch`, `policy-digest-mismatch`, `policy-json-mismatch`, `policy-component-mismatch`, `index-missing`, `index-policy-mismatch`, `plan-mismatch`, `object-missing`, `prefix-digest`, `catalog-mismatch`, `catalog-unexpected`, `object-unlisted` | §11.3 |
| `pack-id`, `pack-source-missing`, `pack-source-rate`, `pack-media-unsupported`, `pack-refinement-unsupported`, `pack-rule-ladder-unsupported`, `pack-floor-encode-unsupported`, `pack-capability-unsupported` (naming the capability) | §11.1, §11.4 |
| `pack-encode`, `pack-piece-length`, `pack-catalog`, `pack-meter`, `pack-measurement-claim`, `pack-listing`, `pack-out-not-empty` | §11.4 |