1448 lines
91 KiB
Markdown
1448 lines
91 KiB
Markdown
# WAI Extension: HTTP Delivery
|
||
|
||
> Status: Draft. Binds WAI envelopes to HTTP: a proposed media type, a `codecs`
|
||
> grammar, content-addressed immutable objects, verified byte-range fetch of one
|
||
> component of a `WAI2`, receipt, key and playlist sidecars, cache rules, delivery
|
||
> order and deadlines, and HLS / DASH adjacency for on-demand, live and low-latency
|
||
> live. No envelope byte and no manifest member changes. The reference implementation
|
||
> is in `wai-rs`: `http_binding` (feature `http`) for §3–§7, the origin side of §5
|
||
> and §9, the playlists of §10.4 and §11 and the adaptation policies of Appendix F;
|
||
> `http_binding::receipts` (with `receipt_claims`) and `http_binding::keys` (with
|
||
> `key_discovery`) for §8.1–§8.5; `wai_video_manifest` (feature `video_manifest`) for
|
||
> the playlist receipts of §8.6; and the `wai http` commands of the `wai` tool.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119 / RFC 8174.
|
||
|
||
## 1. Scope
|
||
|
||
This extension says how a WAI envelope is named, served, cached and fetched over HTTP
|
||
(RFC 9110, RFC 9111), and how WAI objects sit beside a classical HLS or DASH
|
||
presentation without changing it. It is the HTTP sibling of
|
||
[moq-streaming-format](moq-streaming-format.md); Appendix C maps the two. It defines
|
||
no codec, no player and no re-packing: a node that serves bytes other than those it
|
||
received is a producer under [jwp-receipts](jwp-receipts.md) §2.6 and §3.2, not a
|
||
cache under §9. What a refinement does to its base is
|
||
[staged-delivery](staged-delivery.md)'s and [SPEC](../SPEC.md) §7.1's; §10 defines
|
||
only how one is carried over HTTP and when a client gives up on it.
|
||
|
||
## 2. Terms
|
||
|
||
- **Object**: one WAI envelope ([SPEC](../SPEC.md) §2 or §7.1) served as one HTTP
|
||
representation.
|
||
- **Content hash**, **cid**: the object's [jwp-receipts](jwp-receipts.md) §2.1 content
|
||
hash, `BLAKE3("moq-jwp:object\x01" ‖ envelope)`, written as 64 lowercase hexadecimal
|
||
digits. `envelope_len` is the envelope's length in bytes.
|
||
- **Head**: for `WAI1`, the first `12 + man_len` bytes — magic, `man_len`, the manifest
|
||
and `payload_len`; for `WAI2`, the first `10 + man_len + 8·n` bytes — magic, `man_len`,
|
||
the manifest, `n_renditions` and the rendition table. Its length is `head_len`.
|
||
- **Component**: one entry of a `WAI2` rendition table and its payload bytes
|
||
([SPEC](../SPEC.md) §7.1). Its **range** is the `length` bytes from `head_len + offset`.
|
||
- **Covered length**: `covered_len = min(envelope_len, 1024·⌈(15 + head_len)/1024⌉ − 15)`.
|
||
- **Alias**: any URI other than `<cid>.wai` under which an object is served.
|
||
- **Base**: the object, or the component, a sink presents on its own.
|
||
- **Refinement**: bytes a sink MAY add to a base it can already present. It is one
|
||
of three things:
|
||
- a companion whose registered role is `enhancement` ([SPEC](../SPEC.md) §5, §7.1);
|
||
- a component with `role: "refinement"` (SPEC §7.1, *Refinement components*);
|
||
- a **layer object**: a `WAI1` envelope whose capability is refinement-only, which
|
||
carries `refines` ([staged-delivery](staged-delivery.md) §1, §3). A `WAI1` whose
|
||
capability is not refinement-only is not a layer object, whatever else it carries
|
||
(§10.4).
|
||
|
||
## 3. Media type `application/wai` (proposed; not registered)
|
||
|
||
An object is served with media type `application/wai`. The name is **proposed**:
|
||
RFC 6838 §3.1 admits a standards-tree registration only through an IETF document or a
|
||
recognized standards organization, and none has been made. Until one is, the name
|
||
identifies WAI objects between parties that implement this extension, and an
|
||
implementation MUST NOT present it as registered.
|
||
|
||
The type's properties, in the fields of RFC 6838 §5.6:
|
||
|
||
| field | value |
|
||
|---|---|
|
||
| Type name / Subtype name | application / wai |
|
||
| Required parameters | none |
|
||
| Optional parameters | `codecs` (§4) |
|
||
| Encoding considerations | binary |
|
||
| Security considerations | §15 |
|
||
| Interoperability considerations | `WAI1` and `WAI2` containers; a reader refuses an unknown MAJOR ([SPEC](../SPEC.md) §8) |
|
||
| Published specification | WAI SPEC.md; this extension |
|
||
| Fragment identifier considerations | §3.1 |
|
||
| Magic number(s) | `57 41 49 31` ("WAI1") or `57 41 49 32` ("WAI2") at offset 0 |
|
||
| File extension(s) | `.wai` |
|
||
|
||
### 3.1 Fragment identifiers
|
||
|
||
A fragment on an object's URI is a `&`-separated list of `key=value` pairs. It is never
|
||
sent to a server and never changes the object. Unknown keys are ignored.
|
||
|
||
- `cid=<64 hex>`: the object's content hash, on an alias (§5.2).
|
||
- `n=<decimal>`: the object's `envelope_len`.
|
||
- `h=<decimal>`: the object's `covered_len`.
|
||
- `c=<component id>`: a preference for the component whose `id` ([SPEC](../SPEC.md)
|
||
§7.1) is that value, which a sink MAY honour as deployer policy within §7.
|
||
|
||
`n` and `h` are hints, never checks: every length that matters is checked against the
|
||
head proof (§6.3) or, for an object fetched whole, the cid. A wrong `h` costs a request.
|
||
A wrong `n` below `1009` costs the whole object: the client fetches it whole (§5.4),
|
||
whatever its real length.
|
||
|
||
## 4. The `codecs` parameter
|
||
|
||
### 4.1 Grammar
|
||
|
||
The `codecs` parameter of `application/wai`, the HLS `CODECS` attribute and the DASH
|
||
`@codecs` attribute carry WAI elements in this grammar (ABNF, RFC 5234):
|
||
|
||
```
|
||
wai-element = wai-prefix "." alt *( "|" alt )
|
||
wai-prefix = "wai1" / "wai2"
|
||
alt = cap *( "+" cap )
|
||
cap = 1*lcalnum 1*( "." 1*( lcalnum / "_" ) )
|
||
lcalnum = %x61-7A / %x30-39
|
||
```
|
||
|
||
- An element is one RFC 6381 `id-simple`; elements are separated by `,` as RFC 6381
|
||
§3.2 separates codecs, and `.` is its hierarchy delimiter. Spaces and tabs around an
|
||
element are not part of it, and an empty element is malformed. Classical elements
|
||
(for example `mp4a.40.2`) MAY appear beside WAI elements. An element that begins with
|
||
`wai`, in any ASCII case, is a WAI element: it MUST match the grammar, and is never
|
||
read as classical.
|
||
- `wai1` states that every object the element describes is a `WAI1` envelope; `wai2`
|
||
states that at least one is a `WAI2` envelope. Both name WAI MAJOR 1.
|
||
- A `cap` is a capability string ([SPEC](../SPEC.md) §5) without its leading `wai.`.
|
||
Every capability this revision registers begins `wai.` and matches the rule once
|
||
that is removed, so the mapping is one-to-one.
|
||
- In an `alt`, the first `cap` is a **selectable** capability, and each `+cap` is a
|
||
capability whose payload is applied with it: a companion ([SPEC](../SPEC.md) §7.1)
|
||
bound to a rendition of it, or a refinement whose base is a rendition of it
|
||
(§2).
|
||
- An element MUST NOT name a capability whose registered kind is `derivation` or
|
||
`record` (`codecs_not_media`), or a candidate (`codecs_candidate`). A first `cap`
|
||
MUST NOT be companion-only or refinement-only, and a `+cap` MUST NOT be registered as
|
||
standalone media (`codecs_malformed`); a capability this revision does not register
|
||
may stand in either place, as SPEC §7.1 lets it be a rendition or a companion. A
|
||
`cap` MUST NOT appear twice among an element's `alt`s, nor twice in one `alt`. A
|
||
string that breaks any of these is `codecs_malformed`.
|
||
|
||
### 4.2 Meaning: the union and the floor
|
||
|
||
- **Union.** The element describing one object or a sequence of objects (an HLS
|
||
variant, a DASH Representation, a MoQ track) lists every capability any of them may
|
||
dispatch to or apply. For a `WAI1`, its `capability` and its `fallback`, each where
|
||
SPEC §4 dispatches it, as an `alt`: a record, a derivation, a candidate, a
|
||
companion-only or a refinement-only capability is never listed, so a `WAI1` naming a
|
||
record beside a payload fallback is described by the fallback. For a `WAI2`, one
|
||
`alt` per distinct selectable capability ([SPEC](../SPEC.md) §7.1), each with `+` and
|
||
every companion capability bound to a rendition of it and every refinement capability
|
||
whose base is a rendition of it. An object that lists no capability is not media
|
||
(`codecs_not_media`). A `WAI1` whose capability is not refinement-only is described
|
||
by its capability and fallback as above even when it carries `refines`, which a
|
||
client does not read for it (§10.4). A layer object (§10.4) has no element of its own: its
|
||
capability follows `+` in the `alt` of the base its `refines.base` names among the
|
||
sequence's objects — a `WAI1`, through its `capability` (staged delivery never takes
|
||
a base through its fallback), or a component of a `WAI2`, by the content hash of the
|
||
component's exploded envelope ([SPEC](../SPEC.md) §7.1), through the component's
|
||
capability. An object a sequence lists more than once (a repeated piece) is one object
|
||
of it, described once with every layer it has, and a layer joins every object of the
|
||
sequence that is its base, so where a layer is listed beside one occurrence of its base
|
||
it describes them all. A sequence with a layer whose base it does not hold cannot be
|
||
described (`codecs_layer_unbound`). `alt`s are ordered by first appearance, and the
|
||
capabilities after `+` by first appearance in table order, then a sequence's layers
|
||
in order. The union is exhaustive: a sink that finds, in a head it has verified, a
|
||
selectable, companion or refinement capability the union does not list refuses that
|
||
object (`codecs_undeclared`). A layer is declared only when its capability follows `+`
|
||
in the `alt` of its own base's capability — the base its `refines.base` names, as
|
||
above, which the sink holds — and not by following `+` in any other `alt`; a sink
|
||
that does not hold that base cannot find the layer declared.
|
||
- **Floor.** The floor of a sequence is the `alt`s whose first `cap` is selectable in
|
||
**every** object that is not a layer, each with the capabilities after `+` that are
|
||
bound to it, or joined to it by a layer, in every such object. For one object, the
|
||
floor is its union. A sequence whose floor is
|
||
empty MUST NOT be published as one variant (`codecs_no_common_capability`). The
|
||
floor is written as a `wai-element` with the union's prefix, in HLS in the
|
||
`X-WAI-FLOOR` attribute (§11.1) and in DASH in a `SupplementalProperty` (§11.1).
|
||
- **Support.** A sink **supports** an element when it reads the element's container
|
||
(`wai1`: `WAI1`; `wai2`: both) and supports the first `cap` of at least one `alt`
|
||
of the **floor**. The capabilities after `+` never affect support: a sink that
|
||
cannot apply one presents the rendition ([SPEC](../SPEC.md) §7.1). A sink supports
|
||
a `codecs` value when it supports every element.
|
||
- An element states capabilities, never parameter sets: a sink that supports a
|
||
capability may still refuse an object's pin ([SPEC](../SPEC.md) §3.1) and select
|
||
another rendition.
|
||
|
||
### 4.3 Not a question for the browser's decoders
|
||
|
||
An interface that answers for the user agent's own decoders (Media Capabilities,
|
||
`MediaSource.isTypeSupported`, WebCodecs `isConfigSupported`) cannot answer for a WAI
|
||
element. A sink MUST NOT pass a WAI element to such an interface, and MUST NOT read its
|
||
answer about a classical codec string as support for a WAI capability, except for a
|
||
capability whose payload the sink hands to that user-agent decoder (§12).
|
||
|
||
## 5. Objects
|
||
|
||
### 5.1 One envelope, nothing after it
|
||
|
||
An object MUST be exactly one envelope: for `WAI1`, `envelope_len = 12 + man_len +
|
||
payload_len`; for `WAI2`, `envelope_len = head_len + Σ lengths`. A reader that finds
|
||
another length refuses the object (`object_length_mismatch`). This rule binds HTTP
|
||
objects; it does not change which envelopes conform to [SPEC](../SPEC.md).
|
||
|
||
### 5.2 Names and aliases
|
||
|
||
An object SHOULD be published at a URI whose last path segment is `<cid>.wai`. Such a
|
||
URI names those bytes forever: a publisher MUST NOT serve other bytes under it.
|
||
|
||
An object MAY also be served under an alias. An alias MUST serve exactly the object's
|
||
bytes with its `ETag`, and MUST NOT serve other bytes at any time. A client learns no
|
||
content hash from a response, so before it presents anything fetched by alias it MUST
|
||
verify the bytes against a cid it holds from a listing (a `cid` fragment or a
|
||
`<cid>.wai` URI in a playlist) or from a verified receipt.
|
||
|
||
### 5.3 Response headers
|
||
|
||
For a `GET` or `HEAD` of an object an origin MUST send:
|
||
|
||
```
|
||
Content-Type: application/wai (MAY carry ; codecs="…" of §4 for that object)
|
||
Cache-Control: public, max-age=31536000, immutable, no-transform
|
||
ETag: "<cid>"
|
||
Accept-Ranges: bytes
|
||
Access-Control-Allow-Origin: *
|
||
Access-Control-Expose-Headers: Content-Range, Content-Length, ETag
|
||
Timing-Allow-Origin: *
|
||
```
|
||
|
||
It MUST NOT send `Content-Encoding` other than none, MUST NOT send `Vary`, and MUST
|
||
NOT select bytes by any request header. An object listed only at a live edge (§11.4)
|
||
MAY carry a smaller `max-age`, not less than the time it stays listed. An origin MAY
|
||
send `Repr-Digest: sha-256=:<base64>:` (RFC 9530 §3) over the whole envelope.
|
||
|
||
An origin MUST answer a preflight for an object with
|
||
`Access-Control-Allow-Headers: Range, Priority` and `Access-Control-Max-Age`.
|
||
|
||
A server MUST answer a single byte-range request on an object with `206` and a
|
||
`Content-Range` whose complete length is `envelope_len` (RFC 9110 §14.4, §15.3.7), or
|
||
with `200` and the whole object. It MAY answer a multi-range request with `200`.
|
||
|
||
### 5.4 Sizes
|
||
|
||
A publisher SHOULD give each listed object's `n` and, for a `WAI2`, its `h` (§3.1). A
|
||
client that holds `n` below `1009` fetches the object whole: no head proof can cover
|
||
less than one chunk.
|
||
|
||
### 5.5 Every media class (informative)
|
||
|
||
The binding is the same for every capability. What a range fetch saves, and where a
|
||
refinement sits, depend on the media class.
|
||
|
||
| registry media | typical object | range fetch saves | refinement, carried as |
|
||
|---|---|---|---|
|
||
| image | `WAI2`: a pinned integer learned rendition beside standard image renditions | every rendition but the pick | a refinement component, or layer objects |
|
||
| audio | a `WAI1` per segment; or a `WAI2` with an integer or learned codec beside a standard one | the renditions not picked | a refinement component, or layer objects |
|
||
| video | one object per segment or part, aligned with a classical ladder (§11) | the same, per segment | an enhancement companion such as `wai.video.lcevc` (§10.3), a refinement component, or layer objects |
|
||
| metadata (companions) | bound to a rendition in a `WAI2` | fetched only with its rendition | — |
|
||
| interactive (worlds, feeds, avatars) | snapshot and op-delta objects | head first: decide, then fetch | later op-deltas are separate objects |
|
||
| volumetric | `WAI2`: an integer splat codec beside a standard mesh, splat or point-cloud payload | the rendition not picked | a refinement component, or layer objects |
|
||
| haptic, text | small objects | nothing: below 1009 bytes they are fetched whole (§5.4) | — |
|
||
| binary (parameter sets, circuits, ternary) | a parameter set served as an immutable resource, verified by its pin (§8.5) | — | — |
|
||
| records and derivations | carried as objects, never named in `codecs` (`codecs_not_media`) | — | — |
|
||
|
||
### 5.6 AI usage preferences
|
||
|
||
An object whose manifest carries a usage statement ([ai-usage](ai-usage.md) §4.1) carries it
|
||
signed, with the content. An origin **SHOULD** also send it in the fields
|
||
`draft-ietf-aipref-attach-05` §2 and the TDM Reservation Protocol §6.2 define, written from the
|
||
signed statement (ai-usage §4.4): `Content-Usage` with the `aipref` value, byte for byte, and
|
||
`tdm-reservation` and, where the statement has one, `tdm-policy`. It names them in
|
||
`Access-Control-Expose-Headers` beside §5.3's, so that a client in a browser can read them.
|
||
These fields are unsigned: a client compares them with the signed statement, and a response
|
||
whose fields contradict it is refused (`header_conflicts`), the signed statement governing.
|
||
|
||
A gateway that requires consumption claims of agents (ai-usage §7) reads the request field
|
||
`WAI-Usage-Claim`, names it in `Access-Control-Allow-Headers` beside §5.3's, and answers a
|
||
request it refuses with `403`. It never serves other bytes under an object's name: refusing is
|
||
not selecting (§5.3).
|
||
|
||
## 6. The head proof
|
||
|
||
### 6.1 What it proves
|
||
|
||
A head proof lets a client verify the first `covered_len` envelope bytes against the cid
|
||
without the rest of the object. The content hash is a BLAKE3 root over 1024-byte chunks
|
||
of `X = "moq-jwp:object\x01" ‖ envelope`; the proof is the chaining values of the
|
||
subtrees of `X` that lie wholly at or after offset `c = 15 + covered_len`. It is a
|
||
pure function of the object: anyone holding the object can produce it, and a wrong one
|
||
fails verification.
|
||
|
||
### 6.2 Format (`<cid>.wai.hp`)
|
||
|
||
All integers are little-endian ([SPEC](../SPEC.md) §2).
|
||
|
||
| offset | size | field |
|
||
|---|---|---|
|
||
| 0 | 4 | magic `57 48 50 31` ("WHP1") |
|
||
| 4 | 32 | cid, raw bytes |
|
||
| 36 | 8 | `envelope_len` (u64) |
|
||
| 44 | 8 | `covered_len` (u64), equal to §2's formula |
|
||
| 52 | 1 | `n_cv` (u8), at most 64 |
|
||
| 53 | 32·n_cv | chaining values, in verification order |
|
||
|
||
The file is exactly `53 + 32·n_cv` bytes. When `covered_len = envelope_len`, `n_cv` is
|
||
0 and the client verifies the whole object: its content hash is computed over the bytes
|
||
it holds. A head proof is served under §5.3's cache headers with
|
||
`Content-Type: application/octet-stream`.
|
||
|
||
### 6.3 Verification
|
||
|
||
With `L = 15 + envelope_len`, `c = 15 + covered_len`, and
|
||
`left(n) = 1024 · 2^⌊log₂((n−1)/1024)⌋`, when `covered_len < envelope_len`:
|
||
|
||
```
|
||
cv(start, len):
|
||
if start ≥ c: take the next chaining value from the proof
|
||
elif start + len ≤ c: the non-root chaining value of the len bytes of X at start,
|
||
a subtree whose first chunk has counter start/1024
|
||
else: parent_cv(cv(start, left(len)), cv(start+left(len), len−left(len)))
|
||
root = parent_root(cv(0, left(L)), cv(left(L), L − left(L)))
|
||
```
|
||
|
||
`parent_cv` and `parent_root` are BLAKE3's parent node in hash mode, without and with
|
||
the ROOT flag. `c` is a multiple of 1024 whenever `covered_len < envelope_len`, so
|
||
every subtree the procedure meets lies wholly before `c` or wholly at or after it once
|
||
it is split far enough. A client checks, in order:
|
||
|
||
1. the file has the format above; its cid is the object's; its `envelope_len` equals
|
||
the complete length of every `Content-Range` and the length of every `200` body it
|
||
received; and its `covered_len` is `envelope_len`, with `n_cv` 0, or is less than
|
||
it with `15 + covered_len` a multiple of 1024 (`head_proof_malformed`);
|
||
2. the chaining values are consumed exactly, and `root` equals the cid
|
||
(`head_proof_mismatch`);
|
||
3. `envelope_len` is the length the verified head gives (§5.1), and `covered_len` is
|
||
§2's formula for that head (`head_proof_malformed`). The root does not encode the
|
||
object's length, so a wrong `envelope_len` that leaves the tree's shape unchanged
|
||
passes check 2 and is refused here.
|
||
|
||
No covered byte is read as a head before check 2 passes. A producer writes the
|
||
chaining values in the order the procedure consumes them.
|
||
|
||
## 7. Byte-range fetch of a `WAI2` component
|
||
|
||
A client that fetches part of an object MUST verify, before it presents or decodes
|
||
anything, every byte it uses: the head against the cid (§6), and each component it
|
||
uses against the `sha256` of its entry in that verified head ([SPEC](../SPEC.md) §7.1).
|
||
The procedure:
|
||
|
||
1. In parallel, request `Range: bytes=0-(E−1)`, with `E` the `h` fragment if given and
|
||
16369 otherwise, and request `<cid>.wai.hp`.
|
||
2. Check each response (§7.1). If `covered_len` exceeds the bytes received, request the
|
||
rest of the first `covered_len` bytes.
|
||
3. Verify the head (§6.3). Parse it (`head_malformed`, which covers a manifest past
|
||
[SPEC](../SPEC.md) §2's reader limits); apply §5.1; refuse a
|
||
capability the variant's union does not list (`codecs_undeclared`).
|
||
4. Select from the head exactly as [SPEC](../SPEC.md) §7.1 selects, deferring only
|
||
the component-digest test to the bytes. A deployer policy MAY reorder and restrict
|
||
the entries considered, as SPEC §7.1 allows. The result is the picked entry, the
|
||
companions SPEC §7.1 binds to it, and the refinement components whose
|
||
`refines.base` is its `id`. SPEC §7.1 offers an envelope's refinements only when every
|
||
one of its components conforms, which the verified head's manifest decides: where one
|
||
does not, each of those refinement components is dropped
|
||
(`components_nonconforming`) and never requested, one that states no `sha256` being
|
||
dropped for that too (step 5), and the base's path does not change.
|
||
5. If the picked entry, or a bound companion the sink will apply that is not a
|
||
refinement (§2), states no `sha256`, go to step 7 (`component_digest_absent`). A
|
||
refinement that states none is dropped: the client does not request it, and the
|
||
base's path does not change.
|
||
6. Request the ranges of the picked component and of the companions and refinements it
|
||
will apply that the client does not already hold: one request per range, merging
|
||
ranges that touch only when they are in the same priority class (§10.1), so a
|
||
refinement is always requested on its own. Verify each against its entry's
|
||
`sha256`. On a mismatch of the picked component or of a companion that is not a
|
||
refinement (`component_digest_mismatch`), discard every byte held past
|
||
`covered_len` and go to step 7. A refinement that mismatches is dropped alone.
|
||
7. Request the rest, `bytes=covered_len-` (or `GET` the whole object), verify the whole
|
||
envelope against the cid (`object_digest_mismatch`), and select as [SPEC](../SPEC.md)
|
||
§7.1 selects on the whole envelope.
|
||
|
||
A client SHOULD also go to step 7 when the base-class bytes step 6 would request — the
|
||
picked component's and those of the companions that are not refinements — are at least
|
||
90 % of the bytes after `covered_len`. A refinement's bytes never count, so a refinement
|
||
never moves its base to step 7; when the base goes there, the rest of the object carries
|
||
the refinement's bytes with it. A client MUST NOT send a multi-range request and rely
|
||
on a multipart answer. A `WAI1` has one component, its payload: a client decides from
|
||
its verified head whether it can dispatch it ([SPEC](../SPEC.md) §4), and only then
|
||
fetches the rest after the head. A layer object (§10.4) is never dispatched: the
|
||
client fetches it whole, at refinement urgency, verifies it against its cid, and hands
|
||
it to [staged-delivery](staged-delivery.md) §4.
|
||
|
||
### 7.1 Response checks
|
||
|
||
For each request a client checks, in order:
|
||
|
||
- the status is `206`, or `200` with a body that is the whole object
|
||
(`http_status_unexpected`);
|
||
- no `Content-Encoding` other than none is present (`content_coding_present`);
|
||
- on a `206`, `Content-Range` names exactly the requested range, its last position
|
||
reduced to `envelope_len − 1` where the request reached past the end
|
||
(`content_range_mismatch`), and its complete length equals the head proof's
|
||
`envelope_len` (`complete_length_mismatch`). Each of its numbers is one or more ASCII
|
||
digits, RFC 9110 §14.4's `1*DIGIT`: a sign, a separator, a space inside the field or
|
||
another script's digit is refused the same way;
|
||
- on a `200`, the body length equals the head proof's `envelope_len`
|
||
(`complete_length_mismatch`).
|
||
|
||
A refused response's bytes are discarded and never presented. A refused base-class
|
||
response ends the fetch; a refused refinement-class response abandons that refinement
|
||
(§10.2), and the base goes on. A client MAY retry once with `Cache-Control: no-cache`.
|
||
|
||
### 7.2 One pick on every path
|
||
|
||
On bytes that verify, §7 picks the same entry that [SPEC](../SPEC.md) §7.1 picks on
|
||
the whole envelope: the digest test runs on the bytes either way, and every case in
|
||
which a range client cannot run it (no `sha256`, a mismatch) is decided on the whole
|
||
envelope. A range client never picks an entry the whole-envelope selection would skip,
|
||
and never skips one it would pick. When the head alone shows that no entry can be
|
||
picked, the client stops there and reports the reasons the head gives; an entry whose
|
||
pin was refused may also fail its digest, which only its bytes would show.
|
||
|
||
## 8. Receipts, keys, grants and parameter sets over HTTP
|
||
|
||
### 8.1 Receipt files
|
||
|
||
`rcpt/<h>.json`, resolved against the object's URI, holds exactly one receipt: a group
|
||
receipt in the labelled payload whose link hash ([jwp-receipts](jwp-receipts.md) §3 step
|
||
3) is `h`, in its JSON form (§2.4.1 there), or a claim whose `link_hash` (§2.4.3) is `h`,
|
||
each written in JCS (RFC 8785), in at most 1 MiB (1 048 576 bytes). It is immutable and
|
||
served under §5.3's cache headers with `Content-Type: application/json`. A client reads at
|
||
most that many bytes of a receipt file and refuses a longer one (`receipt_too_large`); it
|
||
reads the file as [jwp-receipts](jwp-receipts.md) §3.1 step 1 reads JSON, then as the
|
||
receipt it fetched it for — a group receipt by §3 step 1, a claim by §3.1 steps 1–2, with
|
||
their reasons — and refuses it unless its link hash, computed from what it read, is its
|
||
name (`receipt_name_mismatch`). A group in the legacy payload is refused
|
||
(`receipt_legacy_group`): its link hash does not cover its parent, so a server could
|
||
serve other bytes, naming another parent, under the one name. A name at which nothing is
|
||
served is `receipt_not_served`. Every file a client fetches by name — a group, a parent,
|
||
a claim, or a declaration a claim names (§8.2) — is read so first, and refused with these
|
||
reasons before any check of what it holds. A receipt is a few kilobytes; the cap leaves
|
||
room for claim bodies that list many objects, and bounds what one name costs.
|
||
|
||
A group receipt's `parent_receipt_hash` is the name of its parent's file. A client
|
||
resolves [jwp-receipts](jwp-receipts.md) §3 step 3's `parent_not_held` by fetching
|
||
`rcpt/<parent_receipt_hash>.json`, never by skipping the step: it reads the file as a
|
||
group receipt of that name, requires it on the group's key and track (a group of another
|
||
chain is never a predecessor), and offers it to its chains before the group; while that
|
||
parent is itself `parent_not_held`, it fetches the parent's parent the same way. Once one
|
||
is admitted, the parents fetched after it are offered again, nearest it first, then the
|
||
group. A client fetches at most 64 parents for one group, whatever bound it is asked to
|
||
use, and MAY hold a lower one. A parent that is not served, that does not read or is
|
||
refused, that is of another chain, a walk past the bound, and a walk that would pass the
|
||
index's fetch budget (§8.2), each leave the group `parent_not_held`, and the client says
|
||
which. A client that declares it joins (jwp-receipts §3 step 3, *Joining*) anchors a
|
||
chain it holds nothing of at the first group instead, walks nothing, and reports the
|
||
chain anchored. A claim's `parent_receipt_hash` names its parent's file the same way:
|
||
for §3.1 step 5 the client fetches it, reads it as a claim of that name and verifies it
|
||
under §3.1 steps 1–4, and a parent not served, or refused, leaves the claim
|
||
`parent_not_held`.
|
||
|
||
### 8.2 Receipt index
|
||
|
||
`<cid>.wai.receipts` is the sidecar carriage of the `WAI_RECEIPT` property
|
||
([moq-streaming-format](moq-streaming-format.md) §5), and the place a client finds an
|
||
object's receipts by their hashes: each delivery item's group receipt by the property's
|
||
`group_receipt`, each claim about the object by its link hash — a `stage` claim about a
|
||
stage set's base ([jwp-receipts](jwp-receipts.md) §2.4.10) among them — and each of their
|
||
parents by the link its child names (§8.1).
|
||
|
||
```json
|
||
{ "kind": "wai-receipt-index", "object": "<cid>",
|
||
"delivery": [ { "property": { …WAI_RECEIPT… }, "object_receipt": { …jwp-receipts §2.4.2… } } ],
|
||
"claims": [ "<link hash>" ] }
|
||
```
|
||
|
||
It is mutable and is served with `Cache-Control: no-cache, no-transform` and an `ETag`
|
||
of its own. It is a locator, never evidence. A client reads at most 4 MiB (4 194 304
|
||
bytes) of it and refuses a longer one (`receipt_index_too_large`): room for the most
|
||
items and claims it may list. It reads it as
|
||
jwp-receipts §3.1 step 1 reads JSON, with exactly the members above, `kind` that string,
|
||
`object` 64 lowercase hexadecimal digits and the other two arrays, and refuses it whole
|
||
otherwise (`receipt_index_malformed`), when it lists more than 256 delivery items or more
|
||
than 256 claims (`receipt_index_too_large`), or when `object` is not the cid
|
||
(`receipt_not_about_object`). An index lists the groups an object was delivered under,
|
||
one per track and publisher, and each signer's latest claims, which chain to their
|
||
parents, so these bound a listing, not a history. Each item it lists is read and checked on its own, and one
|
||
that fails is ignored, with its reason, item by item: a delivery item is an object with
|
||
exactly `property` and `object_receipt` (`receipt_index_malformed`), whose property reads
|
||
as moq-streaming-format §5's (`property_invalid`) and whose object receipt reads as
|
||
jwp-receipts §2.4.2's; a claim entry is a link hash (`receipt_index_malformed`).
|
||
|
||
To check one index, a client fetches each receipt file name once, keeping what was served
|
||
for the rest of the check, and fetches at most 1024 files in all, enough for every item
|
||
and claim an index may list, a declaration for each claim and full parent walks for a
|
||
few groups; a fetch past that is `receipt_fetch_budget_exhausted`. It reads at most 64 MiB
|
||
(67 108 864 bytes) of receipt files in all: it reads each file no further than what
|
||
remains of that budget, or than one byte past the file's cap where less remains of
|
||
neither; a file that reaches the end of the budget is refused, since whether it ends
|
||
there is not read, and once the budget is spent no further file is fetched
|
||
(`receipt_fetch_bytes_exhausted`). Without it, 1024 files at their cap would let one
|
||
index make a client read and hold a gibibyte. A client MAY hold lower budgets.
|
||
|
||
A delivery item is checked in this order:
|
||
|
||
1. the property's `content_hash` and the object receipt's are the cid
|
||
(`receipt_not_about_object`);
|
||
2. the group receipt is fetched as `rcpt/<property.group_receipt>.json` and read as
|
||
§8.1 states;
|
||
3. [jwp-receipts](jwp-receipts.md) §3 steps 1–3, against the chains the client holds,
|
||
missing parents recovered as §8.1 states;
|
||
4. §3 steps 4 and 5 for the object receipt, step 4 against what the client holds of the
|
||
object (§8.3): the object is then `delivered-verified`; or, for an object the client
|
||
did not receive, step 4 not run: it is `inclusion-verified`, never delivered;
|
||
5. the property: for a delivered object, against its object receipt as
|
||
moq-streaming-format §5 states (`property_invalid`, `property_disagrees`), which
|
||
gives the property's reading; for one not delivered, alone, giving no reading.
|
||
|
||
A claim is fetched as `rcpt/<link hash>.json` and read as §8.1 states, then checked
|
||
against the delivery items that passed the checks above for this cid, in the index's
|
||
order. Its `subject` is the cid, or, for a claim about a receipt, the link hash of the
|
||
group of one of those items: a group named only by an item that was refused, or by
|
||
nothing, attaches nothing, and the claim is `receipt_not_about_object`. Then
|
||
[jwp-receipts](jwp-receipts.md) §3.1 steps 1–4; step 5, its parent fetched as §8.1
|
||
states; and step 6, against the receipts it names that the client holds here:
|
||
|
||
- an `energy` claim, against the object receipt of each item delivered (step 4 run over
|
||
what the client holds of the object, §8.3), and against the OER receipt it names. This
|
||
revision carries no OER receipt over HTTP, so an energy claim that names one is
|
||
`context_not_held`;
|
||
- a `measurement` or `operating-point` claim, against the receipt that carries its
|
||
figure: for the `moq-jwp:object` profile, the object receipt of an item delivered under
|
||
the group the claim names;
|
||
- a `delivery-gap` claim, against the object receipt of an item at its coordinates,
|
||
verified by inclusion at least;
|
||
- a lawful-basis decision, and a recovery record, against the declaration it names,
|
||
fetched as `rcpt/<link hash>.json` and read as §8.1 states, whose reasons refuse it
|
||
first (another claim served under the declaration's name is `receipt_name_mismatch`);
|
||
a declaration that reads under its name and does not match is refused with step 6's
|
||
reason. A recovery record is also checked against the publisher key of the group of an
|
||
item that passed, the one that signed the declaration where there is one, and with
|
||
the client's matchers (§2.6.3 step 5), whether or not the client holds the object;
|
||
- a `reconstruct-session` claim, against its outputs, the objects it received and the
|
||
envelopes it received, none of which the index carries;
|
||
- a `transmission` claim, against the group of an item whose link hash is its subject
|
||
([bearer-receipts](bearer-receipts.md) §4.3); and a `reconciliation` claim, against that
|
||
group and the object receipts of the items listed under it here (bearer-receipts §5.3):
|
||
one that names an object this index does not list is `context_not_held`.
|
||
|
||
A claim is accepted only when step 6 ran against what it needs. A `measurement` claim
|
||
whose figure the client does not hold is `figure_not_held`, as §3.1 states; any other
|
||
claim whose step 6 needs a receipt the client does not hold is `context_not_held`. Neither
|
||
is accepted. A claim of a class step 6 checks against nothing is accepted after step 5.
|
||
|
||
A soft-binding claim ([jwp-receipts](jwp-receipts.md) §2.6.2) whose subject is another
|
||
object attests that object, the ancestor, never the object in hand, and only through
|
||
§3.2's recovery: the client holds the object, it holds the publisher key of a delivery
|
||
receipt for the ancestor that it verified under §3 over that object's bytes, and §3.2's
|
||
steps 2–5 pass with a matcher for the claim's algorithm, the claim then reported as
|
||
attesting the ancestor. Of the publisher keys it holds for the ancestor, it uses the one
|
||
that signed the claim, whichever delivery it verified first, so a delivery under another
|
||
key cannot block a recovery; holding none that did, it uses the first, and the recovery
|
||
fails with §3.2's reason. A failed recovery is reported with that reason. Without such a
|
||
key or the object, no recovery runs, and the claim is `receipt_not_about_object`.
|
||
|
||
A client MAY confirm the signer of each item it accepts under
|
||
[jwp-receipts](jwp-receipts.md) §7.4, against key logs it holds as §8.4 states, and
|
||
reports the confirmation beside the item's verdict, never folded into it.
|
||
|
||
### 8.3 A range-fetched object
|
||
|
||
A client that holds only the head and some components cannot recompute the content
|
||
hash over the envelope. Under [jwp-receipts](jwp-receipts.md) §3 step 4 it takes the
|
||
cid its head proof verified, and the receipt then attests the object the cid names; the
|
||
components it holds are attested through the verified head's `sha256` values. The
|
||
content hash step 4 compares with comes only from bytes the client hashed, or from a head
|
||
proof it verified with every component it uses checked against its entry's digest (§7):
|
||
never from a URI, an index, a playlist or a receipt, which only state one. A fetch that
|
||
was refused establishes nothing, and an object the client did not receive — a
|
||
refinement it abandoned (§10.5), or one it never requested — has its receipts checked
|
||
by inclusion alone.
|
||
|
||
A component's exploded envelope ([SPEC](../SPEC.md) §7.1, *Exploding a component*) is
|
||
a function of the verified head and the component's bytes, so a client that holds them
|
||
builds it. A receipt about that exploded envelope is checked under step 4 over the
|
||
envelope the client built, as for any object: this is how a refinement component
|
||
fetched by range meets [staged-delivery](staged-delivery.md) §10.1, which applies a layer
|
||
only on a receipt checked over its envelope bytes.
|
||
|
||
### 8.4 Keys, statuses and grants
|
||
|
||
| resource | location | `Cache-Control` |
|
||
|---|---|---|
|
||
| key log ([jwp-receipts](jwp-receipts.md) §7.2) | `/.well-known/wai-keys` | `max-age=300, no-transform` |
|
||
| key status (§7.9 there) | `/.well-known/wai-key-status` | `max-age` at most a quarter of the head's `status_max_age_s`, `no-transform` |
|
||
| module grants (§7.9 there) | `/.well-known/wai-grants/<module digest>.json`: `{"kind":"wai-module-grants","module":"<digest>","grants":[…]}` | `max-age=300, no-transform` |
|
||
|
||
All are `application/json`. A verifier **anchors** an origin ([jwp-receipts](jwp-receipts.md)
|
||
§7.3) only from a key log retrieved from the origin's key-log location — an `https`
|
||
origin followed by `/.well-known/wai-keys` — answered `200` by that same URL: it does
|
||
not follow a redirect, asks with `Cache-Control: no-cache`, and a log that reached it any
|
||
other way anchors nothing (`key_log_not_from_origin`). A retrieval is an `https`
|
||
retrieval the verifier made itself: TLS to the origin's host and port (443 when the
|
||
origin names none), the server's certificate validated for that host against the trust
|
||
anchors the verifier holds, the request and the `200` exchanged on that connection, and
|
||
the response's end authenticated by TLS or by its own framing. These are facts of the
|
||
request it sent and the response it received, so a log retrieved over plaintext, or one
|
||
another party states was so retrieved, anchors nothing. A verifier reads at most 1 MiB
|
||
(1 048 576 bytes) of a key log, however it reaches it, and refuses a longer one whole
|
||
(`key_log_too_large`): a log is a few revisions of a few keys each, and the cap bounds
|
||
what one retrieval costs. Later revisions MAY come through
|
||
any cache, a mirror, or the origin: such a log is read as §7.3 reads one, its revisions
|
||
are held and each is checked against its parent by §7.4's head walk, and it never moves
|
||
the anchor. A copy whose latest revision is a trusted revision older than the anchor is
|
||
reported (`key_log_rolled_back`) and adds nothing.
|
||
|
||
The grants file is read as jwp-receipts §3.1 step 1 reads JSON, with exactly the members
|
||
above, `module` the digest it is fetched for, or not at all (`grants_file_malformed`). It
|
||
is a locator: each grant in it goes to §7.9's checks on its own, and one that fails them
|
||
is passed over.
|
||
|
||
A module grant is §7.9's form. WAI carries a JCP grant only as a reference by its content
|
||
hash inside the receipt that names it, as a playlist receipt carries its publisher's
|
||
grant (§8.6); this binding serves no JCP grant, and a client checks such a reference only
|
||
as the receipt carrying it binds it.
|
||
|
||
### 8.5 Parameter sets
|
||
|
||
A parameter set MAY be served as an immutable resource named by the digest its pin
|
||
states (`<base>/<sha256>`), under §5.3's cache headers with `application/octet-stream`;
|
||
a set in the form `wai.prior.set` is served as its listing under that digest and each
|
||
file the listing names under its own digest. The name locates the bytes and proves
|
||
nothing. A sink holds a fetched set only as [SPEC](../SPEC.md) §3.1's checks hold it over
|
||
the bytes it received, the digest among them, check 5 being
|
||
[jwp-receipts](jwp-receipts.md) §7.9 with the statuses and grants it fetched as §8.4
|
||
states. Bytes served under a set's name that are not the set are not held
|
||
(`prior_not_held`), a set §7.9 refuses is not held (`prior_revoked`,
|
||
`prior_not_authorised`), and dispatch continues as [SPEC](../SPEC.md) §4 and §7.1 state.
|
||
|
||
### 8.6 Playlist receipts
|
||
|
||
A WAI media playlist (§11) is authenticated by a manifest receipt (`wai.video.manifest`,
|
||
[SPEC](../SPEC.md) §5) whose `manifest_hash` is the BLAKE3 of the playlist bytes,
|
||
served immutable at `mr/<manifest_hash>.json` resolved against the playlist URI, in the
|
||
form of Appendix H, and with the same bytes at `mr/<receipt_hash>.json`, its receipt
|
||
hash (Appendix H), so a receipt can be fetched by the link a later one names. Each
|
||
revision of a live playlist has its own receipt, whose `parent_receipt_hash` is the
|
||
receipt hash of the previous revision's receipt, under the same key. A playlist receipt MAY carry `render`, the rendering parameters of the
|
||
playlist it signs (version, media sequence, discontinuity sequence, target duration,
|
||
part target, part hold-back, blocking reload, whether it ends), and `extras`, the
|
||
per-segment lines that rendering adds (discontinuities, program date-times, parts,
|
||
layer lines, a preload hint), rendered as Appendix G states. When both are absent, the
|
||
playlist is rendered as before. `manifest_hash` covers the rendered bytes, and the
|
||
signature also covers a digest of `render` and `extras` themselves (Appendix H), so a
|
||
rendering has one form. A public playlist is sealed with the entitlement `public`, no
|
||
geography, blackout or ad pod, and its media sequence as the window, under the grant
|
||
its publisher holds (`grant`: a grant of no ceilings when it holds none).
|
||
|
||
This revision signs media playlists only. A receipt binds neither the multivariant
|
||
playlist that names the media playlist nor a `wai.mpd`, and nothing binds
|
||
`class.content_id` to the presentation: it is the publisher's label, signed as given,
|
||
so a client learns from it only what the signer chose to write. A client that needs the
|
||
multivariant playlist's variants, or the content's identity, authenticated holds them
|
||
from elsewhere.
|
||
|
||
A client checks a playlist against a receipt file in this order:
|
||
|
||
1. the file reads as Appendix H's form (`playlist_receipt_malformed`);
|
||
2. its name is `<manifest_hash>.json` (`receipt_name_mismatch`);
|
||
3. the playlist's BLAKE3 is `manifest_hash`, and rendering the receipt's segments,
|
||
`render` and `extras` gives the playlist's bytes (`playlist_receipt_mismatch`);
|
||
4. the receipt verifies (`playlist_receipt_invalid`): its signature, and every value it
|
||
recomputes (Appendix H).
|
||
|
||
A client of a live playlist holds a chain: its head, the latest receipt that verified
|
||
and chained, and what the receipts in it bound. The first receipt it verifies is the
|
||
head. A later revision's receipt that is the head itself (a reload with no new
|
||
revision) leaves the chain as it is. Any other becomes the head only when it chains to
|
||
the head: it is under the head's key (`playlist_receipt_unchained`), and following
|
||
`parent_receipt_hash` from it reaches the head's receipt hash. Each parent on the way is
|
||
fetched from `mr/<parent_receipt_hash>.json` and checked in order: it is served
|
||
(`playlist_receipt_parent_missing`); it reads as Appendix H's form
|
||
(`playlist_receipt_malformed`); its receipt hash is the link it was fetched by
|
||
(`receipt_name_mismatch`); it verifies (`playlist_receipt_invalid`); and it is under the
|
||
same key (`playlist_receipt_unchained`). A walk that reaches a receipt with no parent
|
||
before the head does not chain (`playlist_receipt_unchained`), so a revision older than
|
||
the head never does. A client fetches at most 64 parents for one revision, and MAY hold
|
||
a lower bound. When the head is further back than that, the client re-anchors the chain
|
||
at the revision in hand if that revision is ahead of the head (its media sequence number
|
||
after its last complete segment is greater than the head's): the revision becomes the
|
||
head (`reanchored`), the chain keeps what it bound, and the revisions between the old
|
||
head and the new one are not signed: no part they alone list is held at **signed**. A
|
||
revision past the bound and not ahead of the head is refused
|
||
(`playlist_receipt_walk_exceeded`), so an older or stale revision never moves the chain
|
||
back; the head fetched again, however often, changes nothing. Then the walked receipts,
|
||
oldest first, and the new head bind what they list (§11.5), a re-anchored head alone; one
|
||
that lists another cid for a URL or a media sequence number the chain bound, or a gap
|
||
where it bound an object, is refused (`playlist_listing_rebound`). A refused revision
|
||
leaves the chain as it was.
|
||
|
||
## 9. Cache rules
|
||
|
||
An HTTP cache that stores and serves objects under this extension is a transparent
|
||
byte store, not a relay under [jwp-receipts](jwp-receipts.md) §4.1 (see §4.5 there).
|
||
It is bound by §4.3 there, and it MUST:
|
||
|
||
1. store and serve an object byte for byte, and not transform it (RFC 9111 §5.2.2.6)
|
||
or apply a content coding to it;
|
||
2. answer a single byte-range request from a stored complete object with `206` and the
|
||
exact bytes (RFC 9110 §14), and with `416` and `Content-Range: bytes */<len>` for
|
||
an unsatisfiable range. On a miss it MAY forward the range, or fetch the whole object
|
||
and answer from it;
|
||
3. keep `ETag`, `Content-Type`, `Cache-Control` and, on a `200`, `Content-Length` as the
|
||
origin sent them;
|
||
4. not vary a stored object by any request header;
|
||
5. evaluate `If-Range` with a strong comparison (RFC 9110 §13.1.5, §8.8.3.2);
|
||
6. store head proofs, receipt files, playlist receipts and parameter sets under the same
|
||
rules, and revalidate receipt indexes on every use and the other §8.4 resources per
|
||
their `Cache-Control`;
|
||
7. where a configuration cannot meet 1–6 for a request, pass the request through
|
||
uncached and relay the origin's bytes unchanged.
|
||
|
||
A cache SHOULD NOT revalidate an object while it is fresh (RFC 8246 §2). A node that
|
||
serves other bytes than an object's — recompressed, re-packed or split — derives a new
|
||
object: it publishes the new bytes under their own cid, with its own receipts and a
|
||
soft-binding claim to the ancestor ([jwp-receipts](jwp-receipts.md) §2.6.2), listed in
|
||
the new object's index, and MUST NOT serve them under the original name or an alias of
|
||
it. Over HTTP an edge selects only by serving such derived objects; a client selects by
|
||
range.
|
||
|
||
## 10. Delivery order, deadlines and refinements
|
||
|
||
### 10.1 Priority
|
||
|
||
A base and each refinement are separate requests. A client MUST NOT let a request whose
|
||
bytes it needs for a base wait on a refinement's bytes, and a cache MUST NOT hold a
|
||
base response for a refinement request. A client presents a base as soon as it has
|
||
verified it (§7), and applies each refinement when that refinement arrives and
|
||
verifies; it never holds a verified base back for its refinements. A base, once
|
||
verified, is decided: no later response changes its bytes or its outcome. Of a
|
||
response to a refinement request, a client keeps only the bytes of the range it
|
||
requested, even from a `200` that carries the whole object (§7.1), so a refinement's
|
||
response never reaches its base's bytes, before or after the base verified. A client SHOULD signal urgency 2 for a base
|
||
and urgency 5 with incremental for a refinement (RFC 9218 §4.1, §4.2): by the
|
||
`Priority` header (§5 there), or in a browser by the fetch `priority` option. A
|
||
deadline never applies to a base: a base request that is never answered ends the fetch
|
||
(`base_unanswered`).
|
||
|
||
### 10.2 Deadlines
|
||
|
||
A client presenting live sets a deadline on every refinement request: the latest time
|
||
at which the refinement, once received and verified, can still be applied, which for a
|
||
layer is its `lead_ms` before the presentation time of the first unit it covers
|
||
([staged-delivery](staged-delivery.md) §8). When the deadline passes, the client
|
||
abandons the request and presents what it has. This is the HTTP counterpart of the
|
||
delivery timeout after which a MoQ relay drops an object, which MoQ runs at each hop
|
||
([moq-streaming-format](moq-streaming-format.md) §4.1); on either binding a
|
||
refinement never stalls its base, and a layer that arrives late is not applied to the
|
||
units that are already due (`deadline-missed`, staged-delivery §8).
|
||
|
||
A deadline runs on the client's clock from the moment the base verified, the moment
|
||
§10.1 presents it: the time the response that completed the base was received. A
|
||
refinement that has not arrived and verified within it is abandoned, and the base,
|
||
already presented, is unaffected. What the clock read before the base verified never
|
||
moves that start. A clock that steps back has not passed a deadline until it again
|
||
reaches it, and a deadline past the clock's range is at its end. The reference
|
||
client's session reads no clock of its own: every response it is given carries the
|
||
caller's time, and the caller tells it the time between responses.
|
||
|
||
### 10.3 Enhancement companions and refinement components
|
||
|
||
A companion whose registered role is `enhancement` (for example `wai.video.lcevc`,
|
||
[SPEC](../SPEC.md) §5) and a component with `role: "refinement"` are refinements inside
|
||
the object: each range is requested in its own request, at refinement priority, after
|
||
or beside the base component's. A client that abandons an enhancement companion
|
||
presents the base, as [SPEC](../SPEC.md) §7.1 lets a sink that does not apply a
|
||
companion present the rendition; one that abandons a refinement component presents the
|
||
stage it has ([staged-delivery](staged-delivery.md) §7). A refinement that states no
|
||
`sha256`, or does not match it, is dropped (§7 steps 5 and 6), as is every refinement
|
||
of an envelope whose components do not all conform (§7 step 4), and its base is
|
||
unaffected.
|
||
|
||
### 10.4 Layer objects
|
||
|
||
A layer object is a `WAI1` whose capability is refinement-only ([SPEC](../SPEC.md) §5,
|
||
[staged-delivery](staged-delivery.md) §3). Whether an object is one is decided by its
|
||
capability alone: a `WAI1` whose capability is a payload is an ordinary object even
|
||
when it carries `refines` (which staged delivery §3 forbids its producer to write), so
|
||
a client dispatches it under SPEC §4 and does not read `refines`. A client never
|
||
dispatches a layer object under SPEC §4: it
|
||
fetches it whole, at refinement urgency (§10.1) and under a deadline (§10.2), verifies
|
||
it against its cid, and hands it to [staged-delivery](staged-delivery.md) §4, which
|
||
decides what it does to its base. A client that finds one by its head (§7) does the
|
||
same with the rest of it. Against a declared union (§4.2), a layer is declared only in
|
||
the `alt` of its own base's capability. An object a playlist lists as a layer that is
|
||
not one, a payload carrying `refines` among them, is refused (`not_a_layer`); a layer's
|
||
request abandoned at its deadline, or whose response
|
||
§7.1 refuses, leaves the layer not delivered (§10.5).
|
||
|
||
A WAI media playlist lists the layer objects of a segment or part after its `EXTINF`
|
||
or `EXT-X-PART` line, in the order a sink applies them: directly after it, before the
|
||
segment's URI, with only other `#WAI-LAYER` lines (and blank lines) between, and `N`
|
||
increasing from line to line (`playlist_malformed` otherwise):
|
||
|
||
```
|
||
#WAI-LAYER:N=<1..255>,URI="<cid>.wai#n=…"
|
||
```
|
||
|
||
Its attributes are an HLS attribute list (RFC 8216 §4.2): `N` a decimal integer from 1
|
||
to 255, `URI` a quoted string, neither named twice; an attribute this revision does not
|
||
define is ignored. A line that breaks these is `layer_tag_malformed`.
|
||
|
||
`N` is a locator: it lets a client choose the depth it fetches before it fetches any
|
||
layer. The layer an envelope brings its units to is its `refines.layer`, inside the
|
||
envelope and so inside its content hash; a sink reads it from there and ignores `N`, as
|
||
[staged-delivery](staged-delivery.md) §13 reads a MoQ layer header. The base is layer
|
||
0, the segment or part URI. A client fetches to the depth its policy chooses, each
|
||
layer under §10.1 and §10.2.
|
||
|
||
### 10.5 Objects not delivered
|
||
|
||
A refinement a client abandoned is not delivered to it. Its receipts stay listed in
|
||
the index; the client neither presents it nor reports its delivery, and the group
|
||
receipt stays valid for the objects it did receive ([jwp-receipts](jwp-receipts.md)
|
||
§4.4). A client that checks the abandoned refinement's object receipt from the index
|
||
without its bytes finds it `inclusion-verified` (jwp-receipts §3, *Verified by
|
||
inclusion*): signed and not delivered, never applied (staged-delivery §10.1). A `stage`
|
||
claim the client signs lists it as `dropped` with that evidence, or as `late` if it
|
||
arrived after its units were due (staged-delivery §10.2).
|
||
|
||
## 11. HLS and DASH adjacency
|
||
|
||
### 11.1 Mode N (native objects) — the default
|
||
|
||
- The classical multivariant playlist and MPD are unchanged. A publisher MAY add one
|
||
line to the multivariant playlist:
|
||
`#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="<URI of wai.m3u8>"`,
|
||
resolved against the playlist's URI. No other line changes.
|
||
- `wai.m3u8` uses HLS playlist syntax. Its media segments are WAI objects, which are
|
||
not a format of RFC 8216 §3.1, so it is not an HLS presentation and MUST NOT be
|
||
offered to a client that does not implement this extension. Each `EXT-X-STREAM-INF`
|
||
carries `CODECS` (the union, §4.2), `X-WAI-FLOOR` (the floor), and `BANDWIDTH` and
|
||
`AVERAGE-BANDWIDTH` computed from the objects' `envelope_len` over their durations,
|
||
never stated by hand. A segment's bytes are the `envelope_len` of its object and of the
|
||
layer objects listed with it; a segment marked `EXT-X-GAP` counts nothing, and a live
|
||
variant with no segment yet is counted by its parts. With each duration in seconds as
|
||
exactly as the packager holds it, `BANDWIDTH` is the largest `⌈8·bytes/duration⌉` of
|
||
any segment, which is never below RFC 8216's peak over a run of segments, and
|
||
`AVERAGE-BANDWIDTH` is `⌈8·Σbytes/Σduration⌉` (from a playlist's milliseconds,
|
||
`⌈8000·bytes/duration_ms⌉`). Nothing in `wai.m3u8` needs `EXT-X-VERSION`; a publisher
|
||
MAY write one, `EXT-X-INDEPENDENT-SEGMENTS` when every segment it lists decodes
|
||
without the one before it, and further `EXT-X-SESSION-DATA` lines of its own.
|
||
- A WAI media playlist takes the classical one's version, target duration, playlist
|
||
type and `EXT-X-INDEPENDENT-SEGMENTS`, and never its `EXT-X-MAP`: a WAI object has no
|
||
initialization section. A segment a publisher does not offer as a WAI object at that
|
||
point (a class that takes it from the classical presentation) is marked `EXT-X-GAP`,
|
||
and a sink takes the classical segment there.
|
||
- `wai.mpd` is a separate MPD with one `AdaptationSet` per WAI stream,
|
||
`mimeType="application/wai"`, `@codecs` (the union), an
|
||
`EssentialProperty schemeIdUri="tag:transaction.science,2026:wai/http-delivery"`
|
||
so a client that does not know it ignores the set, and a
|
||
`SupplementalProperty schemeIdUri="tag:transaction.science,2026:wai/floor"` whose
|
||
`value` is the floor. The reference `wai.mpd` is static: one `Period` with the
|
||
classical one's `@id` (none when it has none) and `@start` (in seconds, without a
|
||
fraction when whole and with three decimals otherwise), `mediaPresentationDuration`
|
||
the timeline's length in seconds with three decimals, `minBufferTime` its longest
|
||
segment rounded up to whole seconds, and in each `Representation` a `SegmentList` with
|
||
the classical timeline's `@timescale` whose `SegmentTimeline` has one `S` per segment
|
||
it offers, with the classical `t` and `d`, and one `SegmentURL` per such segment naming
|
||
its object. A segment the WAI media playlist marks `EXT-X-GAP` is left out, `S` and
|
||
`SegmentURL` both: a `Representation` whose timeline leaves out a segment offers none
|
||
there. A layer's `Representation` names the one below it by `@dependencyId`. The
|
||
classical timeline is the classical MPD's, read as Appendix I states: by default its
|
||
first `Period` and that period's first `AdaptationSet` with a timeline. Beside a
|
||
presentation with no classical MPD, it is the classical HLS media playlist's at
|
||
timescale 1000, `t` from 0, in a `Period` starting at 0. Attribute values are escaped,
|
||
tab, LF and CR as character references, and a value holding a character that is not
|
||
an XML `Char` is not written (`playlist_malformed`).
|
||
- The published classical multivariant playlist is its original, byte for byte, or the
|
||
original with the session-data line (its own line end, LF or CR LF) inserted at one
|
||
place: after the first line and the `EXT-X-VERSION` and `EXT-X-INDEPENDENT-SEGMENTS`
|
||
lines that directly follow it, before any other line. A packager MUST NOT publish one
|
||
that differs otherwise (`classical_changed`): the line anywhere else, between an
|
||
`EXT-X-STREAM-INF` and its URI among them, is refused, as is an original whose line
|
||
before that place does not end.
|
||
|
||
### 11.2 Alignment
|
||
|
||
A WAI media playlist is **adjacent** to a classical media playlist when, segment for
|
||
segment, they have the same `EXT-X-MEDIA-SEQUENCE`, `EXT-X-DISCONTINUITY-SEQUENCE`,
|
||
`EXTINF` durations to the millisecond, discontinuities and `EXT-X-PROGRAM-DATE-TIME`
|
||
values, and, where both carry parts, the same `EXT-X-PART-INF:PART-TARGET` and the same
|
||
part durations to the millisecond in each segment. In DASH, both read as Appendix I
|
||
states: each WAI `Period` has a classical `Period` with the same `@id` and `@start`
|
||
(compared in milliseconds), and has an `AdaptationSet`; each WAI `AdaptationSet` has a
|
||
timeline; and each of its timelines is not empty, has the `@timescale` of a timeline of
|
||
that classical period, and lists only that timeline's segments, each with its `t` and
|
||
`d`, in order (a segment left out is a gap). The WAI
|
||
segment (or part) with a sequence number covers exactly the presentation interval of the
|
||
classical one, so a sink can move between them at any boundary. A packager MUST NOT
|
||
publish a WAI playlist that breaks this (`adjacency_misaligned`). `#WAI-LAYER` lines
|
||
do not affect alignment, nor does `EXT-X-GAP`: a gap segment keeps its interval.
|
||
|
||
A duration is compared in milliseconds: its decimal value, read exactly, rounded to the
|
||
nearest millisecond with a half rounded up (so `4.00008` and `4.000` are both 4000). A
|
||
missing `EXT-X-MEDIA-SEQUENCE` or `EXT-X-DISCONTINUITY-SEQUENCE` is 0, and either after a
|
||
segment is malformed. Part durations are compared in each segment both playlists list
|
||
parts for, and in the open segments (§11.4) when both have one; one side listing parts
|
||
the other has stopped listing is aligned. A playlist that does not read as a media
|
||
playlist — a delta update (`EXT-X-SKIP`) and a multivariant playlist among them — is not
|
||
compared (`playlist_malformed`).
|
||
|
||
### 11.3 Live at segment granularity (Live-S)
|
||
|
||
Each segment is one object, listed as `<cid>.wai#n=…&h=…` once it is complete. A client
|
||
presents an object only after its bytes verify against the listed cid. The latency
|
||
from the end of a segment's media interval to presentation is at least
|
||
|
||
`S_floor = e + p + r_pl + r_obj + v`
|
||
|
||
where `e` is encode and packaging time, `p` the time to publish and list, `r_pl` the
|
||
wait for a playlist that lists the object (with blocking reload, one request time),
|
||
`r_obj` the object request time (one round trip and transfer), and `v` the verification
|
||
time.
|
||
|
||
### 11.4 Low-latency live (Live-P)
|
||
|
||
Each part is its own object, served under an alias, and listed with its cid:
|
||
|
||
```
|
||
#EXT-X-PART-INF:PART-TARGET=0.333
|
||
#EXT-X-PART:DURATION=0.333,INDEPENDENT=YES,URI="live/720/1200.3.wai#cid=<hex>&n=…"
|
||
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="live/720/1200.4.wai"
|
||
```
|
||
|
||
- The alias of a part names those bytes for its lifetime (§5.2). A preload hint names
|
||
the alias before the part exists; the origin holds the request until it does. An
|
||
origin answers at once with 400 a blocking reload whose `_HLS_msn` is more than two
|
||
past the last segment of its current playlist, or that has `_HLS_part` without
|
||
`_HLS_msn`, as the HLS second edition requires, rather than holding it.
|
||
- A client presents a part only after its bytes hash to the cid that a later playlist
|
||
revision lists for that alias. Issued together, the hint request and a blocking
|
||
reload (`_HLS_msn`, `_HLS_part`) complete when the part is complete, so the floor is
|
||
|
||
`P_floor = e + max(r_obj, r_pl) + v`
|
||
|
||
against `e + r_obj` for a part presented without verification: the added latency is
|
||
`max(0, r_pl − r_obj) + v`, with `v` the BLAKE3 time of one part.
|
||
- The segment line names a separate segment object, published when the segment closes;
|
||
it serves every client behind the live edge. Parts stop being listed as the HLS
|
||
specification permits, and an origin MAY then delete them.
|
||
- An `INDEPENDENT=YES` part is one whose envelope decodes without another object.
|
||
- A WAI Live-P playlist takes the classical playlist's target duration, part target and
|
||
blocking reload, and its part hold-back (three part targets when it states none). A
|
||
segment's parts precede its `EXTINF`, its discontinuity and date-time precede its
|
||
first part, the open segment's parts follow the last segment's URI, and the preload
|
||
hint is the last line (Appendix G), as the HLS second edition places them. A hint
|
||
with no open part yet follows the last segment.
|
||
|
||
### 11.5 Live to on demand
|
||
|
||
Every object is immutable from the moment it is listed, so a live playlist becomes
|
||
on-demand by appending `EXT-X-ENDLIST`; no object is rewritten.
|
||
|
||
A revision MUST NOT list another cid for a URL an earlier revision listed (an alias, or
|
||
any part, segment or layer URI), nor another object at a media sequence number an
|
||
earlier revision listed an object at, nor a gap there: a listed segment is not
|
||
withdrawn (a gap segment lists no object at its number). A URL is the line's URI
|
||
resolved against the playlist's URL (RFC 3986 §5.2) and normalized (§6.2.2: the scheme
|
||
and host in lower case, percent-encodings' hexadecimal digits in upper case, unreserved
|
||
characters decoded, dot segments removed), without its fragment, so two spellings of
|
||
one URL are one binding. A client keeps both bindings from the revisions it accepts, a
|
||
URL's cid being the first one listed with it (§5.2), and refuses a revision that lists
|
||
another (`playlist_listing_rebound`), taking none of its listings. A chain of receipts
|
||
binds the same way over the receipts in it (§8.6).
|
||
|
||
A client keeps a binding while one of the last 256 revisions it took lists it, and MAY
|
||
keep it longer; what it holds is then bounded by what those revisions list, however
|
||
long the playlist runs. A rebinding of a URL or a sequence number that none of the last
|
||
256 revisions listed is not detected.
|
||
|
||
### 11.6 Playlist authentication
|
||
|
||
A WAI playlist SHOULD be authenticated by its receipt (§8.6). A client that verifies it
|
||
holds the cids, durations and order its signer published. Without it, which objects a
|
||
playlist names is only as trustworthy as the connection to the origin that served it;
|
||
each object is still bound to the cid the playlist names. A client reports which held:
|
||
**listed** (bytes match a cid in a playlist fetched over HTTPS from the origin the
|
||
multivariant playlist names), **signed** (that playlist's receipt, or the object's
|
||
receipt, verified), and the confirmation level of [jwp-receipts](jwp-receipts.md) §7.4.
|
||
In Live-P a client MAY present at **listed** and verify the playlist receipt after
|
||
presenting; it MUST NOT report the part as signed until it has. A part is listed by the
|
||
first accepted revision whose playlist names its alias with a `cid`; it is signed when a
|
||
receipt in the chain the client holds (§8.6), the parents walked to it included, binds
|
||
the alias to that cid.
|
||
|
||
A WAI segment whose payload is a classical segment's media relates to the classical
|
||
package's segment map (`wai.video.package`) by sequence number: the package's
|
||
`content_hash` identifies the media and the cid identifies the envelope that carries it.
|
||
They are hashes of different bytes, and neither replaces the other.
|
||
|
||
### 11.7 Mode F (fragmented MP4) — provisional
|
||
|
||
Appendix B defines ISOBMFF sample entries that carry one WAI envelope per sample. Such
|
||
segments are fragmented MP4 (RFC 8216 §3.3), so a WAI track could be a variant of the
|
||
classical multivariant playlist. The 4CCs are not registered: until they are, Mode F
|
||
MUST NOT be published outside test material.
|
||
|
||
### 11.8 CTA-5005-B
|
||
|
||
In Mode N the classical presentation's conformance to CTA-5005-B is preserved by byte
|
||
equality of its manifests. The `EXT-X-SESSION-DATA` option and Mode F are to be checked
|
||
against CTA-5005-B's clauses before use.
|
||
|
||
## 12. Capability probe
|
||
|
||
### 12.1 The Sink Capability Descriptor
|
||
|
||
```json
|
||
{ "wai_sink": 1,
|
||
"containers": ["WAI1", "WAI2"],
|
||
"caps": { "<capability>": { "via": "wasm" | "ua" | "host",
|
||
"asked": { "<ua codec string>": true | false },
|
||
"provisional": true | false } },
|
||
"held_sets": { "<capability>": [ "<pin sha256>" ] } }
|
||
```
|
||
|
||
- `wasm`: decoded by the sink's own compiled decoder. `ua`: by a user-agent decoder,
|
||
under the codec strings in `asked`. `host`: by a native host. A capability the sink
|
||
cannot decode is absent.
|
||
- `provisional` is true for a `ua` capability: the decoder answered for the profiles
|
||
asked, not for a given payload.
|
||
- `held_sets` lists, per capability, the digests of the parameter sets that passed
|
||
[jwp-receipts](jwp-receipts.md) §7.9 when the descriptor was built. A revoked set is
|
||
never listed.
|
||
|
||
### 12.2 Asking the user agent
|
||
|
||
A capability is `ua` only if the sink asked the user-agent decoder it will hand the
|
||
payload to, under that decoder's own name, and the answer was supported: WebCodecs
|
||
`isConfigSupported` for a video or audio elementary stream, or an image decode of a
|
||
known sample. Media Capabilities is asked only as a further question about a decoder
|
||
already found. Before decoding a payload through a `ua` capability, a sink MUST ask
|
||
again with the configuration that payload's own header gives; a refusal makes the
|
||
capability unsupported for that object, and selection continues.
|
||
|
||
### 12.3 Support
|
||
|
||
`supports(codecs, descriptor)` is §4.2's rule with "reads the container" read as "is in
|
||
`containers`" and "supports `cap`" read as "`wai.<cap>` is in `caps`".
|
||
|
||
### 12.4 Capable share (informative)
|
||
|
||
For a WAI variant V and a set C of sessions observed in a stated window:
|
||
|
||
capable_share(V, C) = |{ s ∈ C : supports(codecs(V), descriptor(s)) ∧ probe_ok(V, s) }| / |C|
|
||
|
||
`probe_ok(V, s)` holds when the session decoded V's published probe object and its
|
||
output had the published digest (an exact tier), or decoded without error (any other
|
||
tier). A report states |C|, the window, how C was sampled and the descriptor version. It
|
||
is a share of sessions, not of devices or people.
|
||
|
||
## 13. Reasons
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `object_length_mismatch` | §5.1 |
|
||
| `head_malformed` | §7 step 3 |
|
||
| `head_proof_malformed`, `head_proof_mismatch` | §6.3 |
|
||
| `http_status_unexpected`, `content_coding_present`, `content_range_mismatch`, `complete_length_mismatch` | §7.1 |
|
||
| `component_digest_absent` | §7 step 5 (a reason to fetch whole, never a skip) |
|
||
| `components_nonconforming` | §7 step 4 (a refinement dropped, never a skip) |
|
||
| `component_digest_mismatch` | §7 step 6 ([SPEC](../SPEC.md) §7.1's reason) |
|
||
| `object_digest_mismatch` | §7 step 7, or any whole fetch |
|
||
| `receipt_name_mismatch`, `receipt_not_about_object` | §8.1, §8.2, §8.6 |
|
||
| `receipt_not_served`, `receipt_index_malformed`, `receipt_index_too_large`, `receipt_too_large`, `receipt_legacy_group`, `receipt_fetch_budget_exhausted`, `receipt_fetch_bytes_exhausted` | §8.1, §8.2 |
|
||
| `context_not_held` | §8.2 (jwp-receipts §3.1 step 6) |
|
||
| `key_log_not_from_origin`, `key_log_too_large`, `grants_file_malformed` | §8.4 |
|
||
| `codecs_malformed`, `codecs_not_media`, `codecs_candidate`, `codecs_no_common_capability`, `codecs_undeclared`, `codecs_layer_unbound` | §4 |
|
||
| `base_unanswered` | §10.1 |
|
||
| `not_a_layer`, `layer_tag_malformed` | §10.4 |
|
||
| `adjacency_misaligned` | §11.2 |
|
||
| `playlist_malformed` | §10.4, §11.2, Appendix G, Appendix I |
|
||
| `classical_changed` | §11.1 |
|
||
| `playlist_receipt_malformed`, `playlist_receipt_mismatch`, `playlist_receipt_invalid`, `playlist_receipt_unchained`, `playlist_receipt_parent_missing`, `playlist_receipt_walk_exceeded` | §8.6 |
|
||
| `playlist_listing_rebound` | §8.6, §11.5 |
|
||
| `abr_state_out_of_bounds` | Appendix F |
|
||
|
||
Selection, pin, receipt, key and module reasons are those of [SPEC](../SPEC.md) §3.1
|
||
and §7.1 and [jwp-receipts](jwp-receipts.md) §3, §7.7 and §7.9, and the layer reasons
|
||
those of [staged-delivery](staged-delivery.md) §4.2, unchanged. An object at which
|
||
selection picks nothing is inert at the sink, with each entry's reason, as SPEC §7.1
|
||
step 5 states.
|
||
|
||
## 14. Conformance
|
||
|
||
- [`http-conformance/`](../http-conformance/) holds objects, head proofs and cases for
|
||
§4–§7 and §10 and their reasons, with positive cases. `wai_http_vectors verify`
|
||
(also `wai http vectors verify`) and the independent `http_verify.py`, which shares no
|
||
code with `wai-rs`, report the same outcome for every case, and for every case of a
|
||
seeded differential over inputs outside the corpus (`wai_http_vectors fuzz`): every
|
||
`Content-Range`, `codecs` value, `#WAI-LAYER` line, manifest, member type, encryption,
|
||
`refines` and layer sequence it generates. For §8.6, §10.4, §11 and Appendix F the
|
||
corpus holds WAI playlists rendered from their inputs beside their classical ones,
|
||
their receipts (a live playlist's chained, and walked across revisions a client
|
||
skipped or fetched again), a published classical master and a
|
||
playable classical presentation for players, and the policies' choices; the
|
||
differential adds playlists, adjacent pairs, published masters, renderings, receipt
|
||
files, adaptation states and MPD pairs. For §8.1–§8.5 it holds group receipts and
|
||
claims served as receipt files, an index beside each object that has receipts, an
|
||
origin's key log, key status and grants file, and a parameter set served by its
|
||
digest, with cases for receipt files read under their names, indexes checked against
|
||
the whole object, a head proof's cid and nothing, parents walked and not recovered,
|
||
walks at and past the bound, legacy groups, files and indexes at and past their caps,
|
||
each name fetched once within budgets of files and bytes, claims by subject, parent
|
||
and the receipts §3.1 step 6 checks them against for every class (declarations read
|
||
by their names first), ancestors by recovery under each publisher key held,
|
||
anchoring from the origin only, logs through caches,
|
||
grants files and sets held by their pins and §7.9; the differential adds receipt files,
|
||
indexes, grants files and key logs each changed one way. `http_verify.py` checks these
|
||
cases with the `cryptography` package, through the corpora's own second verifiers of
|
||
[jwp-receipts](jwp-receipts.md) §3, §3.1 and §7.
|
||
- [`cache-conformance/`](../cache-conformance/) checks §9 against HTTP caches and
|
||
reports, per cache label and version, the configuration that meets it. Products are
|
||
not named in published reports.
|
||
|
||
## 15. Security and privacy considerations
|
||
|
||
- **What a range fetch establishes.** A head proof binds the head to the cid; the head's
|
||
`sha256` values bind each component to the head; a receipt binds the cid to a signer;
|
||
a playlist receipt binds the list of cids to a signer. Each link is a hash or a
|
||
signature.
|
||
- **Changed bytes.** A cache that changes bytes changes a hash and is refused. A content
|
||
coding is refused before any byte is used (§7.1), so no decompression runs on the
|
||
range path.
|
||
- **Lengths.** `Content-Range`, `Content-Length`, `n` and `h` are untrusted; every
|
||
length that matters is checked against the head proof.
|
||
- **Playlists.** An unsigned playlist can name other objects; §11.6 says what a client
|
||
then holds.
|
||
- **Privacy.** The ranges a sink fetches reveal which rendition it picked, and so part
|
||
of its capabilities, to every cache and the origin. A sink that does not want this
|
||
fetches whole objects.
|
||
- **Unsigned files.** Receipt indexes, grants files and aliases are locators, never
|
||
evidence. A receipt file is named by its link hash and a parameter set by its digest,
|
||
so a cache or mirror that serves other bytes under the name is refused; a group whose
|
||
name does not fix its bytes is not read (§8.1); a key log anchors only from its origin
|
||
(§8.4).
|
||
- **What a server can make a client do.** Every receipt file, index and item count is
|
||
capped, each name is fetched once, and the files and bytes one index costs are bounded
|
||
(§8.1, §8.2), so an index cannot make a client fetch or hold without limit.
|
||
|
||
## Appendix A — CDNI configuration
|
||
|
||
Path patterns `/objects/*.wai`, `*.wai.hp`, `rcpt/*` and `mr/*` take the RFC 8006
|
||
metadata `MI.Cache` with `include-query-strings: []`, and `MI.SourceMetadata`; a
|
||
receipt index (`*.wai.receipts`) takes the cache rule with no storage beyond
|
||
revalidation. RFC 8006 has no metadata for range handling, transformation or a time-to-
|
||
live override: a configuration interface that has them states, for these paths, the
|
||
behaviour §9 requires.
|
||
|
||
## Appendix B — ISOBMFF sample entries (provisional; 4CCs not registered)
|
||
|
||
```
|
||
aligned(8) class WAIConfigurationBox extends FullBox('waiC', 0, 0) {
|
||
unsigned int(8) wai_major; // 1
|
||
unsigned int(8) max_minor; // highest manifest MINOR in the track
|
||
unsigned int(16) element_len;
|
||
unsigned int(8) element[element_len]; // the track's union element, UTF-8
|
||
unsigned int(16) floor_len;
|
||
unsigned int(8) floor[floor_len]; // the track's floor element, UTF-8
|
||
}
|
||
class WAIVisualSampleEntry(type) extends VisualSampleEntry(type) { WAIConfigurationBox config; } // 'vide'
|
||
class WAIAudioSampleEntry(type) extends AudioSampleEntry(type) { WAIConfigurationBox config; } // 'soun'
|
||
class WAIMetaSampleEntry(type) extends MetaDataSampleEntry(type) { WAIConfigurationBox config; } // 'meta'
|
||
// type = 'wai1' when every sample is a WAI1 envelope, else 'wai2'
|
||
```
|
||
|
||
- Each sample is one envelope, byte-identical, so its cid is unchanged.
|
||
- A sample is a sync sample when it decodes alone.
|
||
- Composition time equals decode time.
|
||
- There is no `encv`/`enca` wrapping; confidentiality is [SPEC](../SPEC.md) §6.
|
||
|
||
## Appendix C — Mapping to moq-streaming-format
|
||
|
||
| MoQ ([moq-streaming-format](moq-streaming-format.md)) | HTTP (this extension) |
|
||
|---|---|
|
||
| object = one envelope (§3 there) | object `<cid>.wai` (§5.1) |
|
||
| `packaging: "wai"` | media type `application/wai` |
|
||
| catalog `wai_capabilities`, exhaustive (§4 there) | `codecs` union (§4.2); `codecs_undeclared` is the same protocol error |
|
||
| (no floor) | `X-WAI-FLOOR` / DASH `SupplementalProperty` |
|
||
| `WAI_RECEIPT` property (§5 there) | index item `property` (§8.2) |
|
||
| group and object receipts, on each track's receipts track (§4.1 there) | `rcpt/<link hash>.json`; a group per segment (on demand) or per segment with its parts (Live-P) |
|
||
| a layer object on its own track | a layer object listed by `#WAI-LAYER` (§10.4); its layer is its `refines.layer` either way |
|
||
| a subscriber's layer cap: the layer tracks it subscribes to (§4.1 there) | the client's depth policy; nothing is sent to the edge |
|
||
| a relay forwards the tracks asked for, drops an object only at a delivery timeout or at its discretion, and forwards its receipts (§4.1, §5 there) | the client does not request them; caches never drop; receipts stay listed (§10.5) |
|
||
| the delivery timeout after which a relay drops an object, run at each hop (§4.1 there) | the client's refinement deadline, run at the client (§10.2) |
|
||
| publisher priority | RFC 9218 urgency (§10.1) |
|
||
|
||
## Appendix D — Worked example
|
||
|
||
**The envelope.** A `WAI2` with a 560-byte manifest:
|
||
|
||
```
|
||
{"wai":"1.1","media":"video","intent":"replicate","renditions":[{"capability":"wai.video.av1","kind":"av1","id":"low","sha256":"f541874101876255b4baf3a739778d04cb9cba25ffa38b30bc1fb8b0701f2a45"},{"capability":"wai.video.av1","kind":"av1","id":"high","sha256":"b76bf31be170d9df8eafbd020941b5fd4b7ab017af6a1e5c1c03e909aa8f9fb7"},{"capability":"wai.meta.hdr10plus","kind":"descriptor","id":"hdr","sha256":"74739b7242c81aa5ec2a1601fd04a5f81bed0302970e6a51f3a7dc9e92ea8520","role":"companion","companion_of":"wai.video.av1","companion_of_id":"high"}],"target":null}
|
||
```
|
||
|
||
Payloads, synthetic (they show the layout, and decode to nothing):
|
||
- `low`: 3,000 B, byte i = (7i+3) mod 256;
|
||
- `high`: 50,000 B, byte i = (13i+5) mod 256;
|
||
- `hdr`: 200 B, byte i = (31i+1) mod 256.
|
||
|
||
**Layout.**
|
||
- `head_len` = 594. Table: `0300 00000000 b80b0000 b80b0000 50c30000 08cf0000 c8000000`.
|
||
- `envelope_len` = 53,794.
|
||
- cid = `eecd29409ee510d39ef1b17653a3ae9a2a176c1d76c02dde3319f014a3ef3228`.
|
||
- `covered_len` = 1009, `n_cv` = 6, so the proof file is 245 B.
|
||
- Component ranges: `low` [594, 3593], `high` [3594, 53593], `hdr` [53594, 53793].
|
||
- `codecs` = `wai2.video.av1+meta.hdr10plus`, and the floor is the same.
|
||
|
||
The plans below assume the object is listed with `#h=1009` (and `n`), so the first
|
||
request ends at `covered_len`. Without `h`, the first request is 16,369 bytes.
|
||
|
||
**Plan 1: the sink picks `low`** (the table's order, or a policy under low throughput).
|
||
- Requests: `bytes=0-1008` (1,009 B) and the proof (245 B), then `bytes=1009-3593`
|
||
(2,585 B).
|
||
- Body bytes: 3,839, with `envelope_len` 53,794.
|
||
- Without `h`: the first request, `bytes=0-16368`, holds `low` whole; 2 requests and
|
||
16,614 body bytes.
|
||
|
||
**Plan 2: a policy reorders to `high`, which binds `hdr`.**
|
||
- [3594, 53793] is 50,200 B of base-class bytes: 951 ‰ of the 52,785 B after
|
||
`covered_len`, which is at least 900 ‰.
|
||
- So the client requests `bytes=1009-53793` and verifies the whole envelope.
|
||
- Body bytes: 53,794 and the proof's 245.
|
||
- Without `h`: the first request holds [0, 16368], and the base-class bytes still
|
||
needed, [16369, 53793], are 37,425 B, 709 ‰ of the rest; so a range fetch of 3
|
||
requests and 54,039 body bytes.
|
||
|
||
## Appendix E — References and their verification status
|
||
|
||
| reference | status |
|
||
|---|---|
|
||
| RFC 9110 §7.7, §8.4, §8.8.3, §13.1.5, §14.1–§14.6, §15.3.7, §15.5.17 | read |
|
||
| RFC 9111 §5.2.2.6 · RFC 8246 §2 · RFC 6381 §3.2 · RFC 6838 §3.1, §3.2, §5.6 | read |
|
||
| RFC 9530 §2, §3 · RFC 9218 §4.1, §4.2, §5 | read |
|
||
| RFC 8216 §3.1, §3.3, §4.3.4.2, §4.3.4.4 · the HLS second-edition draft (its date only) | read |
|
||
| RFC 8006 `MI.*` objects | read |
|
||
| The HLS second-edition draft (draft 22): `EXT-X-PART` (its placement before the parent segment's URI, and the media segment tags before its first part), `EXT-X-PART-INF`, `EXT-X-SERVER-CONTROL` (`PART-HOLD-BACK` required with parts, at least twice the part target), `EXT-X-PRELOAD-HINT` | read |
|
||
| The HLS second-edition draft's blocking-reload clauses and when parts stop being listed | not re-read here: §11.4 is to be checked against them |
|
||
| CTA-5005-B | title and date only |
|
||
| The BLAKE3 specification | the tree rule is checked by the reference implementation against whole-input hashing |
|
||
| RFC 8615, RFC 8785, RFC 4151, RFC 5234, ISO/IEC 14496-12 and -15, ISO/IEC 23000-19, ISO/IEC 23009-1, the Fetch Standard (CORS-safelisted `Range`, `priority`), WebCodecs, Media Capabilities | not re-read here |
|
||
|
||
Every row not read is read before this extension leaves Draft.
|
||
|
||
## Appendix F — Reference adaptation policies (informative)
|
||
|
||
A client MAY order a `WAI2`'s entries by deployer policy ([SPEC](../SPEC.md) §7.1
|
||
step 2) and choose how many layers it fetches (§10.4). The reference client's policies
|
||
choose among options of increasing size from measured delivery, in integers only, so
|
||
every implementation chooses the same; the corpus pins their choices.
|
||
|
||
**Inputs.** A state holds the buffer `b` (ms), samples `(at, bytes, ms)` oldest first,
|
||
the last choice, each option's bytes `S_k`, each option's energy figure where a receipt
|
||
labels it with its acquisition class ([energy-measurement](energy-measurement.md) §2;
|
||
none otherwise), a joule budget if held, and the media duration `D` one option covers
|
||
(ms). Divisions round down unless stated; `⌈x/y⌉` rounds up.
|
||
|
||
**Bounds.** Each value lies within its field's type: `b`, `D`, and the policies' `R`,
|
||
`C`, `v` and `M` below 2^32; `n` and `h` below 2^8; `p` below 2^16; each sample's
|
||
`at`, `bytes` and `ms`, the last choice, each `S_k`, each figure and the budget below
|
||
2^64. A state holds at most 256 options (and at most 256 figures), and a layer depth is
|
||
chosen over at most 256 objects, the bytes and the figure through each depth below 2^64.
|
||
A state or a policy beyond these is refused (`abr_state_out_of_bounds`). Within them
|
||
every quantity below is computed exactly, never saturated: a rate or a throughput
|
||
reaches 2^85, the look-ahead's stall times `max_k u_k + 65536` reaches 2^103, and the
|
||
utility-over-buffer cross-multiplication reaches 2^141, so an implementation in fixed
|
||
widths compares those products in 192 bits.
|
||
|
||
- The throughput of the last `n` samples (`n` 0 taken as 1): `⌊8000·Σbytes / Σmax(ms, 1)⌋`
|
||
b/s, 0 with no sample.
|
||
- An option's rate: `⌈8000·S_k / max(D, 1)⌉` b/s. "The highest option under `r`" is the
|
||
largest index whose rate is at most `r`, else 0.
|
||
- `log2_q16(a, b)`, `log2(a/b)` in units of 1/65536: 0 when `a ≤ b` (`b` taken as at
|
||
least 1); else with `x = ⌊65536·a/b⌋`, `e = ⌊log2 x⌋ − 16`, `f = (x >> e) − 65536`,
|
||
`i = f >> 12`, `r = f & 4095` and `T = [0, 5732, 11136, 16248, 21098, 25711, 30109,
|
||
34312, 38336, 42196, 45904, 49472, 52911, 56229, 59434, 62534, 65536]`
|
||
(`T[i] = round(65536·log2(1 + i/16))`), it is `65536·e + T[i] + ⌊(T[i+1] − T[i])·r / 4096⌋`.
|
||
- An option's utility: `u_k = log2_q16(max(S_k, 1), max(S_0, 1))`.
|
||
|
||
**Policies.** Each chooses an option index (0 when there is none):
|
||
|
||
- *Throughput* `(n, p)`: the highest option under `⌊throughput(n)·p/1000⌋`.
|
||
- *Buffer* `(R, C)`: 0 when `b ≤ R`; the highest index when `C = 0` or `b ≥ R + C`; else
|
||
the highest option under `rate_0 + ⌊max(rate_top − rate_0, 0)·(b − R)/C⌋`.
|
||
- *Utility over buffer* `(v, M)`: with `q = ⌊65536·b/D'⌋`, `q_max = ⌊65536·M/D'⌋`
|
||
(`D' = max(D, 1)`), `V_n = max(q_max − 65536, 0)` and `V_d = max_k u_k + 65536`, each
|
||
option scores `N_k = ⌊V_n·v·(u_k + 65536) / (V_d·1000)⌋ − q`. If any `N_k > 0`, the
|
||
option with the largest `N_k/S'_k` among them (`S'_k = max(S_k, 1)`, compared by
|
||
cross-multiplication); else the option with the largest `N_k`. Ties go to the lower
|
||
index. This is a rule of the utility-over-buffer kind; unlike the published algorithm
|
||
of that kind, it never waits for the buffer to drain.
|
||
- *Look-ahead* `(h, p)`: with `P = ⌊throughput(5)·p/1000⌋` (0 chooses 0), `h` taken in
|
||
1 to 5 and lowered while `K^h > 65536` for `K` options, every sequence of `h` choices,
|
||
in increasing order of the sequence read as base-`K` digits, most significant first,
|
||
is played from the buffer `b`: each choice downloads in `⌈8000·S_k/P⌉` ms, stalling by
|
||
what passes the buffer, then adds `D`. Its score is `1000·Σu − 1000·Σ|u_t − u_{t−1}|`
|
||
(the first step against the last choice, when there is one below `K`) less the stall
|
||
in ms times `(max_k u_k + 65536)`. The first choice of the first best sequence is
|
||
taken.
|
||
|
||
**Budget.** A budget never raises the policy's choice. Without a budget the choice
|
||
stands (`not_set`). When no option has a labelled figure, the choice stands and the
|
||
budget is not applied (`not_applied`): an unlabelled figure is never used. Otherwise the
|
||
choice is the highest option at or below the policy's whose labelled figure is within
|
||
the budget: the policy's own (`within`) or one below it (`lowered`), so an unlabelled
|
||
option never escapes the budget. When no option at or below the policy's has a labelled
|
||
figure within it, the choice is option 0 and the budget is not met (`unmet`): a client
|
||
fetches the least it can, and reports that the budget was not held. A figure past the
|
||
options names none.
|
||
|
||
**Entry order.** Within a `WAI2`, the selectable entries whose capability the sink has
|
||
are grouped by capability in order of first appearance; a group's options are its
|
||
entries by component length (then table index), each option's figure the entry's. The
|
||
group is ordered as the choice under the budget, then the options below it from the
|
||
nearest down, then those above it from the nearest up; every other entry follows in
|
||
table order. Selection then runs unchanged on the head and on the whole envelope, so
|
||
the pick is the same on every path (§7.2).
|
||
|
||
**Layer depth.** Over a base and its layers, option `k` fetches through layer `k`: its
|
||
bytes are the `envelope_len`s through it, and its figure the sum of the labelled figures
|
||
through it (none when any is unlabelled).
|
||
|
||
## Appendix G — The rendering a playlist receipt signs
|
||
|
||
A WAI media playlist is rendered from its segments (each a duration in milliseconds, a
|
||
URI, and whether it is an ad), the parameters of §8.6 and the extras, with lines ending
|
||
in LF and every duration written as seconds with three fractional digits:
|
||
|
||
1. `#EXTM3U`; `#EXT-X-VERSION:<v>`; `#EXT-X-TARGETDURATION:<t>`, `t` the parameter or,
|
||
when it has none, the largest segment duration rounded up to whole seconds;
|
||
`#EXT-X-PART-INF:PART-TARGET=<d>` with a part target; `#EXT-X-SERVER-CONTROL:` with
|
||
`CAN-BLOCK-RELOAD=YES` and `PART-HOLD-BACK=<d>`, those that apply, in that order,
|
||
when either does; `#EXT-X-MEDIA-SEQUENCE:<n>`; `#EXT-X-DISCONTINUITY-SEQUENCE:<n>`
|
||
when it is not 0; `#EXT-X-PLAYLIST-TYPE:<VOD or EVENT>` with a type;
|
||
`#EXT-X-INDEPENDENT-SEGMENTS` when it applies; `#EXT-X-MAP:URI="<uri>"` with a map
|
||
(a classical playlist's; a WAI playlist has none).
|
||
2. Each segment: `#EXT-X-DISCONTINUITY` once, for an ad or a discontinuity;
|
||
`#EXT-X-PROGRAM-DATE-TIME:<value>`; each part as
|
||
`#EXT-X-PART:DURATION=<d>[,INDEPENDENT=YES],URI="<uri>"` followed by its layer lines;
|
||
`#EXT-X-GAP` for a gap; `#EXTINF:<d>,`; the segment's layer lines; its URI. A layer
|
||
line is `#WAI-LAYER:N=<n>,URI="<uri>"`.
|
||
3. The open segment, when there is one extra more than segments: its discontinuity,
|
||
date-time and parts, written as in step 2.
|
||
4. `#EXT-X-PRELOAD-HINT:TYPE=PART,URI="<uri>"` from the last extra; `#EXT-X-ENDLIST`
|
||
when the playlist ends.
|
||
|
||
Extras are none, one per segment, or one more. A rendering is refused
|
||
(`playlist_malformed`) when it would not read back as its inputs: a URI that is empty,
|
||
begins with `#` or holds CR or LF (or `"`, where quoted, the map's among them); a
|
||
program date-time outside
|
||
ASCII letters, digits and `:.+-`; layer numbers outside 1 to 255 or not increasing;
|
||
parts with no part target; a part target with no part hold-back of at least twice it;
|
||
a preload hint on any but the last extra, or in a playlist that ends; a gap segment
|
||
with parts or layer lines (it offers no object to divide or refine); an open segment
|
||
in a playlist that ends, with layer lines or a gap of its own, with a discontinuity or date-time
|
||
before any part, or with neither a part nor a hint; version 0.
|
||
|
||
## Appendix H — The playlist receipt file
|
||
|
||
`mr/<manifest_hash>.json` is one JSON object with these members and no other:
|
||
`kind` (`"wai.video.manifest"`), `manifest_hash`, `session_class_hash`, `class`
|
||
(`content_id`, `entitlement`, `geo`, `blackouts`, `ad_pod_id`, `window`), `segments`
|
||
(each `index`, `kind` — `content` or `ad` —, `uri`, `duration_ms`, `ad_id`),
|
||
`merkle_root`, `work_compose_ops`, `viewers_served`, `joules_micro`, the acquisition
|
||
class's members of [energy-measurement](energy-measurement.md) §2.7 when the figure is
|
||
metered, `grant` (`grant_hash`, `capability`, `joule_ceiling_micro`, `funds_ceiling`),
|
||
`parent_receipt_hash`, `signer_pubkey`, `signer_id`, `sig`, and `render` (`version`,
|
||
`media_sequence`, `discontinuity_sequence`, `target_duration_s`, `part_target_ms`,
|
||
`part_hold_back_ms`, `can_block_reload`, `end_list`, `playlist_type` — `VOD`, `EVENT` or
|
||
null —, `independent_segments`, `map_uri`) with `extras` (each `discontinuity`, `gap`,
|
||
`program_date_time`, `parts` — each `duration_ms`, `independent`,
|
||
`uri`, `layers` —, `layers` — each `n`, `uri` —, `preload_hint`), the two together or
|
||
not at all. Hashes are lowercase hexadecimal; a number is an unsigned integer of at
|
||
most 2^53 − 1 written as digits alone, and a writer refuses to seal a receipt it cannot
|
||
write so. A grant's `joule_ceiling_micro` is `null` when the grant sets no joule ceiling
|
||
(it signs as 2^64 − 1), and `funds_ceiling` `null` when it sets no funds ceiling. The
|
||
file is read as I-JSON (RFC 7493): no object holds a member name
|
||
twice, at any depth and with names compared after their escapes are read, and no
|
||
string holds a `\u` escape that leaves a surrogate unpaired; either is
|
||
`playlist_receipt_malformed`, so no two readers can read two receipts from one file.
|
||
|
||
Below, `‖` concatenates, integers are big-endian of the stated width in bytes, and a
|
||
string is its UTF-8 length in 4 bytes, then its bytes:
|
||
|
||
- a segment's leaf: BLAKE3 of `"wai:video-manifest-seg\x01"` ‖ `index` (8) ‖ 1 for
|
||
content or 2 for an ad (1) ‖ `duration_ms` (8) ‖ `uri` ‖ (1 ‖ `ad_id`, or 0);
|
||
- `merkle_root`: [jwp-receipts](jwp-receipts.md) §2.2's root over the leaves;
|
||
- `session_class_hash`: BLAKE3 of `"wai:video-manifest-class\x01"` ‖ `content_id` ‖
|
||
`entitlement` ‖ `geo` ‖ `ad_pod_id` ‖ the number of distinct `blackouts` (4) ‖ each,
|
||
in byte order ‖ `window` (8);
|
||
- the render digest: BLAKE3 of `"wai:video-manifest-render\x01"` ‖ the RFC 8785 form of
|
||
`{"render": …, "extras": […]}`;
|
||
- the signed bytes: `"wai:video-manifest-receipt\x01"` ‖ `manifest_hash` ‖
|
||
`session_class_hash` ‖ `merkle_root` ‖ `work_compose_ops` (8) ‖ `viewers_served` (8) ‖
|
||
`joules_micro` (8) ‖ the class's tag and evidence of
|
||
[energy-measurement](energy-measurement.md) §2.7, or 0 unmetered ‖ `grant_hash` ‖
|
||
`capability` ‖ `joule_ceiling_micro` (8) ‖ (1 ‖ `funds_ceiling` (8), or 0) ‖
|
||
(1 ‖ `parent_receipt_hash`, or 0) ‖ the number of segments (8) ‖ each leaf ‖
|
||
`signer_pubkey` ‖ `signer_id`, and with a rendering, 1 ‖ the render digest;
|
||
- the receipt hash: BLAKE3 of `"wai:video-manifest-receipt-id\x01"` ‖ the signed bytes ‖
|
||
`sig`.
|
||
|
||
A receipt verifies when: the leaves give `merkle_root`; `class` gives
|
||
`session_class_hash`; without `render` there are no `extras`, and with it the rendering
|
||
reads back (Appendix G) and has an RFC 8785 form; the BLAKE3 of the rendering is
|
||
`manifest_hash`; `work_compose_ops` is the number of segments and `viewers_served` is
|
||
at least 1; the segment at position `i` has `index` `i`, and `ad_id` exactly when it is
|
||
an ad; `joules_micro` and the class are both present or the figure is the unmetered 0;
|
||
and `sig` is an Ed25519 signature by `signer_pubkey` over the signed bytes, verified as
|
||
SPEC §7 *Signatures* requires: a key that is not its point's one encoding or is of small
|
||
order, `S` ≥ L, an `R` of small order, or an equation that does not hold with `R`
|
||
compared byte for byte fails the receipt.
|
||
|
||
## Appendix I — Reading an MPD
|
||
|
||
An MPD is read as XML 1.0 in this subset, and refused (`playlist_malformed`) outside it:
|
||
|
||
- UTF-8, with an optional byte order mark; CR LF and a lone CR are read as LF (XML 1.0
|
||
§2.11), and every character is an XML `Char`;
|
||
- an optional XML declaration first (`<?xml` followed by white space or `?`, up to the
|
||
first `?>`); before and after the root element, and among content, comments (with no
|
||
`--` inside) and white space; no document type declaration, CDATA section or other
|
||
processing instruction;
|
||
- names of ASCII letters, digits, `_`, `:`, `-` and `.`, not beginning with a digit,
|
||
`-` or `.`; an element's local name is its name after its last `:`;
|
||
- attribute values in `"` or `'`, holding no `<`, a literal tab or LF read as a space
|
||
(XML 1.0 §3.3.3); no attribute twice in an element;
|
||
- references, in values and in text, to `amp`, `lt`, `gt`, `quot` and `apos`, and
|
||
character references (`&#` and at most 10 decimal digits, or `&#x` and at most 8
|
||
hexadecimal digits) to an XML `Char`; no `]]>` in text;
|
||
- elements nested at most 64 deep, and nothing after the root element but comments and
|
||
white space.
|
||
|
||
Text is not read. The root's local name is `MPD`, with at least one `Period`. A
|
||
`Period`'s `@start` is an `xs:duration` of days, hours, minutes and seconds
|
||
(`P[nD][T[nH][nM][n[.n]S]]`, with at least one of them, in digits alone), read in
|
||
milliseconds as §11.2 reads a duration; absent, it is 0 for the first period and refused
|
||
for a later one. An `AdaptationSet`'s timelines are the `SegmentTimeline` of its own
|
||
`SegmentTemplate` or `SegmentList`, and of each of its `Representation`s'. A timeline's
|
||
`@timescale` is that element's, else that of the same kind of element in the
|
||
`AdaptationSet` and then the `Period`, else 1; 0 is refused. Each `S` has `@d`, a
|
||
positive integer, and may have `@t` (by default the end of the `S` before it, 0 for
|
||
the first) and `@r` (by default 0: the segment repeated `@r` more times). `@t`, `@d`,
|
||
`@r` and `@timescale` are digits alone; a segment whose end, `@t + @d·(@r + 1)` for the
|
||
last of a run, passes 2^64 − 1 is refused, and so is a document of more than 256
|
||
timelines. Other elements and attributes are not read.
|
||
|
||
A reader holds a timeline as it is written, one run per `S`, and never expands `@r`:
|
||
what it holds is bounded by the document's size, whatever `@r` says. §11.2's comparison
|
||
runs run by run: each step passes a WAI run or a classical one, so it is linear in the
|
||
runs of the two timelines. The names an element's attributes have are kept in a set, so
|
||
an element is read in time linear in its attributes.
|