WAI Extension: HTTP Delivery
Mirrored from the canonical text at commit 626d9db2 ().
Status: Draft. Binds WAI envelopes to HTTP: a proposed media type, a
codecsgrammar, content-addressed immutable objects, verified byte-range fetch of one component of aWAI2, 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 inwai-rs:http_binding(featurehttp) for §3–§7 and the origin side of §5 and §9, and thewai httpcommands of thewaitool. 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; 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 §2.6 and §3.2, not a cache under §9. What a refinement does to its base is staged-delivery’s and SPEC §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 §2 or §7.1) served as one HTTP representation.
- Content hash, cid: the object’s jwp-receipts §2.1 content
hash,
BLAKE3("moq-jwp:object\x01" ‖ envelope), written as 64 lowercase hexadecimal digits.envelope_lenis the envelope’s length in bytes. - Head: for
WAI1, the first12 + man_lenbytes — magic,man_len, the manifest andpayload_len; forWAI2, the first10 + man_len + 8·nbytes — magic,man_len, the manifest,n_renditionsand the rendition table. Its length ishead_len. - Component: one entry of a
WAI2rendition table and its payload bytes (SPEC §7.1). Its range is thelengthbytes fromhead_len + offset. - Covered length:
covered_len = min(envelope_len, 1024·⌈(15 + head_len)/1024⌉ − 15). - Alias: any URI other than
<cid>.waiunder 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 §5, §7.1); - a component with
role: "refinement"(SPEC §7.1, Refinement components); - a layer object: a
WAI1envelope whose capability is refinement-only, which carriesrefines(staged-delivery §1, §3). AWAI1whose capability is not refinement-only is not a layer object, whatever else it carries (§10.4).
- a companion whose registered role is
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 §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’senvelope_len.h=<decimal>: the object’scovered_len.c=<component id>: a preference for the component whoseid(SPEC §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 examplemp4a.40.2) MAY appear beside WAI elements. An element that begins withwai, in any ASCII case, is a WAI element: it MUST match the grammar, and is never read as classical. wai1states that every object the element describes is aWAI1envelope;wai2states that at least one is aWAI2envelope. Both name WAI MAJOR 1.- A
capis a capability string (SPEC §5) without its leadingwai.. Every capability this revision registers beginswai.and matches the rule once that is removed, so the mapping is one-to-one. - In an
alt, the firstcapis a selectable capability, and each+capis a capability whose payload is applied with it: a companion (SPEC §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
derivationorrecord(codecs_not_media), or a candidate (codecs_candidate). A firstcapMUST NOT be companion-only or refinement-only, and a+capMUST 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. AcapMUST NOT appear twice among an element’salts, nor twice in onealt. A string that breaks any of these iscodecs_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, itscapabilityand itsfallback, each where SPEC §4 dispatches it, as analt: a record, a derivation, a candidate, a companion-only or a refinement-only capability is never listed, so aWAI1naming a record beside a payload fallback is described by the fallback. For aWAI2, onealtper distinct selectable capability (SPEC §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). AWAI1whose capability is not refinement-only is described by its capability and fallback as above even when it carriesrefines, 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 thealtof the base itsrefines.basenames among the sequence’s objects — aWAI1, through itscapability(staged delivery never takes a base through its fallback), or a component of aWAI2, by the content hash of the component’s exploded envelope (SPEC §7.1), through the component’s capability. A sequence with a layer whose base it does not hold cannot be described (codecs_layer_unbound).alts 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 thealtof its own base’s capability — the base itsrefines.basenames, as above, which the sink holds — and not by following+in any otheralt; a sink that does not hold that base cannot find the layer declared. - Floor. The floor of a sequence is the
alts whose firstcapis 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 awai-elementwith the union’s prefix, in HLS in theX-WAI-FLOORattribute (§11.1) and in DASH in aSupplementalProperty(§11.1). - Support. A sink supports an element when it reads the element’s container
(
wai1:WAI1;wai2: both) and supports the firstcapof at least onealtof the floor. The capabilities after+never affect support: a sink that cannot apply one presents the rendition (SPEC §7.1). A sink supports acodecsvalue 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 §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.
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) | — | — |
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 §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:
- the file has the format above; its cid is the object’s; its
envelope_lenequals the complete length of everyContent-Rangeand the length of every200body it received; and itscovered_lenisenvelope_len, withn_cv0, or is less than it with15 + covered_lena multiple of 1024 (head_proof_malformed); - the chaining values are consumed exactly, and
rootequals the cid (head_proof_mismatch); envelope_lenis the length the verified head gives (§5.1), andcovered_lenis §2’s formula for that head (head_proof_malformed). The root does not encode the object’s length, so a wrongenvelope_lenthat 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 §7.1).
The procedure:
- In parallel, request
Range: bytes=0-(E−1), withEthehfragment if given and 16369 otherwise, and request<cid>.wai.hp. - Check each response (§7.1). If
covered_lenexceeds the bytes received, request the rest of the firstcovered_lenbytes. - Verify the head (§6.3). Parse it (
head_malformed); apply §5.1; refuse a capability the variant’s union does not list (codecs_undeclared). - Select from the head exactly as SPEC §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.baseis itsid. - 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. - 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 pastcovered_lenand go to step 7. A refinement that mismatches is dropped alone. - Request the rest,
bytes=covered_len-(orGETthe whole object), verify the whole envelope against the cid (object_digest_mismatch), and select as SPEC §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 §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 §4.
7.1 Response checks
For each request a client checks, in order:
- the status is
206, or200with a body that is the whole object (http_status_unexpected); - no
Content-Encodingother than none is present (content_coding_present); - on a
206,Content-Rangenames exactly the requested range, its last position reduced toenvelope_len − 1where the request reached past the end (content_range_mismatch), and its complete length equals the head proof’senvelope_len(complete_length_mismatch). Each of its numbers is one or more ASCII digits, RFC 9110 §14.4’s1*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’senvelope_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 §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 whose link hash (jwp-receipts §3 step 3) is h, in its
canonical form, or a claim whose link_hash (§2.4.3) is h, in its JCS form. It is
immutable and served under §5.3’s cache headers with Content-Type: application/json.
A client refuses a receipt file whose computed hash is not its name
(receipt_name_mismatch). A group receipt’s parent_receipt_hash is the name of its
parent’s file, so a client resolves jwp-receipts §3 step 3 by
fetching rcpt/<parent_receipt_hash>.json, and reports parent_not_held when it
cannot, never skipping the step.
8.2 Receipt index
<cid>.wai.receipts is the sidecar carriage of the WAI_RECEIPT property
(moq-streaming-format §5):
{ "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 checks each item on its own and
ignores, item by item, one that fails:
- a delivery item: the property’s
content_hashand the object receipt’scontent_hashequal the cid (receipt_not_about_object); the group receipt is fetched asrcpt/<property.group_receipt>.jsonand verified under jwp-receipts §3 steps 1–5, step 4 as §8.3 states; the property is checked against the object receipt as moq-streaming-format §5 states; - a claim: verified under jwp-receipts §3.1; its
subjectis the cid or the link hash of a group listed in the index. A soft-binding claim (jwp-receipts §2.6.2) is reported under the ancestor rule (§3.2): as attesting the ancestor, never the object in hand.
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 §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.
A component’s exploded envelope (SPEC §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 §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 §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 that anchors an origin fetches its key log
from the origin over HTTPS without following a redirect, as jwp-receipts
§7.3 states, and with Cache-Control: no-cache. Later revisions MAY come through any
cache: each is checked against its parent, and a rolled-back copy is reported
(key_log_rolled_back). The grants file is a locator: each grant is checked on its
own.
8.5 Parameter sets
A parameter set MAY be served as an immutable resource named by its pin’s digest
(<base>/<sha256>), under §5.3’s cache headers with application/octet-stream. A
sink verifies it by its pin (SPEC §3.1) and decodes against it only if
the checks of jwp-receipts §7.9 pass. A set they refuse is not
held, and dispatch continues as SPEC §4 and §7.1 state.
8.6 Playlist receipts
A WAI playlist (§11) is authenticated by a manifest receipt (wai.video.manifest,
SPEC §5) whose manifest_hash is the BLAKE3 of the playlist bytes,
served immutable at mr/<manifest_hash>.json resolved against the playlist URI. Each
revision of a live playlist has its own receipt, whose parent_receipt_hash names the
previous one. A playlist receipt MAY carry render, the rendering parameters of the
playlist it signs (version, media sequence, discontinuity sequence, part target,
whether it ends), and extras, the per-segment lines that rendering adds
(discontinuities, program date-time, parts, layer lines). When both are absent, the
playlist is rendered as before. manifest_hash covers the rendered bytes, so both are
bound by the signature through it.
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 §4.1 (see §4.5 there). It is bound by §4.3 there, and it MUST:
- 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;
- answer a single byte-range request from a stored complete object with
206and the exact bytes (RFC 9110 §14), and with416andContent-Range: bytes */<len>for an unsatisfiable range. On a miss it MAY forward the range, or fetch the whole object and answer from it; - keep
ETag,Content-Type,Cache-Controland, on a200,Content-Lengthas the origin sent them; - not vary a stored object by any request header;
- evaluate
If-Rangewith a strong comparison (RFC 9110 §13.1.5, §8.8.3.2); - 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; - 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 §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 §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; 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 §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 §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 §7). A refinement that states no
sha256, or does not match it, is dropped (§7 steps 5 and 6), and its base is
unaffected.
10.4 Layer objects
A layer object is a WAI1 whose capability is refinement-only (SPEC §5,
staged-delivery §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 §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:
#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 §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
§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.m3u8uses 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. EachEXT-X-STREAM-INFcarriesCODECS(the union, §4.2),X-WAI-FLOOR(the floor), andBANDWIDTHandAVERAGE-BANDWIDTHcomputed from the objects’envelope_lenover their durations (peak and mean of8·envelope_len/duration), never stated by hand.wai.mpdis a separate MPD with oneAdaptationSetper WAI stream,mimeType="application/wai",@codecs(the union), anEssentialProperty schemeIdUri="tag:transaction.science,2026:wai/http-delivery"so a client that does not know it ignores the set, and aSupplementalProperty schemeIdUri="tag:transaction.science,2026:wai/floor"whosevalueis the floor.
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: the same Period@ids and
@starts, and a SegmentTimeline with the same timescale, t and d. 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.
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.
-
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 isP_floor = e + max(r_obj, r_pl) + vagainst
e + r_objfor a part presented without verification: the added latency ismax(0, r_pl − r_obj) + v, withvthe 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=YESpart is one whose envelope decodes without another object.
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.
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 §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 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
{ "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 inasked.host: by a native host. A capability the sink cannot decode is absent.provisionalis true for auacapability: the decoder answered for the profiles asked, not for a given payload.held_setslists, per capability, the digests of the parameter sets that passed jwp-receipts §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) |
component_digest_mismatch | §7 step 6 (SPEC §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 |
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 |
Selection, pin, receipt, key and module reasons are those of SPEC §3.1 and §7.1 and jwp-receipts §3, §7.7 and §7.9, and the layer reasons those of staged-delivery §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/holds objects, head proofs and cases for §4–§7 and §10 and their reasons, with positive cases.wai_http_vectors verify(alsowai http vectors verify) and the independenthttp_verify.py, which shares no code withwai-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): everyContent-Range,codecsvalue,#WAI-LAYERline, manifest, member type, encryption,refinesand layer sequence it generates.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
sha256values 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,nandhare 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.
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/encawrapping; confidentiality is SPEC §6.
Appendix C — Mapping to moq-streaming-format
| MoQ (moq-streaming-format) | 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 receipt per group | 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 client’s depth policy; nothing is sent to the edge |
| a relay drops layers above the cap, receipts intact | the client does not request them; caches never drop; receipts stay listed (§10.5) |
| the delivery timeout after which a relay drops an object | the client’s refinement deadline (§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), thenbytes=1009-3593(2,585 B). - Body bytes: 3,839, with
envelope_len53,794. - Without
h: the first request,bytes=0-16368, holdslowwhole; 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-53793and 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’s EXT-X-PART, EXT-X-PRELOAD-HINT and blocking-reload clauses | 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.