open-standards/wai/extensions/live-http-egress.md
Transaction Science ce32ef0571 live-http-egress §8.5: a client verifies a statement's signature before it holds the statement's key to the delegation
The signature first, by the statement's own signer, then that signer,
its aliases and its time held to the owner's delegation (D103). A
statement by another key is refused as not delegated only once its
signature verified, and a forged one, by any key, for its signature;
only a statement both forged and by another key changes its reason
(statement_not_delegated to statement_signature). http-delivery §8.10
step 5 states its two parts in that order.

wai-rs Holder::check checks so, and the web players' core with it. The
egress-statements corpus gains other-key-bad-signature (statement_
signature); egress_statements_verify.py checks in the new order and
catches the old one as a weakened verifier (signature_last), which only
the new case catches. The browser run holds statement-other-key to the
check that refuses it, "signed by a key the delegation does not name",
so the run's own key's signature was verified first, in every player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-13 02:49:55 -05:00

1590 lines
111 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# WAI Extension: Live HTTP Egress
> Status: Draft. Registers no capability and no record. It is how a room's CMAF tracks, which
> a room delivers over MoQ ([live-rooms](live-rooms.md) §6), are served over HTTP while the
> room is live: an **egress** subscribes to an owner's tracks and their receipts tracks as any
> subscriber does, holds each object once its bytes verify against its group's signed receipts
> (§3), or, while the group is still growing, against a **running receipt** the owner signed
> for that object (§3.5), and packages what it holds when it is asked: low-latency HLS, one part per object,
> with preload hints, blocking playlist reload and playlist delta updates (§5), and
> low-latency DASH, a dynamic MPD whose segments are sent as they grow (§6). Nothing is
> written ahead of a request. A track under Common Encryption is served as it is, by an egress
> that holds no key: its playlists and its MPD state the protection the catalog states, and
> name a new header where the key changes (§4.5). A room's breaks are carried through in the forms
> [live-ads](live-ads.md) gives them (§7). A stitched session's composed tracks (live-ads §14.8),
> whose Groups change ingredient at their boundaries, are served as they were signed, each Group
> placed where the one before it ended and each change of ingredient marked: a discontinuity in
> HLS, a Period in DASH (§4.6). Every part is served beside the publisher's
> receipts for it, so a client verifies what it was sent as a subscriber of the room does
> (§8); and every playlist and MPD beside a statement, signed under a key the owner delegates
> to the egress, so a client acts on none that was changed, is of another track, or is older
> than one it holds (§8.5). A trace of what an egress received and what it answered can be checked by anyone:
> a part listed before its object was held, a withheld object served, and a blocking reload
> answered early are each refused with a reason (§10, §11). §9 says what latency is here: a
> figure measured on a host, never a property of the format.
> WAI defines no transport. The reference implementation is the `live_egress` module of
> `wai-rs` (feature `live_egress`) and the gateway `joule-moq-http`
> (`joule-code/joule-code-rs/crates/joule-moq-http`); the corpus is
> [`live-conformance/http-egress/`](../live-conformance/http-egress/), and its second
> implementation is `live-conformance/http_egress_verify.py`; the statements' corpus is
> [`live-conformance/egress-statements/`](../live-conformance/egress-statements/), and its second
> implementation `live-conformance/egress_statements_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: the WAI texts this one cites; `draft-pantos-hls-rfc8216bis-22`
> (the HLS second edition: §4.4.3.7, §4.4.3.8, §4.4.4.5, §4.4.4.7, §4.4.4.9, §4.4.5.2,
> §4.4.5.3, §6.2.2, §6.2.5, §6.2.6, §6.3.7 and §6.3.8); `draft-ietf-moq-cmsf-01` (CMSF, whole)
> and `draft-ietf-moq-msf-01` (MSF, §5); and the DASH Industry Forum's change request
> *Low-latency Modes for DASH* of 27 March 2020 (clauses 9.X.4 and 9.X.5). For §4.5:
> `draft-pantos-hls-rfc8216bis-22` again (§4.4.3.3, §4.4.4.3, §4.4.4.4, §4.4.4.5, §4.4.5.2,
> §5, §6.2.3 and §6.2.5.1), CMSF §4, and the DASH Industry Forum's *Interoperability Points,
> Part 6: Content Protection*, version 5.0.0 of January 2022 (clauses 5, 7.2, 8 and 9.2). For §4.6:
> `draft-pantos-hls-rfc8216bis-22` again (§3, §3.1.2, §4.4.3.3, §4.4.4.3, §4.4.4.9, §6.2.1, §6.2.2
> and §6.3.3), and the DASH Industry Forum's *Implementation Guidelines: Restricted Timing Model*
> (commit snapshot of 24 October 2024: §9.1, §12 and §18.4).
> ISO/IEC 23009-1, ISO/IEC 23000-19 and ISO/IEC 23001-7 were not read: what this text writes
> of an MPD and reads of a CMAF file is what those texts and CMSF state of them.
## 1. Scope
In scope: what an egress holds of a track and what it withholds (§3); the resources it
serves (§4); the HLS playlists (§5) and the MPD (§6) it packages from what it holds; the
breaks (§7); the receipts beside each part (§8), and the statements beside each playlist and
MPD (§8.5); what a latency figure is (§9); and the trace and its check (§10).
Out of scope: a transport; a codec; how a publisher makes CMAF; tracks of WAI envelopes
that state no time for their objects, which an egress does not serve (§4.4), and envelopes
as objects of their own, which are [http-delivery](http-delivery.md) §11.4's; a key, a
licence and any content protection system's exchange, none of which an egress holds or
makes (§4.5 states a protected track's protection from its catalog and reads no sample);
and who may fetch from an egress, which is the
deployment's (live-rooms §10.3 names one way).
Relationship to [moq-streaming-format](moq-streaming-format.md): a track here is of
`packaging` `cmaf` (CMSF §3.5.1) in an MSF catalog, with a receipts track as §4.1 there
gives any media track. Relationship to [http-delivery](http-delivery.md): the playlists here
are classical ones, of fragmented MP4, that any HLS or DASH client reads; receipt files and
the receipt index have the forms §8.1 and §8.2 there give them. Relationship to
[live-metering](live-metering.md): an egress is a hop of the room of role 4 (§3.12 there). Its
session at a relay is a subscriber session under the grant it holds, metered at that relay as a
viewer's is, and what it answers over HTTP it states in a per-hop record of its own, whose bytes
the room's session receipt counts beside the relays' and a tariff bills
([receipt-ledger](receipt-ledger.md) §4.7).
## 2. Terms
- **Owner**: the publisher of a set of tracks of a room, or a slot (live-rooms §6.1).
- **Track**: a CMAF track of an owner, named here `<kind>/<layer>` from the last namespace
field and the Track Name of live-rooms §6.1: `video/0`, `audio/0`, `screen/0`.
- **Group**, **object**: MOQT's. A group is a segment and an object is a part.
- **Running receipt**, **run**: §3.5's. A running receipt is the owner's signed statement of
one object of a group that is still growing; a group's run is its running receipts, in
position order.
- **Held**: an object is held once §3.2 has accepted it. **Withheld**: it arrived and was
refused, with a reason of §3.3.
- **Closed**: a group is closed once §3.4 says it is over. The **open group** of a track is
its lowest group that is not closed, once it has a held part.
- **Ticks**: units of a track's timescale. `ms(x)` is `⌊1000·x / timescale⌋`; the duration in
milliseconds from tick `a` to tick `b` is `ms(b) − ms(a)`, so durations along a track sum to
its length exactly.
## 3. What an egress holds
### 3.1 Reading CMAF
A box is a 32-bit size and a type, or a size of 1 and a 64-bit size after the type; a size
of 0, one below its header's length, or one past the end of what holds it does not read.
A track's **header** is the `initDataList` entry its catalog entry's `initRef` names (CMSF
§3.1). It reads when: its first box is `ftyp`; it has exactly one `moov` and no `moof` or
`mdat`; the `moov` has exactly one `trak`, whose `tkhd` gives the track ID and whose one
`mdia` has one `mdhd`, giving the timescale, and one `hdlr`; and the `moov`'s one `mvex` has
one `trex`, of that track ID, giving the default sample duration and default sample flags.
The timescale and the track ID are above 0. A track whose header does not read serves
nothing.
An **object** reads as CMAF chunks (CMSF §3.3) when its boxes are, in order, any of `styp`,
`prft` and `emsg`, and one or more `moof` each directly followed by an `mdat`, and no other
box; each `moof` has one `mfhd` and one `traf`; the `traf` has one `tfhd`, of the header's
track ID, one `tfdt` and one or more `trun`. A sample's duration is its own in the `trun`,
else the `tfhd`'s default, else the `trex`'s; its flags are the `trun`'s first-sample flags
for the first sample, else its own, else the `tfhd`'s default, else the `trex`'s. The
object's **start** is its first `tfdt`'s decode time, its **duration** the sum of its
samples' durations, which is above 0, and each later `moof`'s decode time is where the
samples before it ended. It is **independent** when its first sample's flags do not set
`sample_is_non_sync_sample` (bit 16).
### 3.2 Holding
An egress keeps, per track, the receipts and objects of the groups that are not closed, in
the order they arrived. The first receipts to arrive for a group, and the first bytes to
arrive for an object, stand: a second arrival is ignored, as is anything of a group at or
below the highest group closed, and an object already held or withheld.
A group's **receipts** are the one object of that group on the receipts track
(moq-streaming-format §4.1). They are **read** when: they are in the binary form of
[jwp-receipts](jwp-receipts.md) §2.7; the group receipt is in the labelled payload; its
`track_alias` is the catalog's `wai_receipt_alias`, its `group_id` the MOQT group, and its
`signer_pubkey` the owner's key; it lists `leaf_count` object receipts, whose `object_id`s
are 0, 1, … in order; and its signature verifies (jwp-receipts §3 steps 1 and 2). Receipts
that are not read are **refused**.
Only the lowest group that is not closed is worked on. When its receipts are read they are
taken up on the track's chain: where the egress took up a group before and this group's
`parent_receipt_hash` is not that group's link hash, the chain is **anchored again** here,
which the egress counts and reports. An egress joins a room that is running and misses what
it misses; it never waits for a parent.
Then, in object order from the first object not yet held, while the object's bytes and its
receipt are both there, the object is held if every check passes, and withheld with the
first reason that fails:
1. the content hash of the bytes, [jwp-receipts](jwp-receipts.md) §2.1's, is the receipt's
`content_hash` (`content_hash_mismatch`);
2. the receipt's proof reaches the group's signed root at its position, jwp-receipts §3
step 5 (`inclusion_proof_failed`);
3. the bytes read as CMAF chunks of the track (`cmaf_malformed`);
4. the group's first object is independent (`group_not_sap`), and starts no earlier than
the track's last closed segment ended; each later object starts where the one before it
ended (`cmaf_discontinuous`). On a composed track the group's first object is placed by
§4.6 instead (`composed_unplaced`), and a later object starts where the one before it ended
on the track's presentation timeline;
5. the object lasts at most the part target, in milliseconds (`part_over_target`);
6. from the group's first object's start to this object's end is less than the target
duration and half a second, in milliseconds (`segment_over_target`).
Once an object of a group is withheld, no later object of that group is held: the group is
**cut** there.
Until a group's receipts arrive, an object of it is held on the group's **run** (§3.5)
instead: in object order, while the object's bytes are there and the run covers its position,
the object is held if its content hash is the one the run states for that position
(`content_hash_mismatch`) and checks 3 to 6 pass, and withheld, the group cut, otherwise.
Check 2 has no part in it: a running receipt's place in the group's tree was checked when it
was taken into the run. Once the group's receipts have arrived, read or refused, nothing more
of the group is held on its run.
### 3.3 Why an object is withheld
| reason | the object |
|---|---|
| `receipt_refused` | is of a group whose receipts were refused |
| `receipt_missing` | is of a group that closed before its receipts arrived, or has no receipt in them |
| `object_missing` | followed an object of its group that never arrived |
| `after_withheld` | followed an object of its group that was withheld |
| `content_hash_mismatch` | does not hash to what its receipt states |
| `inclusion_proof_failed` | has a receipt whose proof does not reach the signed root |
| `cmaf_malformed` | does not read as CMAF chunks of the track |
| `group_not_sap` | starts a group and does not start with a sync sample |
| `cmaf_discontinuous` | does not start where its track left off |
| `part_over_target` | lasts longer than the part target |
| `segment_over_target` | takes its group past the target duration |
| `track_not_timed` | is of an envelope track that states no time for it (§4.4) |
| `track_not_independent` | is of an envelope track whose objects do not each decode alone (§4.4) |
| `envelope_malformed` | is not one whole `WAI1` envelope of media that decodes alone (§4.4) |
| `envelope_undeclared` | is an envelope the catalog or the track's header does not describe (§4.4) |
| `protection_unstated` | is of a track whose header states protection its catalog does not, or of a group before the track's first key period (§4.5) |
| `composed_unplaced` | is of a composed track off its grid, at or after a Group whose place the egress could not tell from the bytes (§4.6) |
A withheld object's bytes are never served and never listed, in a part, in a segment or by
range.
### 3.4 Closing a group
The lowest group that is not closed is **over** when either holds:
- a later group's receipts are read, and that group's object 0 has arrived, hashes to its
receipt's `content_hash` and is included under the group's root: the publisher has moved
on. So it has when a later group's receipts have not arrived, its run (§3.5) covers its
object 0, and that object has arrived and hashes to what the run states; but not on a
composed track (§4.6), which places a group only after the whole one before it: a group
closed by a later group's run before its own receipts came would not be whole, and the next
could not be placed;
- the publisher ended the group (its stream finished) and the group's receipts have
arrived, read or refused.
It is then closed. Each of its objects that arrived and is not held is withheld: for
refused receipts `receipt_refused`; for none, or no receipt for that object,
`receipt_missing`; in a cut group `after_withheld`; otherwise `object_missing`. Where it has
held parts they become a **segment**, named by the group's number; the segment is **whole**
when the group was not cut, no object of it was left over, its receipts were read, and every
object they list was held. A group whose parts were held on its run and whose receipts never
came, or were refused, leaves a segment that is not whole. A group with no held part leaves no
segment. Closing goes on to the next
group while that one is over too.
A track's **media sequence numbers** count its segments in order, the first being the number
of the first group the egress held a part of. When a group's first part is held and it
starts after the last closed segment ended, the interval between is filled with **gap
segments**, each of at most the target duration, the last one shorter: each takes a media
sequence number and offers no bytes. So what an egress missed, and what it withheld, is a
stated gap on the timeline and never a silent splice.
An egress keeps twice the window's closed segments of each track, with their parts and
their group receipts, and drops the oldest past that.
### 3.5 Running receipts
A group's receipts are signed over all its objects (jwp-receipts §2.2), so a publisher that
signs a group when it ends can publish them only then. Such a publisher **MAY** sign, for
each object as it sends it, a **running receipt**: its statement of that object's place in
the group so far. An egress that takes running receipts holds an object once its running
receipt has arrived (§3.2), and the group's receipts, when they come, close the run and must
agree with it. Group and object receipts, their digests and their binary form are unchanged.
**What it is.** A running receipt is not a receipt kind of jwp-receipts and has no signature
format of its own. It is the payload of a `meta` event ([live-events](live-events.md)
§4.2), signed by the owner as every event of the owner's is (live-events §3.2),
anchored (live-events §3.1) to the object it is about, and carried on the owner's events
track as any event is (live-events §8.1), where its timeline record is indexed by that
object's Location. The payload is 141 bytes; integers are little-endian, as an event's are:
| offset | size | field | meaning |
|---|---|---|---|
| 0 | 4 | `magic` | `WRR1` (ASCII) |
| 4 | 1 | `version` | 1 in this revision |
| 5 | 8 | `track_alias` | u64: the media track's `wai_receipt_alias` |
| 13 | 32 | `content_hash` | the anchored object's content hash (jwp-receipts §2.1) |
| 45 | 32 | `leaf` | that object's leaf in the group's tree (jwp-receipts §2.2), in the form its object receipt will take, legacy or labelled |
| 77 | 32 | `prior` | the root (jwp-receipts §2.2) over the leaves of the group's objects before it; 32 zero bytes, the empty group's root, exactly for object 0 |
| 109 | 32 | `root` | the root over the leaves of the group's objects through it |
A publisher signs one for each object, in object order, the anchor's `group` and `object`
the object's MOQT Group ID and Object ID, and **MUST NOT** sign two that differ for one
object.
**The compact form.** A publisher **MAY** instead state one or more consecutive objects of a
group in one running receipt, ending at the object its event is anchored to, in a payload of
46 + 64·`count` bytes:
| offset | size | field | meaning |
|---|---|---|---|
| 0 | 4 | `magic` | `WRR2` (ASCII) |
| 4 | 1 | `version` | 1 in this revision |
| 5 | 8 | `track_alias` | u64: the media track's `wai_receipt_alias` |
| 13 | 1 | `count` | how many objects it states, 1 to 64, and at most the anchor's `object` plus one |
| 14 | 64·`count` | `objects` | for each object it states, in object order from the anchor's `object` less `count` − 1 to the anchor's `object`: its content hash (32 bytes) and its leaf (32 bytes), as `WRR1`'s `content_hash` and `leaf` |
| 14 + 64·`count` | 32 | `root` | the root over the leaves of the group's objects through the anchored one |
It states no `prior`. Its `root` is over every leaf of the group before the objects it states and
over theirs, so a reader that holds the run holds the order a `prior` gave it, and a reader that
does not hold the run takes none (*The run*, below): a `prior` binds nothing the root does not.
A publisher signs one once the last object it states is sent, so each object it states but the
last waits for the ones after it: a `count` trades one event's record and signature, shared by
`count` objects, against `count` − 1 objects' durations more before the first of them is held
(§9; decisions D94). A publisher states each object of a group once, in object order, in a
running receipt of either form, and the rule above holds of both: it **MUST NOT** sign two that
differ for one object.
**A covering statement.** A running receipt taken into its group's run is a covering
statement of [jwp-receipts](jwp-receipts.md) §4.1: the registry admits the class
`running-receipt`, its form `WRR1`, and the class `compact-running-receipt`, its form `WRR2`, as
covering objects (`covering_statements` in
[`registry/capabilities.json`](../registry/capabilities.json)). It covers each object it states,
at its position, whose content hash it states, on the owner's word. The two classes are one
run's: what this section says of a running receipt says of both. This section is what §4.1 there
asks of a class's text: a statement of the class is verified as *Reading one* and *The run*
say, it covers what *The run* says, and a group's receipts that contradict it are handled as
*Closing* says, and at a relay as *At a relay's door* says. An egress verifies the receipts
it serves, so it is a relay of jwp-receipts §4 (§4.5 there), and a part it holds on its run
(§3.2) is held on a covering statement.
**Reading one.** An egress that takes running receipts knows its room's id and, for each
track, the owner's key, the track's `wai_receipt_alias` and the track's digest as an anchor
names it (live-events §3.1). It reads each record alone: the event's chain, its rate and the
room's event policy are the room verifier's (live-events §6.2, §6.3), a subscriber of one
owner does not hold them, and nothing here reports on them. A record is **refused**, and
changes nothing, at the first of these that fails:
1. the record reads as an event (live-events §3.3), of kind `meta`, with an anchor
(`running_malformed`);
2. its `room` is the egress's room (`running_wrong_room`);
3. its signature verifies (live-events §3.2) (`running_bad_signature`);
4. its `principal` is the owner's key (`running_not_owner`);
5. its payload is 141 bytes, begins with `WRR1`, has version 1, and has a `prior` of 32 zero
bytes exactly when the anchor's `object` is 0; or begins with `WRR2`, has version 1, a
`count` from 1 to 64 and at most the anchor's `object` plus one, and is 46 + 64·`count` bytes
(`running_malformed`);
6. the anchor's `track` is the track's digest and `track_alias` its `wai_receipt_alias`
(`running_other_track`);
7. the anchor's `group` and `object` are the Location its timeline record was indexed at
(`running_location_mismatch`).
**The run.** A group's **run** is what the running receipts taken for it state, position by
position: for each, the content hash and the leaf stated, and the root over the leaves through
it. It **covers** positions 0 to its length less one, and its root is the root through its
last position, or 32 zero bytes while it is empty. A running receipt that passed the checks
above, of a group that is not closed, whose receipts have not arrived, whose run is not
broken and that the egress did not join inside, is, with *n* the run's length, *j* the anchor's
`object` and *i* the first position it states (*j* for a `WRR1`, *j* less `count` − 1 for a
`WRR2`):
- where *n* is 0, *i* > 0, and the group is the track's first group read or one before it: of
a group the egress **joined inside** (*Joining inside a group*, below), and ignored;
- where *i* < *n*: ignored when *j* < *n* and, for each position from *i* to *j*, it states the
run's content hash and leaf there, its `root` is the run's root through *j*, and a `WRR1`'s
`prior` is the run's root through *i* − 1 (a group of an events track repeats records at its
start, live-events §8.2); otherwise the owner has stated an object twice
(`running_restated`);
- where *i* > *n*: it skips an object (`running_skips_object`);
- where it is a `WRR1` and its `prior` is not the run's root (`running_prior_mismatch`);
- where its `root` is not the root over the run's leaves followed by the leaves it states
(`running_root_mismatch`);
- otherwise **taken**: each position it states is the run's next, in order.
Each of the four refusals **breaks** the run: no later running receipt of that group is
taken, the parts already held stay held, and the rest of the group waits for the group's
receipts, as every object does where a publisher signs no running receipts. A running
receipt of a closed group, of a group whose receipts have arrived, of a group whose run is
broken, or of a group the egress joined inside, is ignored.
**Joining inside a group.** A run starts at object 0, so an egress that joins inside a group
takes no run for it. A track's **first group read** is the group of the first of its running
receipts that the egress did not ignore before reading it and that passed the checks above. A
group whose run is empty was **joined inside** when such a receipt of it states no object 0
(*i* > 0) and it is the first group read or a group before it: that receipt and every later
one of the group are ignored, none is refused, and the group's objects wait for its receipts
as they do where a publisher signs no running receipts. Past the first group read the egress
reads every group's records from its start, and a group's first receipt with *i* > 0 skips an
object (`running_skips_object`). An events track's group is a minute of the room clock
(live-events §8.1), and a media group need not begin and end inside one: an egress that joins
a room on its events track's latest group joins inside the media group that began before that
minute did, and does so whenever it joins, however long the room has run. A compact receipt
anchored past object 0 whose first stated position is 0 is no join: its group's run starts
there.
**Closing.** The group's receipts, when they arrive and are read (§3.2), **close** the run,
and **MUST** agree with it: for every position the run covers, the group's receipts hold an
object receipt, whose `content_hash` is the run's and whose leaf (jwp-receipts §2.2) is the
run's `leaf`. Receipts that do not are refused (`running_contradicted`), as receipts that are
not read are: the owner signed two accounts of one group. The parts held on the run stay
held, since each was held to a digest the owner signed; the objects not yet held are
withheld `receipt_refused` when the group closes; and the egress counts the group and
reports it. Receipts that agree are taken up on the track's chain as §3.2 says, and each part
held on the run has its object receipt from then on (§8.1).
| reason | the running receipt, or the group's receipts |
|---|---|
| `running_malformed` | does not read as a `meta` event with an anchor and a `WRR1` payload |
| `running_wrong_room` | is of another room |
| `running_bad_signature` | has a signature that does not verify |
| `running_not_owner` | is signed by a key that is not the owner's |
| `running_other_track` | names another track or another receipt alias |
| `running_location_mismatch` | is anchored to another object than its record was indexed at |
| `running_restated` | states again, differently, an object the run holds |
| `running_skips_object` | is past the run's next position |
| `running_prior_mismatch` | does not continue the run's root |
| `running_root_mismatch` | states a root that is not over the run's leaves and its own |
| `running_contradicted` | (the group's receipts) do not agree with the run |
**Cadence.** A running receipt is an event, and counts against its principal's event policy
(live-events §5, §6.3) with every other event of the owner's. The default policy admits 50
events in a second and a payload of 1024 bytes: an owner of a video and an audio track
whose parts last 200 ms sends ten running receipts a second, fewer where it states several
objects in one, and an owner whose tracks together send more than 50 objects a second needs a
policy that says so, or the compact form. A `WRR2` of 64 objects is 4142 bytes, past the
default policy's payload; a publisher that states more than 15 objects in one names a policy
whose `max_payload` holds it. A stitcher owns every
session's composed tracks (§4.6), so its rate is the sum of theirs and of its own tracks'. What
running receipts and a group's receipts add to a track in bytes is a dated record of WAI's own
([research/overhead.md](../research/overhead.md) §4).
**A composed track's run.** A stitcher signs a composed track's running receipts as any owner
does ([live-ads](live-ads.md) §14.8): each a `meta` event of its own chain, anchored to the
composed Object (the composed track's digest as an anchor names it: the session's namespace and
`K`), stating it under the composed track's receipt alias with the leaf the stitcher's group
receipt gives it, on the stitcher's events track. An egress of a stitched session reads that
track as it reads an owner's, and holds a composed part on its run as it holds any part; the
group's receipts, which the stitcher signs as the group ends, close the run, and with it the
group (§3.4), so that the next group is placed (§4.6) one signature after the last part of the
one before it.
**At a relay's door.** A relay that reads the receipts it forwards at a door
([jwp-receipts](jwp-receipts.md) §4.1) **MAY** read running receipts as an egress does, for a
room's media track whose owner is a key its door holds the track's group receipts to and whose
`wai_receipt_alias` the owner's catalog states. It reads the owner's events track in its stream
order, which is Location order (live-events §8.1) across the track's groups as within one,
whatever order the streams that carry them reach it in; holds each
record to the checks of *Reading one* and each group's run to its rules (a group it began
reading inside takes no run, as for an egress that joins inside one); and fans out an object
its group's run covers once the object's content hash is the one the run states: the run's
receipt at the object's position is a covering statement of it (jwp-receipts §4.1), signed by
the owner, a key the door holds the group's receipts to. An object no run covers, one past
what a broken run took, and one that carries its receipt as a property wait for the group's
receipts, the one covering statement left to them. The group's receipts close the run as
*Closing* says, and this is how a relay handles receipts that contradict it, as jwp-receipts
§4.1 has a class's text say: they are refused (`running_contradicted`) and dropped with their
group as receipts whose signature does not verify are, so that no subscriber is given two
accounts of one group; the objects already fanned out stay fanned out, each covered by a
statement the owner signed; and the rest of the group is refused. Every other duty of
jwp-receipts §4.1 holds whichever statement covers an object: the receipts the door forwards
are unaltered, each group receipt is verified when it arrives, and an object no covering
statement covers is not fanned out. A relay's tally of signed figures counts an object passed
on its run under no figure, the figure being in a leaf only the group's receipts open.
## 4. Resources
### 4.1 Names
Under an owner's base URL:
| resource | what it is |
|---|---|
| `master.m3u8` | the multivariant playlist (§5.6) |
| `manifest.mpd` | the MPD (§6.1) |
| `dvb.mpd` | the MPD in the DVB-DASH form (§6.4) |
| `<track>/media.m3u8` | the track's media playlist (§5.1), with the delivery directives of §5.4 and §5.5 |
| `<track>/init.mp4` | the track's header, byte for byte; a protected track has none (§4.5) |
| `<track>/init.<i>.mp4` | a protected track's header of the key period whose index is `i`, byte for byte (§4.5) |
| `<track>/p/<g>.<o>.m4s` | the held object `o` of group `g`: a part |
| `<track>/p/<g>.<o>.m4s.receipts` | that part's receipt index (§8.1), once its group's receipts were taken up |
| `<track>/p/<g>.<o>.m4s.running` | the run that part was held on (§8.4), where it was held on one |
| `<track>/p/rcpt/<h>.json` | the group receipt whose link hash is `h` (§8.2) |
| `<track>/s/<g>.m4s` | the segment of group `g`: its held parts, concatenated (§6.3) |
| `<track>/s/<g>.m4s.parts` | that segment's parts list, once it is closed (§8.3) |
| `<track>/period/<i>/<g>.m4s` | of a composed track (§4.6), the segment of group `g` where it is of the MPD's Period whose `id` is `i`, the same bytes as `s/<g>.m4s`; otherwise 404 |
| `wai.m3u8`, `wai.mpd` | the multivariant playlist and the MPD of the envelope tracks (§4.4) |
| `d/<h>` | the owner's delegation whose digest is `h`, or the owner's revocation of it (§8.5), byte for byte |
`<g>`, `<o>` and `<i>` are decimal with no leading zero, and `<h>` 64 lowercase hexadecimal
digits. A name that is none of these, a delegation the egress does not hold, a part that
is not held, a segment that is neither closed nor open, a group receipt the egress does not
keep, and a playlist of a track with no held part, are answered 404. A request for a media
playlist of a track that has a held part and has closed no segment, and for the multivariant
playlist while that holds of any track, waits for it (§5.1, §5.6). Two other requests wait
instead of being answered 404: one for the part a preload hint names (§5.3), and one for the
segment of the open group or of the group after the highest group closed (§6.3), at its
Period's address too while the group is of that Period. A request at a Period's address is
answered by the state when it is answered: one that waited for a group that then began a later
Period is answered 404.
### 4.2 Parameters
An egress states four parameters, which do not change while it serves a room: the **target
duration** in whole seconds (1 to 60), the **part target** in milliseconds (at most the
target duration), the **window**, the number of closed segments a playlist lists (3 to
4096), and the **latency target** in milliseconds. A publisher whose groups and objects do
not fit them has those objects withheld (§3.2, checks 5 and 6).
### 4.3 The clock
When an egress first holds a part, of any track, it fixes its **anchor**: that instant on
its own clock, less `ms` of the part's end. The anchor is the wall-clock time of tick 0 of
every track, so an owner's tracks **MUST** share one decode timeline, starting together.
The anchor is the egress's statement of when it had the media, not the publisher's of when
it was made: an HLS program date-time and a DASH availability time are both read from it.
### 4.4 Envelope tracks
A room's track of `packaging` `"wai"` carries one whole `WAI1` envelope an object
([moq-streaming-format](moq-streaming-format.md) §3), and its receipts are over the
envelopes. An egress **MAY** serve such a track as Mode F
([http-delivery](http-delivery.md) §11.7, Appendix B): each held envelope is the one sample
of one CMAF chunk, byte for byte. Mode F's sample entries are not registered, so an egress
serves envelope tracks only where its operator turns that on, for test material, as
http-delivery §11.7 requires.
**What the track must state.** Packaging needs, for each object, a decode time, a duration
and whether it decodes alone. An envelope states none of them and the catalog states them
in one way only: time-aligned groups. A track is **served** when its catalog entry
- carries `wai_tempo` with `objects_per_720s`, from 1 to 2^32 − 1, and `start_ms`
(moq-streaming-format §4.4): otherwise it is not served, `track_not_timed`. An egress
guesses no duration, from a frame rate or from when objects arrive;
- declares no frame stream (`wai_frame_stream`) and is no layer track (`wai_staged.layer`
above 0): otherwise it is not served, `track_not_independent`. A frame after a group's
first and a layer do not decode alone, and Mode F has no sample that is not a sync sample.
Every object that arrives on a track that is not served is withheld with that reason, its
receipts are not read, and every resource under the track is answered 404.
**The timeline.** A served track's timescale is `objects_per_720s`. Object `o` of group
`g` starts at tick `(g + o) × 720` and lasts 720 ticks, exactly; tick 0 is `start_ms`, which
the egress reads for its presence alone (§4.3). The formula steps a group by one object, so
it fits a track of one object a group; a second object of a group is held where §3.2's
checks pass, and the next group's object then starts before the track left off and is
withheld (`cmaf_discontinuous`). An object whose `g + o` is 2^32 − 1 or more is withheld
`track_not_timed`: its fragment has no sequence number.
**Holding.** §3.2 holds for an envelope track with its checks 3 and 4 replaced; checks 1
and 2 are over the object's bytes, the envelope, as its receipt is:
3. the bytes are exactly one `WAI1` envelope of WAI MAJOR 1 that is not a layer object
(http-delivery §10.4) and that http-delivery §4.2 gives an element
(`envelope_malformed`); every capability of that element is one the catalog entry's
`wai_capabilities` lists; and its handler (`vide` for a manifest `media` of `video`,
`soun` for `audio`, `meta` otherwise) and its element are the header's, and its manifest
MINOR at most the header's (`envelope_undeclared`);
4. it starts no earlier than the track's last closed segment ended, and each later object
of a group starts where the one before it ended (`cmaf_discontinuous`). Every object is
independent.
**The header** is fixed by the first envelope that passes check 3 with no header to hold
it to: the catalog states neither the samples' class nor their highest MINOR. It is Appendix
B's header: `ftyp` (`cmfc`; `cmfc`, `iso6`), and a `moov` of an `mvhd`, one `trak` of track
ID 1 and an `mvex`, at the track's timescale, the handler that envelope's, one `wai1` sample
entry whose `waiC` states MAJOR 1, that envelope's MINOR, and its element as both the union
and the floor; a visual entry states the catalog's width and height, and an audio entry no
channel count. `<track>/init.mp4` is that header, and 404 before it is fixed.
**A part** of an envelope track is one `moof` and one `mdat`: an `mfhd` whose sequence
number is `g + o + 1`; a `traf` of a `tfhd` of track 1 with default-base-is-moof, a `tfdt` of
version 1 at the object's start, and a `trun` (flags `0x701`) of one sample with the data
offset, the duration 720, the envelope's length and the flags `0x02000000`; and an `mdat`
whose payload is the envelope. The bytes are those wai-pack's Mode F writer gives a segment
of one sample. Parts, segments, the media playlist, the preload hint, blocking reload, delta
updates, gaps and the grid are §3.4, §5 and §6's, unchanged: on a track of one object a
group, a segment is one part.
**What a client hashes.** The receipts are over the envelope, not the chunk. For a part of
an envelope track, `<part>.receipts` (§8.1) has as `object` the content hash of the part's
`mdat` payload, and a client checks that payload, the sample, against it; in
`<segment>.parts` (§8.3), `n` is the length of the part, the chunk, and `object` the content
hash of its `mdat` payload.
**Where they are listed.** `master.m3u8` (§5.6) and `manifest.mpd` (§6.1) are of the
owner's CMAF tracks alone, each served once every CMAF track has a held part or is on its
grid, and each the bytes it would be with no envelope track beside them: a classical player
is never offered a sample it cannot decode, and a CMAF track beside an envelope track plays
as it did. The served envelope tracks are listed in two files of their own:
- `wai.m3u8`, once each has a held part: `#EXTM3U`, `#EXT-X-VERSION:9`,
`#EXT-X-INDEPENDENT-SEGMENTS`, and per track
`#EXT-X-STREAM-INF:BANDWIDTH=<b>,CODECS="<e>",X-WAI-FLOOR="<e>"`, with
`,RESOLUTION=<w>x<h>` for a `vide` track, and `<track>/media.m3u8`; `e` is the header's
element and `b`, `w` and `h` the catalog's;
- `wai.mpd`, while each is on its grid: §6.1's MPD over those tracks, its `publishTime` the
instant they were first all on their grids, each `AdaptationSet` with no `contentType`,
`mimeType` `video/mp4`, `audio/mp4` or `application/mp4` by handler, an
`EssentialProperty` of scheme `tag:transaction.science,2026:wai/http-delivery`, by which
a client that does not know the scheme ignores the set, a `SupplementalProperty` of scheme
`tag:transaction.science,2026:wai/floor` whose `value` is the element, and a
`Representation` whose `codecs` is the element, with its `bandwidth`.
**What a player needs.** No browser and no classical player decodes these samples: the
sample entry `wai1` names no codec a media pipeline has, so a page cannot give such a track
to Media Source Extensions or to a native HLS or DASH player. A player of an envelope track
reads `wai.m3u8` or `wai.mpd`, supports the element by http-delivery §4.2 and never asks
the user agent's decoders about it (http-delivery §4.3), reads the header's `waiC`, takes
each part's `mdat` payload as one envelope, checks it against the receipts beside the part
(§8), and hands it to a WAI sink ([browser-sink](browser-sink.md),
[native-sink](native-sink.md)), which dispatches it by its manifest and presents it at the
time its fragment states. A classical player given `master.m3u8` or `manifest.mpd` finds no
envelope track there; one given `wai.m3u8` finds only codecs it does not know.
### 4.5 Protected tracks
A room's CMAF track may be under Common Encryption
([moq-streaming-format](moq-streaming-format.md) §4.9): the samples of each object are
encrypted, the catalog states the protection as CMSF §4 has it, and the track's key changes
with its **key period**, each stated by a catalog of its own. An egress serves such a track
with no key. It holds each object as §3.2 holds any, against receipts that are over the
protected bytes; it reads nothing of a sample, of a `senc` or of a `pssh`; and it writes, from
the catalog alone, what a player needs to ask for a key: which scheme, which KID, which
systems, and which header, for every segment it lists. It names systems by their system IDs,
as the catalog does, and takes part in no system's licence exchange.
**What a header states.** A header **states protection** when its one sample entry (the
first entry of the `stsd` in the `stbl` in the `minf` of the `mdia`) is `encv` or `enca`. It
then **gives** a scheme and a KID when that entry, after its own fields (78 bytes of a visual
entry, 28 of an audio one), holds exactly one `sinf`, with one `schm`, whose scheme type is
the scheme, and one `schi` holding one `tenc`, whose default KID (the 16 bytes after its
version, flags and four more bytes) is the KID. A header with no such path, or another
sample entry, is clear.
**A key period.** What a catalog states of a track's key period is: the period's `index` and
`from_group`, the track's `wai_key_period` (0 and 0 where a protected track's catalog carries
none); the **entries**, the `contentProtections` entries the track's `contentProtectionRefIDs`
name, in that order; and the period's header, which the track's `initRef` names. It **reads**
when all of these hold:
1. the header gives a scheme and a KID, and the scheme is `cenc` or `cbcs`;
2. the header reads (§3.1) with the track ID, timescale, handler and `trex` defaults of the
track's first header;
3. there is at least one entry, and every entry is an object whose `scheme` is the header's,
whose `defaultKID` is an array of UUID strings that holds the header's KID, and whose
`drmSystem` is an object with a `systemID` that is a UUID string;
4. where a `drmSystem` has a `pssh`, it is a non-empty string of Base64 in the standard
alphabet, padded; where it has a `laURL`, that is an object whose `url`, and whose `type`
where it has one, are strings of printable ASCII with no space, `"`, `<` or `>`.
An egress carries a `pssh` and a licence URI through as the catalog gives them and reads
neither; it does not hold a `pssh` to the KID.
**A track's first key period** is the one its catalog states when the egress reads it. A
track whose header states protection and whose catalog names no entry for it, or states a
key period that does not read, is **not served**: every object that arrives on it is
withheld `protection_unstated`, its receipts are not read, and every resource under it is
answered 404. So an egress never offers protected bytes under a playlist that calls them
clear. An object of a group before the first key period's `from_group` is withheld
`protection_unstated` too, and its group's receipts are not read. A track whose header is
clear and whose catalog names no entry for it is a clear track, served as §3 to §6 say.
**Later key periods.** A catalog published later states the track's next key period
(moq-streaming-format §4.9 has it published before the period's first group). The egress
**takes** it, and from then on a group is of the last key period taken whose `from_group` is
at most its number. It is refused, and changes nothing, at the first of these:
| reason | the key period |
|---|---|
| `key_period_not_protected` | is of a track that has no first key period: a clear track, an envelope track, or one that is not served |
| `key_period_malformed` | does not read |
| `key_period_out_of_order` | has an `index` or a `from_group` that is not above the last taken period's, or another scheme |
| `key_period_late` | has a `from_group` at or below a group the egress has closed or holds a part of |
A key period whose index, first group, entries and header are those of one already taken is
the same catalog again and is ignored. An egress counts what it refused and reports it. A
refused period's groups stay of the period before: where the publisher did change the key,
its players find segments their key does not open, and the report says why. An egress
cannot see that from the bytes.
**The headers.** `<track>/init.<i>.mp4` is the header of the key period whose index is `i`,
for every period taken, and 404 before. A protected track has no `<track>/init.mp4`: it is
answered 404, so a client that ignores the protection finds no header to start with.
**In a media playlist** (§5.1) of a protected track:
- the header lines end with `#EXT-X-DISCONTINUITY-SEQUENCE:<n>` in place of
`#EXT-X-MAP:URI="init.mp4"`, `n` the number of segments before the first one the whole
playlist lists, from the first the egress held, that **began a key period**: a segment
begins a key period when the segment before it on the track is of another. A gap segment
is of the key period of the segment before it;
- before the `#EXT-X-PROGRAM-DATE-TIME` of a closed segment, and of the open group, that
begins a key period: `#EXT-X-DISCONTINUITY`; and there, and before the first segment the
playlist writes (the first after an `EXT-X-SKIP`, in a delta update), the period's **key
lines**, one for each entry in order, and `#EXT-X-MAP:URI="init.<i>.mp4"`.
A key line is
`#EXT-X-KEY:METHOD=<m>,URI="<u>",KEYFORMAT="urn:uuid:<system ID>",KEYFORMATVERSIONS="1"`:
`m` is `SAMPLE-AES` for `cbcs` and `SAMPLE-AES-CTR` for `cenc`; the system ID is in
lowercase; and `u` is `data:text/plain;base64,` and the entry's `pssh` where it has one,
else its licence URI where it has one, else `urn:uuid:` and the KID as a UUID. The HLS
second edition defines the `identity` key format alone, and CMSF maps no entry to a key
line: this mapping is this text's. No `IV` is written: under `cbcs` the constant IV is in
the header's `tenc`. A client that knows none of the key formats ignores every line, as the
HLS second edition has it do, and does not play the track.
**In the MPD** (§6.1), where a track of the owner is protected:
- the `MPD` element declares `xmlns:cenc="urn:mpeg:cenc:2013"` and
`xmlns:dashif="https://dashif.org/CPS"` after its own namespace, and its `publishTime` is
the later of §6.1's instant and the instant a key period after a track's first was last
taken;
- there is a `Period` for each **boundary**: 0, and each instant a key period after its
track's first begins, which is its `from_group` times its track's grid duration. A
Period's `id` is its place among the boundaries, from 0, and its `start` the boundary, as
`PT<seconds with three decimals>S` (the first is `PT0S`). A boundary that is not a whole
number of some track's segments from 0 leaves that track no first segment there, and the
MPD is answered 404 from then on. A Period that ends at or before the start of the first
segment every track still keeps is left out; the others keep their `id`. The room's event
streams (§7) are written in the Period whose `id` is 0 and in no other: this revision
carries a break after the first boundary in the playlists alone;
- in each Period a track's `SegmentTemplate` has, as `startNumber`, the number of the
segment that starts at the boundary and, where that is above 0, `presentationTimeOffset`,
that segment's start in ticks, after it; its `initialization` is
`<track>/init.<i>.mp4`, `i` the index of the key period that segment's group is of, or of
the track's first where the Period starts before it;
- a protected track's `AdaptationSet` begins, before its `ProducerReferenceTime`, with that
key period's **descriptors**, each on a line:
`<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="<scheme>" cenc:default_KID="<the KID as a UUID>"/>`,
then for each entry in order `<ContentProtection schemeIdUri="urn:uuid:<system ID>">`
holding `<cenc:pssh>`, the entry's `pssh`, where it has one, and
`<dashif:Laurl licenseType="<type>">`, its licence URI, where it has one (`licenseType`
only where the `laURL` has a `type`), and written as an empty element where it has
neither. `&` is written `&amp;`.
A clear track beside a protected one has no descriptor and no key line, and its segments
are numbered through the Periods as a protected track's are.
**What is not here.** An egress does not check that an object is encrypted, that it is
under the key period's key, or that a `pssh` names the KID: it has no key, and
moq-streaming-format §4.9's verification, which has, is the publisher's and a verifier's.
The multivariant playlist carries no session key. No `EXT-X-PRELOAD-HINT` names a header.
Output protection and any system's licence format are that system's.
### 4.6 Composed tracks
A stitched session's tracks ([live-ads](live-ads.md) §14.8) are **composed**: each Group carries
an ingredient Group whole, the programme's or a creative's (live-ads §14.3), so a Group taken
from a creative carries the creative's Objects with the creative's own decode times, from 0.
The receipts over them are the stitcher's, over those bytes (jwp-receipts §2.1 is over the
payload), and a creative's digest names those bytes (ad-presentation §4): nothing of a composed
Object is rewritten, by the stitcher or by an egress, and its `tfdt` stays what was signed. What
moves an ingredient to its place is the playlist and the MPD, as the HLS second edition and DASH
place a segment whose timestamps do not continue the one before it.
**Which tracks.** An egress serves a session's composed video and sound as `video/0` and
`audio/0`: the tracks named `K` in the namespaces `[room, stitcher, video, stitched, D, K]` and
`[room, stitcher, audio, stitched, D, K]`, as the session's catalog in the first of them names
them, each under the stitcher's key and its receipt alias. Such a track is clear and of CMAF
(§3.1); one that is protected or of envelopes is not composed. A trace states it (§10.1).
**The presentation timeline.** Each segment of a composed track has a **shift**: its time on the
track's presentation timeline less the decode time its bytes state. The first group the egress
holds a part of has shift 0. A later group's first object, which is independent as every
group's first is (§3.2 check 4), is placed:
1. where the group's ID is one past the last closed segment's group, and that segment is whole
(§3.4): at that segment's end. Where the object's decode time is that segment's end less its
shift, the group has the same shift: the ingredient goes on. Otherwise it **begins an
ingredient**, and its shift is that segment's end less the object's decode time;
2. otherwise, where a Group was missed or the segment before it was cut short, and the track is
on its grid (§6.2) of duration `D`: at its group's number times `D`, which is no earlier than
the last closed segment's end. The interval between is gap segments (§3.4), so what was
missed is a stated gap, and its shift is that place less the object's decode time: it begins
an ingredient where that is not the last closed segment's shift. On a grid every segment the
track closed began at its group's number times `D`, by signed Group IDs and signed bytes, so
the group's number places it as it would have been placed after the one before it;
3. otherwise not at all: the egress cannot tell from the bytes where the group belongs, the
object is withheld `composed_unplaced`, and nothing of the track after it is held: each of its
later objects is withheld with that reason and no later group's receipts are read. Off a grid,
a Group missed, or a segment cut short, leaves the egress nothing to place what follows
against but timing no receipt covers (live-ads §14.3), and it does not place media by that:
placing the group where the last segment ended would state a gap of no length where media
was missed, the silent splice §3.4 rules out.
A group's first object is held on its group's run (§3.5) where the stitcher signs running
receipts, and placed by the same rule: the group before it is closed by its own receipts, never
by this group's run (§3.4), so a whole segment is there to place it after.
Every later object of a group has the group's shift. A part's start and end, a segment's, gaps,
the grid (§6.2), the dates (§4.3) and a segment's availability are on the presentation timeline;
check 4's continuity and checks 5 and 6 are held there too. A composed track whose creatives'
Groups last as long as the programme's Groups they fill stays on its grid (§6.2); one whose do
not leaves it, and is served by HLS alone.
**In a media playlist** (§5.1) of a composed track, the header lines end with
`#EXT-X-DISCONTINUITY-SEQUENCE:<n>` and then `#EXT-X-MAP:URI="init.mp4"`, `n` the number of
segments before the first one the whole playlist lists, from the first the egress held, that
**began a period**: a segment begins a period when its shift, or its key period (§4.5), is not
the segment's before it on the track; a gap segment has the shift of the segment before it.
Before the `#EXT-X-PROGRAM-DATE-TIME` of a closed segment, and of the open group, that begins a
period: `#EXT-X-DISCONTINUITY`, and after it `#EXT-X-MAP:URI="init.mp4"` again. The HLS second
edition requires the tag where the timestamp sequence changes (its §4.4.4.3 and §6.2.1) and has a
client place a segment by the playlist's timeline, not by its timestamps (its §3, §6.3.3); the one
header serves every ingredient, which are prepared alike (live-ads §14.7). A client must be ready
to reset its parser at a discontinuity (its §6.3.3), and a header named again after the tag is
the one it then takes (its §4.4.4.5: a map applies until the next): a player that holds a header
to one sequence of timestamps fetches nothing that is not named.
**In the MPD** (§6.1), there is a Period for each boundary of §4.5 and, for each composed track,
for each group that began an ingredient after the first: its number times the track's grid
duration. Each Period's `id`, `start` and `startNumber` are §4.5's; its `presentationTimeOffset`
is the decode time of that segment, its time less its shift, where that is above 0. So the DASH
timing model's mapping (`Period@start` plus the sample time less `@presentationTimeOffset`, over
the timescale) places the ingredient's first segment at the Period's start, whatever decode time
its bytes state. The MPD's `publishTime` is the later of §6.1's instant and the instant a group
that began an ingredient was first held.
**A Period ahead of its first segment.** The egress adds a Period when the group that begins it
has its first part held, and nothing signed states the change of ingredient sooner. A client
holding an MPD fetched before, which the DASH timing model has stay valid for its minimum update
period (its §13.6.1), takes the Period before as going on; were the new Period's first segment
already available to it, it would ask the earlier Period for it and place it by that Period's
offset: a creative at its own decode time, a Period's start early, or the programme after it a
creative's Period start late. So an owner with a composed track announces each Period ahead of its
first segment's availability instead, by making every segment available later: its MPD's
`minimumUpdatePeriod` is the part target and its `availabilityStartTime` the anchor plus the part
hold-back (three part targets, §5.1). An MPD fetched before a Period was added is then no longer
valid when that Period's first segment becomes available, while that group's first part is held
no more than two part targets later than the anchor places it, and a client that keeps its MPD
valid has the Period before it asks. The producer references still state the anchor (§6.1), so a
player's latency target stays reckoned from the producer's clock.
**Each Period's own addresses.** A composed track's `SegmentTemplate` in the Period whose `id` is
`i` has `media` `<track>/period/<i>/$Number$.m4s`, and the egress answers that address with a
segment only where its group is of that Period (§4.1): from the Period's `startNumber` to the next
Period's start. A client that asks an earlier Period for a later Period's first segment all the
same (one that kept an MPD past its validity, or a group held later than the margin above) waits,
is answered 404 once the group is held and begins the later Period, and finds that Period in the
MPD it fetches next; it is never given the segment under another Period's offset. A plain track's
Periods share `s/$Number$.m4s`: there every Period's offset places a segment where the Period
before would have placed it, so an older MPD places it right.
**What a client holds.** Every part is checked against the stitcher's receipts (§8), which
cover a composed Object as published; a playlist and an MPD against their statements (§8.5)
where the stitcher delegates a statement key. A checker of the trace holds the marks to the
state (§10.2): a composed track's playlist that leaves out the discontinuity before a segment
that begins a period (`discontinuity_missing`), and an MPD whose Periods do not begin where the
state's do, with their numbers and offsets (`period_off_boundary`), are each refused with that
reason.
**What is not here.** The egress reads no Object property: the stitcher's timing (live-ads
§14.3) is for a sink that presents by it, and is in no receipt. It does not hold a composed
track to its recipe: that is live-ads §14.4's, over the room's records. A Period is not marked
period-connected (the timing model's §12) in this revision; each ingredient shares the one
header, so a player may treat the Periods as connected. The DASH timing model's §13.6.3 would
have an update remove no reference to a segment that starts at or before its earliest removal
point (the availability window's end plus the minimum update period); adding a Period shortens
the one before inside that point, by the part hold-back's margin above. Meeting it would hold a
composed owner's DASH segments back by a whole segment more, which this revision does not. A client that refreshes its MPD less often than the minimum update period keeps one past its
validity and may ask an earlier Period's address for a later Period's first segment, which is
answered 404 (*Each Period's own addresses*); a lead covering a refresh a second apart would put
DASH availability past a latency target of 1.5 s, which this revision does not.
## 5. Low-latency HLS
### 5.1 The media playlist
Lines end in LF. A duration is written in seconds with three decimals, and a date as
`YYYY-MM-DDTHH:MM:SS.mmmZ`, the anchor plus `ms` of a start. With `T` the target duration,
`P` the part target and `W` the window:
1. `#EXTM3U`; `#EXT-X-VERSION:9`; `#EXT-X-TARGETDURATION:<T>`;
`#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=<3·P>,CAN-SKIP-UNTIL=<6·T>`;
`#EXT-X-PART-INF:PART-TARGET=<P>`; `#EXT-X-MEDIA-SEQUENCE:<n>`, `n` the number of the
first segment listed; `#EXT-X-MAP:URI="init.mp4"`. (A protected track's playlist differs
here and before each key period's first segment, as §4.5 says.)
2. In a delta update that skips segments (§5.5), `#EXT-X-SKIP:SKIPPED-SEGMENTS=<k>`.
3. The room's date ranges (§7), each on a line.
4. The last `W` closed segments, less those skipped, each as:
`#EXT-X-PROGRAM-DATE-TIME:<its start>`; then for a segment, its parts where they are
still listed (below), `#EXTINF:<duration>,` and `s/<g>.m4s`; for a gap segment,
`#EXT-X-GAP`, `#EXTINF:<duration>,` and `gap/<start in ticks>.m4s`, at which nothing is
served.
5. The open group, where there is one: `#EXT-X-PROGRAM-DATE-TIME:<its start>` and its
parts.
6. `#EXT-X-PRELOAD-HINT:TYPE=PART,URI="p/<g>.<o>.m4s"` (§5.3).
A part is `#EXT-X-PART:DURATION=<d>[,INDEPENDENT=YES],URI="p/<g>.<o>.m4s"`, `INDEPENDENT`
for an independent object (§3.1). A closed segment's parts stay listed while its end is less
than three target durations, in milliseconds, before the end of the last held part of the
open group, or of the last closed segment when no group is open.
A live playlist has no `EXT-X-PLAYLIST-TYPE` and, in this revision, no `EXT-X-ENDLIST`: an
egress does not turn a room into an on-demand presentation.
**The first playlist.** A media playlist is answered once its track has closed a segment. Before
then it would list no Media Segment, only the open group's parts, and a client that begins its
playback at a hold-back from the playlist's end (the HLS second edition's `HOLD-BACK` and
`PART-HOLD-BACK`) has no segment to begin at: one open-source player reports such a playlist
empty, another asks for a segment no tag names. So a request then, with or without delivery
directives, waits for the first segment to close, as a blocking reload waits (§5.4), and is
answered by §5.4 from then on; after three target durations it **MAY** be answered 503.
### 5.2 Parts
A part is one object, and so one or more CMAF chunks that start where the part before
ended. A publisher that wants parts near the part target cuts its objects so; an egress
never re-cuts media, since the receipts are over the publisher's objects. The HLS second
edition asks that a part other than a segment's last be at least 85% of the part target:
an egress does not withhold a shorter one, and a publisher **SHOULD** meet it.
### 5.3 The preload hint
The hint names the next part of the open group; with no open group, part 0 of the group
after the highest group closed. (A track with no held part has no playlist, so no hint, and
no request for a part of it waits.) A request for the part the hint
names is held until that object is held, then answered with its bytes, all at once: no byte
of a part is sent before the part is held. Where the hinted object is then withheld, or its
group closes without it, the request is answered 404, as the HLS second edition has a server
answer for a hinted resource it will not publish.
### 5.4 Blocking playlist reload
A request for a media playlist with `_HLS_msn=<M>`, and optionally `_HLS_part=<N>`, is
answered when the playlist holds what it names. With `L` the number of the last closed
segment:
- `M ≤ L` and no `N`: at once. With `N`: at once when segment `M` is no longer kept, or has
more than `N` parts; otherwise (the request is then for part 0 of the next segment) when
`M < L` or the open group has a part.
- `M = L + 1`: only with `N`, once the open group has more than `N` parts. Without `N` the
request waits for segment `M` to close.
- Before any segment has closed, the request waits for the first to close (§5.1), and is then
answered by the rules above.
A request is refused at once with 400, by the state when it arrives, when `_HLS_part` comes
without `_HLS_msn`; when either is not a decimal integer; when `M > L + 2` (`L` being
`F − 1` before any segment has closed, and `F` 0 before any part is held); or when
`M = L + 1` and `N` is at least the open group's parts plus the advance part limit, which is
`⌈3000 / P⌉` for a part target under a second and 3 otherwise. A request that has waited
three target durations **MAY** be answered 503. A refusal comes before the 404 of a track
with no held part. An egress **MUST NOT** answer a blocking
request with a playlist before the playlist holds what the request names
(`blocking_answered_early`).
### 5.5 Delta updates
A request with `_HLS_skip=YES` or `_HLS_skip=v2` is answered with a delta update: of the
closed segments the playlist would list, the leading ones whose end is at least the skip
boundary, six target durations in milliseconds, before the end of the last closed segment
are left out, with their tags, and `EXT-X-SKIP` states how many. Where none is that old the
playlist is the whole one. `EXT-X-MEDIA-SEQUENCE` is unchanged: it is the number of the
first segment the whole playlist lists. Date ranges are never skipped (the playlist does not
state `CAN-SKIP-DATERANGES`), so `v2` is answered as `YES`.
### 5.6 The multivariant playlist
Served once every track of the owner has closed a segment (§5.1), and waited for once every
track has a held part: `#EXTM3U`, `#EXT-X-VERSION:9`,
`#EXT-X-INDEPENDENT-SEGMENTS`; with video and audio, one
`#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="<track>",DEFAULT=YES,AUTOSELECT=YES,URI="<track>/media.m3u8"`
per audio track, then per video track
`#EXT-X-STREAM-INF:BANDWIDTH=<b>,CODECS="<c>",RESOLUTION=<w>x<h>,FRAME-RATE=<f>,AUDIO="audio"`
and `<track>/media.m3u8`, `b` the video's bit rate plus the first audio track's and `c`
the video's codec, a comma and that audio track's; with video alone, the same without
`AUDIO` and with the video's own values; with audio alone, per track
`#EXT-X-STREAM-INF:BANDWIDTH=<b>,CODECS="<c>"` and its playlist. Codecs, bit rates,
dimensions and frame rate are the catalog's (MSF §5), which the publisher states and nothing
signs.
## 6. Low-latency DASH
### 6.1 The MPD
The MPD addresses segments by number, the group's, at a fixed duration, so it is served
only while every track of the owner is on a grid (§6.2), and is answered 404 otherwise. It
is one document for every viewer, and it changes only when the room's breaks do (§7), a
protected track's key period is taken (§4.5), or a composed track begins an ingredient (§4.6):
each gives such an owner's MPD a Period for each, and what follows is said of each:
- `MPD` of `type` `dynamic`, `profiles`
`urn:mpeg:dash:profile:isoff-live:2011,http://www.dashif.org/guidelines/low-latency-live-v5`,
`availabilityStartTime` the anchor, `publishTime` the instant every track was first on its
grid, `minimumUpdatePeriod` the target duration, `timeShiftBufferDepth` the least over the
tracks of `ms` of the window's segments, `minBufferTime` the part target; for an owner with a
composed track (§4.6, *A Period ahead of its first segment*), `availabilityStartTime` the anchor
plus three part targets and `minimumUpdatePeriod` the part target;
- a `ServiceDescription` whose `Latency` `target` is the latency target, in milliseconds;
- one `Period`, `id` 0, starting at 0, holding the room's event streams (§7) and one
`AdaptationSet` per track, with a `ProducerReferenceTime` of type `encoder` whose
`wallClockTime` is the anchor at `presentationTime` 0 (in a later Period, §4.5, §4.6, §6.2,
the anchor plus the Period's `start` as written, at the `presentationTimeOffset` of that
Period's template, 0 where it has none: so the reference is the wall-clock time of the MPD
timeline's 0 by ETSI TS 103 285 clause 10.20.4's reading, which takes `presentationTime` less
the offset, over the timescale, from the Period's start, in every Period), a
`SegmentTemplate` with the track's `timescale`, `duration` the grid's, `startNumber` 0,
`initialization` `<track>/init.mp4` and `media` `<track>/s/$Number$.m4s`, and one
`Representation` with the catalog's values;
- on each `SegmentTemplate`, `availabilityTimeOffset`, in seconds with three decimals: `ms`
of the grid's duration less `ms` of the first part of the first whole segment, with
`availabilityTimeComplete="false"`; neither is written where that is 0;
- a `UTCTiming` of scheme `urn:mpeg:dash:utc:http-iso:2014`, value `time`: the egress's
clock, as an ISO 8601 date, at `time` beside the MPD.
`live-conformance/http-egress/` holds the bytes; the corpus's traces are their definition
where this list and they differ in white space.
### 6.2 The grid
A track is on a grid of duration `D`, in ticks, from the first whole segment it closes: `D`
is that segment's duration. It stays on the grid while every segment it closes, before and
after, starts at its group's number times `D` and lasts at most `D`, and every whole one
lasts `D`. A track that leaves the grid does not return, and the MPD is then answered 404:
its template would name bytes at the wrong times. HLS needs no grid and goes on.
A segment that is not whole is served under its number with the parts that were held; a
DASH client finds less media in it than the template states.
**Missing segments.** Groups of a track that left no segment between two that did (missed,
withheld whole, or never sent) are the track's **missing segments**, known when the next group's
first part is held. The DASH timing model's §14 has a service publish no Period with missing
segments, and start a Period without the affected representation instead. So the MPD has a
Period at the first missing segment's start, without the track's `AdaptationSet`, and one at the
next segment's start, with it again: each is a boundary of §4.5's list, and each Period holds the
tracks that are not missing there. A Period no track is in holds no `AdaptationSet`. A boundary
that is not a whole number of segments of a track the Period holds leaves that track no first
segment there, and the MPD is answered 404 from then on, as for a key period. The MPD's
`publishTime` is then the later of §6.1's instant and the instant the missing segments were
known. A segment cut short is not missing: it is served as above.
### 6.3 Segments as they grow
A request for the segment of the open group is answered 200 with a body sent in pieces
(HTTP/1.1 chunked transfer coding, or its equivalent): at once the parts held so far, then
each part as it is held, whole, and the body ends when the group closes. A request for the
segment of the group after the highest group closed, before that group has a held part, waits
the same way; a track with no held part and no closed group has no such group, and a request
for a segment of it is answered 404.
An egress **MUST NOT** send a byte of a segment that is not a held part's
(`served_before_held`), and **MUST NOT** end the body while the group is open
(`segment_answered_open`). A closed segment is answered with its parts concatenated.
So a segment is available from the moment its first part is held, which is what
`availabilityTimeOffset` states: on a grid whose segments last `D` and whose first part
lasts `p`, the offset is `D − p`.
### 6.4 The DVB-DASH form
`dvb.mpd` is the MPD of §6.1 in the form of the DVB profile of MPEG-DASH (ETSI TS 103 285
V1.4.1), so that a player built to that profile, a broadcaster's or an HbbTV terminal's own,
plays the room's tracks from it. It is served while the MPD is, and is answered 404 besides
where its grids, its Periods or its size are outside TS 103 285 clause 4.5: a grid under 960 ms
or over 15 s, more than 64 Periods or 16 tracks, or more than 256 Kbytes. It is the MPD but
for these, which decision D56 states the reasons for and which `dvb-dash-conformance/` holds a
verifier and planted faults of:
- `MPD@profiles` is `urn:dvb:dash:profile:dvb-dash:2014,urn:dvb:dash:profile:dvb-dash:isoff-ext-live:2014`,
then the MPD's own two (clauses 4.1, 4.2.5, 11.1);
- a video `AdaptationSet` carries `par`, its width and height in lowest terms, after
`mimeType` (clause 4.4);
- in every `AdaptationSet`, after its protection descriptors (§4.5) and before its
`ProducerReferenceTime`,
`<SupplementalProperty schemeIdUri="tag:transaction.science,2026:wai/receipts" value="<track>/"/>`,
the directory, relative to the MPD, under which each of the track's parts has its receipt
index and its run and the track's group receipts and segments' parts lists are (§4.1, §8);
then, where the set states an `availabilityTimeOffset` (§6.1),
`<SupplementalProperty schemeIdUri="urn:dvb:dash:lowlatency:critical:2019" value="true"/>`
(clause 11.18.1);
- where a Period has more than one video track, or more than one audio track, the first of
them carries `<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>` after its
`ProducerReferenceTime` (clauses 4.2.2, 6.1.2).
Its statement (§8.5) rides in the response's `WAI-Statement` header, as the MPD's does. No
descriptor of WAI's in it is an `EssentialProperty`: a DVB-DASH player ignores the parent of
one whose scheme it does not know (clause 10.10), which would hide the track, and ignores a
`SupplementalProperty` it does not know and nothing else. The tracks' codecs are the
catalog's: a player that does not decode one ignores its set (clause 10.1, NOTE 1).
## 7. Breaks
A room's breaks are [live-ads](live-ads.md)'s. An egress that holds the room's events and
its ad policy writes, into every media playlist, the tags live-ads §5 gives the schedule,
in the order live-ads §5.4 states, each on a line of its own after the header; and into the
MPD's Period, before its adaptation sets, the event streams of live-ads §6.3. It writes them
byte for byte and adds none: the asset list a break names, the decision and the receipts are
live-ads's, at the decider. An egress that does not hold them writes none, and its playlists
are the program's alone.
## 8. Receipts beside the media
### 8.1 A part's receipt index
`<part>.receipts` is a receipt index in the form of [http-delivery](http-delivery.md) §8.2:
`object` is the part's content hash (jwp-receipts §2.1), `delivery` holds one item, the `WAI_RECEIPT` property
(moq-streaming-format §5) built from the object's receipt and its group's link hash, and the
object receipt itself, and `claims` is empty. A client checks the part as §8.2 there checks
a delivery item, over the bytes it was sent. A receipt that states a figure above 0 with no
class in a labelled leaf has no valid property, and such a part's index is answered 404.
### 8.2 Group receipts
`p/rcpt/<h>.json` is the group receipt in the form of http-delivery §8.1, for every group
whose receipts the egress took up and still keeps. A group's `parent_receipt_hash` names its
parent's file; a client that joins anchors its chain at the first group it verifies
(jwp-receipts §3 step 3).
### 8.3 A segment's parts
`<segment>.parts` is, in RFC 8785's form,
`{"kind":"wai-egress-segment","group":<g>,"parts":[{"uri":"../p/<g>.<o>.m4s","n":<bytes>,"object":"<content hash>"}…]}`:
the parts whose bytes, in order, are the segment's. A client that fetched a segment splits
it at these lengths and checks each piece as §8.1's part. Nothing signs the list: a piece
that does not hash to a part whose receipts verify is not verified, whatever the list says.
### 8.4 A part's run
`<part>.running` is served for a held part whose position its group's run covers, for as
long as the part is kept: in RFC 8785's form,
`{"kind":"wai-egress-running","group":<g>,"object":<o>,"records":["<record>"…]}`, the event
records of the run's receipts that state positions 0 to `o`, in position order, each in
lowercase hexadecimal: the last of them is the one that states `o`, which in the compact form
may state positions after it. A client that knows the room, the owner's key, and the track's
anchor digest and receipt alias checks each record as §3.5 reads one, at the Location `(g, x)`
the run takes it to, `x` the run's length before it plus the objects it states less one (for
`WRR1`s, `(g, 0)` to `(g, o)` in turn), takes each in turn into a run as §3.5 does (each taken,
none ignored), requires that no record follow the one that states `o`, and requires its part's
bytes to hash to the content hash the run states at `o`. That is the owner's signed word that these
bytes are object `o` of group `g`.
While a group is open a part held on its run has no receipt index (`<part>.receipts` is
answered 404): the group receipt it would name does not exist yet. Once the group's receipts
are taken up, the index is §8.1's, and a client checks the part against either. A part of a
group whose receipts never came, or contradicted its run, keeps its run alone.
### 8.5 Statements beside playlists and MPDs
Every media playlist, multivariant playlist and MPD an egress answers 200 (§4.4, §5, §6) is
served with a statement ([http-delivery](http-delivery.md) §8.10) in the response's
`WAI-Statement` header. It binds the bytes of that response, under the owner's key, to what
they are: the resource, its tracks and their aliases, when it was answered, and for a media
playlist where it stands. A client holds a playlist to it before acting on it, as it holds
each part to its receipts (§8.1), and refuses one changed on its way, one of another track,
one older than one it holds, and a delta update that does not follow from what it holds. The
group receipts, the runs and every other file of §8 are unchanged, and so is every byte of a
playlist and an MPD: a statement is beside them.
**The delegation.** An egress holds no key of the owner's (§13). The owner names a
**statement key** for it in a `meta` event of its own ([live-events](live-events.md) §4.2),
with no anchor, signed as every event of the owner's is (live-events §3.2). Its payload,
integers little-endian as an event's are:
| offset | size | field | meaning |
|---|---|---|---|
| 0 | 4 | `magic` | `WMD1` (ASCII) |
| 4 | 1 | `version` | 1 in this revision |
| 5 | 32 | `key` | the statement key |
| 37 | 8 | `not_before_ms` | the earliest a statement under it is issued, in milliseconds since 1970 |
| 45 | 8 | `not_after_ms` | the latest, at or after `not_before_ms` |
| 53 | 1 | `n` | how many aliases follow, 1 to 64 |
| 54 | 8·`n` | `aliases` | each track's `wai_receipt_alias` that statements under it may state, strictly ascending |
The owner carries it on its events track (live-events §8.1), where an egress that reads the
track finds the one that names its key, or hands it to the egress otherwise; the egress
carries it in every statement, or names it there by its digest and serves it beside the
playlists (*Delegation by reference*, below), so a client needs nothing more than the room's
id and the owner's key, which it holds already to read a run (§8.4). The aliases are the catalog's
`wai_receipt_alias`, which an egress reads to subscribe: an egress states each track it serves
with its alias to the party that signs the delegation for the owner (the reference gateway's
`Gateway::tracks`), so that party reads the catalog no second time. A delegation names
aliases, not a catalog: the catalog is unsigned and is published again (a key period, §4.5),
so a delegation named by one catalog's digest would lapse with that catalog, while an alias
names a track's receipts for as long as the owner keeps it. Under a delegation, the statement
key states playlists and nothing else: it signs no receipt and no event, and a receipt verifies
only under the owner's key.
**Delegation by reference.** A statement carries the delegation (version 1) or names it by its
digest (version 2, http-delivery §8.10): the digest live-events §3.2 names an event by. An
egress that names it serves it at `d/<h>` under the owner's base (§4.1), `h` the digest in
lowercase hexadecimal: the record byte for byte, with `Cache-Control: no-cache` and an `ETag`
that is the record's digest, quoted, for as long as it signs under it and a statement's lifetime
after. A request whose `If-None-Match` names that tag is answered 304. A client **resolves** a
name: it uses the delegation of that digest it holds, where it last resolved the name no longer
ago on its clock than the statement's lifetime (its `expires_ms` less its `issued_ms`), and
otherwise fetches the address, conditionally where it holds the record. The answer resolves the
name where it is 200 and its body, of at most 1200 bytes, is an event record whose digest is
the one named, or 304 to a client that holds that record; the client then holds the record,
resolved at that instant. Where the body is the owner's revocation of that digest
(*Revocation*, below), the name is revoked. Any other answer (another status, a record of
another digest, a revocation that is not the owner's, none) leaves the name **unresolved**:
the client refuses the statement (`delegation_unresolved`) and acts on nothing of its body, so
a delegation is never taken on the egress's word. So a client fetches each delegation once and
keeps it, by its digest, fetching it again only a statement's lifetime after it last resolved
it, which is how a revocation reaches it; it holds at least 64 names at once. A request at a
delegation's address is not the egress's state, and a trace (§10) records none, as it records
no statement.
**Revocation.** The owner withdraws a delegation before its interval ends with another `meta`
event of its own, with no anchor, signed as every event of the owner's is, whose payload is:
| offset | size | field | meaning |
|---|---|---|---|
| 0 | 4 | `magic` | `WMR1` (ASCII) |
| 4 | 1 | `version` | 1 in this revision |
| 5 | 32 | `delegation` | the digest (live-events §3.2) of the delegation it revokes |
It reaches the egress as a delegation does, on the owner's events track or handed to it. From
then on the egress signs nothing under that delegation, and answers the delegation's address
with the revocation in its place. A client answered there with the owner's revocation (its
record reads as an event of kind `meta` with no anchor, of the room, whose signature verifies,
by the owner's key, whose payload is the 37 bytes above, naming the digest it asked for) holds
the digest revoked from then on, and refuses every statement under that delegation, whether
the statement names it or carries it (`delegation_revoked`). A revocation by another key, or of
another digest, revokes nothing.
**What a statement states.** For a request for `name` (the path under the owner, with no
query) answered 200, the egress signs a statement whose `sha256` is that of the body,
`issued_ms` its clock (§4.3) when it answered, `expires_ms` that plus six target durations,
`delegation` the record, `package` `null`, and the rest as the body states them:
- `form`: `hls-media` for `<track>/media.m3u8`, `hls-multivariant` for `master.m3u8` and
`wai.m3u8`, and `dash-mpd` for `manifest.mpd`, `dvb.mpd` and `wai.mpd`;
- `tracks`: for a media playlist, its track, with `first` and `last` the lowest and highest
group its segments (`s/<g>.m4s`) and its parts (an `EXT-X-PART`'s `URI="p/<g>.<o>.m4s"`)
name, both `null` where it names none; for a multivariant playlist, each track that an
`EXT-X-MEDIA`'s `URI` or a URI line names as `<track>/media.m3u8`, and for an MPD each that
a `media` attribute names as `<track>/s/$Number$.m4s` or, a composed track's Period's own
address (§4.6), `<track>/period/<i>/$Number$.m4s`, in order of first appearance, with
`first` and `last` `null`; each with its `wai_receipt_alias` as `alias`;
- `position`, for a media playlist, and `null` otherwise: `msn` its `EXT-X-MEDIA-SEQUENCE`,
`dsn` its `EXT-X-DISCONTINUITY-SEQUENCE` (0 where it has none), and `next` the next part it
promises, `[m, p]`: `m` the media sequence number after its last segment, and `p` how many
`EXT-X-PART` lines follow that segment's URI;
- `skip`, for a delta update (§5.5) that leaves `k` segments out, and `null` otherwise:
`skipped` `k`, and `entries` the SHA-256 of the **entries** of the segments it leaves out,
numbers `msn` to `msn + k − 1`, each followed by a line feed, as the whole playlist the
egress would have answered at that instant writes them.
A playlist is read for these line by line, split at LF, a CR anywhere not reading: its first
line `#EXTM3U`; `EXT-X-MEDIA-SEQUENCE` exactly once; `EXT-X-DISCONTINUITY-SEQUENCE` and
`EXT-X-SKIP` at most once each, the latter before any segment. Each line that is not empty and
does not begin with `#` is a segment's URI, and needs an `EXTINF` after the segment before it.
The segments are numbered from `EXT-X-MEDIA-SEQUENCE` plus what `EXT-X-SKIP` leaves out, and a
segment's **entry** is its number, its `EXTINF` duration (the text before the comma), its URI
and the last `EXT-X-PROGRAM-DATE-TIME` value since the segment before it (empty where there is
none), joined by single spaces. A multivariant playlist's first line is `#EXTM3U`. A body that
does not read so has no statement, and is refused by its client.
So what a statement states is read from the bytes it signs, as a client reads them again, and
an egress pays one signature a response. A trace (§10) records no header: the statements are
held by the egress's clients, not by the trace's check.
**What a client holds.** A client of an egress keeps the room's id and the owner's key; each
track's alias, as the page gives it or as the first statement it accepts that names the track
states it; for each resource, the issue time and position of the last statement it accepted;
for each track the entries of the segments the playlists it accepted list, at least the
last 8192; each delegation it resolved by digest, with when it last resolved it; and each
digest it was answered the owner's revocation of. Before acting on a playlist or an MPD answered 200, it checks the statement beside
it as http-delivery §8.10 does, with an egress's shape (`package` `null`, a time, a
delegation, and an alias on every track) and its step 5 in full, and then the rest, refusing
at the first that fails:
1. the delegation: where the statement names it (version 2), the name is not one the client
holds revoked (`delegation_revoked`) and resolves (`delegation_unresolved`), both as
*Delegation by reference* says; its record, carried or resolved, reads as an event
(live-events §3.3) of kind `meta` with no anchor, of the room, its signature verifies
(live-events §3.2), its `principal` is the owner's key, and its payload reads as above
(`delegation_invalid`); and the client holds no revocation of its digest
(`delegation_revoked`);
2. its signature verifies, by its own `signer` (`statement_signature`); and that `signer` is the
key the delegation names, each of its aliases is one the delegation lists, and its
`issued_ms` is within the delegation's interval (`statement_not_delegated`). The signature
comes first, so that a statement by another key than the delegated one is refused as not
delegated only once its signature verified, and a forged one by any key for its signature;
3. each track's alias is the one the client holds for the track, where it holds one
(`statement_other_track`);
4. the client's clock is at most `expires_ms` (`statement_expired`);
5. the body reads as its name's form, and the statement's `form`, `tracks`, `position` and
the count its `skip` states are what the body states (`statement_inconsistent`);
6. where the client accepted a statement for the same name, this one was issued no earlier,
and, for a media playlist, its `msn`, its `dsn` and its `next` are none below that one's,
`next` compared number first (`statement_replayed`);
7. for a delta update, the client holds the entries of every segment it leaves out, and their
SHA-256 is `skip`'s `entries` (`delta_not_following`);
8. no segment the playlist lists has another entry than the one the client holds for its
number (`statement_rebound`): a closed segment's duration, URI and date do not change;
9. for a blocking reload (§5.4) asked with `_HLS_msn=M`, `next` is at least `[M, N + 1]` with
`_HLS_part=N`, and at least `[M + 1, 0]` without (`statement_short`).
A statement that passes is accepted and held: its time and position for its name, each alias
it gave, and the entries its playlist lists. One refused changes nothing, and the client acts
on nothing of its body.
| reason | the playlist or MPD, by its statement |
|---|---|
| `statement_not_served` | is served with no statement |
| `statement_malformed` | is served with one that does not read, or is not an egress's |
| `statement_other_resource` | is served with a statement of another resource |
| `statement_mismatch` | is not the bytes its statement is over |
| `delegation_unresolved` | is stated under a delegation named by a digest the client could not resolve |
| `delegation_invalid` | is stated under a delegation that is not the owner's in this room |
| `delegation_revoked` | is stated under a delegation the owner revoked |
| `statement_not_delegated` | is stated by a key, for a track or at a time the delegation does not cover |
| `statement_signature` | has a statement whose signature does not verify |
| `statement_other_track` | is stated under another alias than its track's |
| `statement_expired` | has a statement that has expired |
| `statement_inconsistent` | has a statement that states other than its bytes do |
| `statement_replayed` | is stated as older than one the client accepted |
| `delta_not_following` | is a delta update that leaves out other segments than the client holds |
| `statement_rebound` | lists a segment the client holds otherwise |
| `statement_short` | answers a blocking reload without what it named |
**What it binds.** An accepted statement binds, under the owner's key through its delegation:
these bytes; that they are this resource of these tracks under these aliases; when they were
answered; and, for a playlist, what it lists and where it stands. A client that acts only on
what it accepted never acts on a playlist changed on its way, of another track, or older than
one it acted on, and the delta updates it accepts are, segment for segment, the whole
playlists the egress would have answered. It binds no more than the egress put in: what it
lists and when are the egress's, held to its arrivals by its trace (§10); its dates are its
clock's (§4.3); and its codecs, rates and headers are the catalog's (§13).
## 9. Latency
Nothing in this text makes a latency true. A figure is a measurement, and states: the host;
the path (loopback, a network); what was timed, from which instant to which; and the
parameters of §4.2. Three intervals are defined so that figures can be compared:
- **sent to listed**: from the instant a publisher has handed a part's object to its
transport to the instant an HTTP client that was waiting on a blocking reload (§5.4) has the
last byte of the playlist that lists it;
- **sent to served**: from that instant to the instant a client that was waiting on the
hinted part (§5.3) has its last byte;
- **edge to presentation**: by a player's own report, how far the position it presents is
behind the end of the last part the playlist or the MPD offers.
**Receipts bound it.** A part is held only once its group's receipts have arrived (§3.2),
and a group's receipts are signed over all its objects (jwp-receipts §2.2). A publisher that
knows a group's objects before it sends the first (content made ahead, or an encoder that
looks a group ahead) publishes the receipts first, as moq-streaming-format §4.1 asks, and
each part is held as it arrives. A publisher that signs a group when it ends publishes the
receipts last, and every part of the group is then held at the group's end: the group's
duration is added to its first part's latency. A measurement states which of the two the
publisher did. A publisher that signs at a group's end and also signs running receipts
(§3.5) has each part held when its running receipt arrives: the wait is then for one
signature and its carriage on the events track, not for the group. In the compact form a part
is held when the receipt that states it arrives, so the first of `count` objects waits
`count` − 1 objects' durations more, and the last none. A measurement states whether running
receipts were used, and in which form and count on each track. A composed track's group's first part waits too for the
receipts of the group before it, which close that group (§3.4, §4.6): one signature more, at
each group's start.
The `ServiceDescription` target and `PART-HOLD-BACK` are what an egress asks of a player,
not what a viewer gets.
## 10. The trace
### 10.1 Form
A trace is one JSON object: `kind` `"wai-egress-trace"`, `version` 1, the four parameters
(`target_duration_s`, `part_target_ms`, `window`, `latency_target_ms`), `tracks`, and
`entries`. A track is `name`, `publisher` (the owner's key), `receipt_alias`, `init` (the
header), `codecs`, `bandwidth`, `width`, `height`, `frame_rate` and `sample_rate`; a track's
name is `video`, `audio` or `screen`, `/` and a decimal layer, and no two are alike. Bytes
and keys are lowercase hexadecimal. An envelope track (§4.4) has five members more:
`packaging`, `"wai"`; `capabilities`, the catalog's `wai_capabilities`; `tempo`, `null` or
`objects_per_720s` and `start_ms`; `frame_stream`, a boolean; and `layer`; and its `init`
and `codecs` are empty.
The trace of an egress that takes running receipts
(§3.5) also has `room`, the room's id, and on every track `anchor`, the track's digest as an
anchor names it; a trace has both or neither. A protected track (§4.5) has one member more, `protection`: its
first key period, an object of exactly `index`, `from_group` and `entries`, the catalog's
entries as JSON; its `init` is that period's header. An envelope track has none. A composed
track (§4.6) has `composed`, `true`, and neither `packaging` nor `protection`.
An entry is `t`, the egress's clock in milliseconds since 1970, which never goes back, and
exactly one event:
| event | members | what happened |
|---|---|---|
| `receipts` | `track`, `group`, `bytes` | the group's receipts arrived |
| `object` | `track`, `group`, `object`, `bytes` | an object arrived |
| `end` | `track`, `group` | the publisher ended the group |
| `running` | `track`, `group`, `object`, `bytes` | an event record arrived on the owner's events track as a running receipt of `track`, its timeline record indexed at that Location; only in a trace with a `room` |
| `key_period` | `track`, `index`, `from_group`, `entries`, `init` | a later catalog stated a key period of `track` (§4.5): its entries, and its header |
| `ads` | `hls` (date-range lines), `dash` (event streams) | the room's breaks, as §7 writes them from here on |
| `request` | `id`, `target` | a request arrived; `target` is the path under the owner, with its query |
| `emit` | `id`, `bytes` | that many more bytes of the request's body were sent, before its end |
| `response` | `id`, `status`, `body`; or `id`, `status`, `sha256`, `length` | the request was answered: its status and its whole body, or the SHA-256 of that body and its length in bytes |
**A response recorded by its digest.** A `response` records its body whole, or by `sha256`, the
SHA-256 of the whole body (64 lowercase hexadecimal digits), and `length`, its length in bytes,
in place of `body`; it has the one set of members or the other, and a trace may hold both forms.
The check gives the state's body again (§10.2), so a body's digest is as checkable as its bytes,
and an egress that keeps a trace of a large audience records its responses so: the trace then
holds every arrival and every request, as before, and no copy of any body. The arrivals are what
the check replays, and are bounded by what the room published, not by its audience.
### 10.2 Checking
A checker replays the arrivals through §3 and requires each answer to be the one this text
gives for the state at the answer's entry. It refuses the trace at the first entry that
fails, with the first of these that applies:
1. the trace or the entry does not read as §10.1 (`trace_malformed`);
2. an `emit` or a `response` for an `id` no open request has (`response_unasked`);
3. an `emit` after which more bytes were sent for the request than the state has to send:
the whole body where the state answers, a growing segment's held parts where it waits,
nothing otherwise (`served_before_held`);
4. for a `response`:
1. where the request was one §5.4 refuses when it arrived, any status but 400
(`status_mismatch`);
2. a media playlist answered 200, recorded with its body, that lists a part that was
withheld (`withheld_listed`), a part that is not held, or a segment that is not closed
(`listed_not_held`);
3. where the state waits and the request has waited three target durations, 503 with an
empty body (a `length` of 0, by its digest) passes;
4. a media playlist request that the state still waits on (`blocking_answered_early`);
5. a part or segment answered 200: a withheld part (`withheld_served`); a part the state
waits on, or whose object has arrived and is neither held nor withheld: where its
group's receipts have not arrived and its run holds a receipt and does not cover the
part (`served_uncovered`), and otherwise (`served_before_held`); a segment at a Period's
address (§4.6) whose group is closed or open and not of that Period
(`segment_other_period`); a segment the state waits on (`segment_answered_open`);
6. a status that is not the state's, 200 or 404 (`status_mismatch`);
7. a body that is not the state's: of a playlist (`playlist_mismatch`), of the MPD
(`manifest_mismatch`), of a receipt index, a group receipt, a parts list or a run
(`sidecar_mismatch`), of anything else (`bytes_mismatch`); but, for a part of an
envelope track whose last box is not an `mdat` holding exactly the held envelope,
`sample_not_alone` where that payload begins with the envelope and holds more, and
`sample_mismatch` otherwise; and, for a media playlist and for `manifest.mpd` and
`dvb.mpd`, the reason of rule 5 where one applies, then of rule 6. A response recorded by
its digest is the state's body when its `sha256` is that body's SHA-256 and its `length`
that body's length; one that is not is refused by the reason this rule names for its
resource (`playlist_mismatch`, `manifest_mismatch`, `sidecar_mismatch` or
`bytes_mismatch`) and by no other, since the reasons that read a body (rule 2's, this
rule's for an envelope track's part, and rules 5 and 6) have none to read.
5. A media playlist or an MPD, recorded with its body, whose body is not the state's is read for
what it states of protection (§4.5), and refused with the first of these that applies.
For a media playlist, read line by line: the **key lines in force** at a line are those
of the last unbroken run of `#EXT-X-KEY:` lines before it, and the **header in force** the
last `#EXT-X-MAP:` line before it. A clear track's playlist with a key line whose method
is not `NONE`: `clear_stated_protected`. A protected track's, at the first part
(`#EXT-X-PART:` with a `URI="p/<g>.`) or segment (`s/<g>.m4s`) of a group `g` that has a
key period, in line order: no key line in force with a method other than `NONE`
(`protection_omitted`); key lines in force that are not exactly that period's
(`protection_not_catalogs`); a header in force that is not that period's `EXT-X-MAP` line
(`key_period_header`).
For the MPD, where the state has one: for each Period the state lists, in order, a
served Period of that `id` is looked for (the text from ` <Period ` at a line's start),
and where there is none and a track is protected, `key_period_header`; then for each
track in order, in the served `AdaptationSet` of that Period whose `SegmentTemplate`'s
`media` is the track's, with its lines that begin with `<ContentProtection`: for a clear
track, any such line (`clear_stated_protected`); for a protected one, none
(`protection_omitted`), lines that are not exactly the key period's descriptors
(`protection_not_catalogs`), an `initialization` that is not the key period's header
(`key_period_header`).
6. A media playlist of a composed track, recorded with its body, whose body is not the state's is
read for its marks
(§4.6): for each group it lists, in line order, at its first part or its segment's URI,
whether an `#EXT-X-DISCONTINUITY` line came after the group listed before it. Where the
state's playlist marks a group that the served one lists unmarked: `discontinuity_missing`.
`manifest.mpd` or `dvb.mpd` whose body is not the state's is read, after rule 5, by its
Periods (the text from ` <Period ` at a line's start, by `id`) and in each its
`AdaptationSet`s, each the track its `SegmentTemplate`'s `media` names (less
`/s/$Number$.m4s` or `/period/<i>/$Number$.m4s`): in a Period of the state's, a set of a track
the state's Period leaves out (`period_missing_segments`, §6.2); then a set whose
`ProducerReferenceTime`'s `wallClockTime` or `presentationTime` is not the state's set's for
that track (`producer_reference_off`, §6.1). Then, for an owner with a composed track: where
its Periods, each read as its `start` and, for each `SegmentTemplate` in it, its
`startNumber`, its `presentationTimeOffset` (0 where it has none) and its `media`, are not the
state's: `period_off_boundary`.
A trace that passes shows that, for what it records, the egress listed and served only what
it held, when it held it. It shows nothing of what the trace leaves out: a trace is the
egress's own account, and a client's protection is §8, not the trace. A trace whose responses are
recorded by their digests shows the same of what the egress listed and served, and refuses the
same answers, some under the reason of their resource rather than a finer one.
## 11. Reasons
| reason | the trace |
|---|---|
| `trace_malformed` | does not read |
| `response_unasked` | answers a request that was not made |
| `status_mismatch` | gives a status the state does not |
| `listed_not_held` | lists a part or a segment before the egress held it |
| `withheld_listed` | lists a withheld object |
| `blocking_answered_early` | answers a blocking reload before the playlist held what it named |
| `playlist_mismatch` | gives playlist bytes the state does not |
| `manifest_mismatch` | gives MPD bytes the state does not |
| `withheld_served` | serves a withheld object |
| `served_before_held` | sends bytes the egress did not yet hold |
| `served_uncovered` | serves a part its group's run does not cover |
| `segment_answered_open` | ends a segment while its group was open |
| `bytes_mismatch` | gives media bytes that are not the held object's |
| `sidecar_mismatch` | gives receipts, a parts list or a run the state does not |
| `sample_mismatch` | gives, for a part of an envelope track, a sample that is not the held envelope |
| `sample_not_alone` | gives, for a part of an envelope track, an `mdat` that holds the envelope and more |
| `protection_omitted` | gives a playlist or an MPD that lists a protected track's media and does not state its protection |
| `protection_not_catalogs` | states a scheme, a KID, a system, a `pssh` or a licence URI that is not the catalog's for that key period |
| `key_period_header` | lists a key period's media under a header that is not that period's |
| `clear_stated_protected` | states protection for a clear track |
| `discontinuity_missing` | lists a segment of a composed track that begins a period, with no discontinuity before it |
| `period_off_boundary` | gives an MPD of composed tracks whose Periods do not begin where an ingredient does, or do not name their segments at their own addresses |
| `segment_other_period` | serves a composed track's segment at the address of a Period it is not of |
| `period_missing_segments` | gives an MPD that lists a track in a Period over the track's missing segments |
| `producer_reference_off` | gives an MPD whose Period states a producer reference that is not the anchor at the MPD timeline's 0 |
## 12. Conformance
An egress conforms when a trace of what it received and answered passes §10.2, whatever it
was sent. A checker conforms when it reaches the outcome
[`live-conformance/http-egress/vectors.json`](../live-conformance/http-egress/) states for
every case: `ok`, or the reason; and, for a case with no `base`, the objects it states were
withheld, each with its reason, how often a chain was anchored again, and, for a trace with
a `room`, each running receipt refused (`running_refused`: its track, its group, the
position its record was indexed at and the reason, in the tracks' order and then the order
they were refused in; a group whose receipts contradicted its run is among them, at the
run's length); and, for a trace in which a track states `protection`, each key period
refused (`key_periods_refused`: its track, its index and the reason, in the tracks' order and
then the order they were refused in). The objects withheld are stated by track, in the trace's
order, then by group and object. The corpus has a stitched session's composed video and sound,
a creative's two Groups in the programme's; the same from a stitcher that signs running receipts,
and with a Group's receipts arriving after the next Group's first part; a composed track on its
grid that misses a Group and is placed again, the MPD then holding a Period without either track;
and one off its grid that misses a Group (§4.6); a request at an earlier Period's address for a
later Period's first segment, made before that segment was held and answered 404 after; and a
plain track whose group is skipped, left out of the MPD's Period over it (§6.2). It also has
`digests`, the first trace with every response recorded by its digest (§10.1), and faults planted
in it: digests of a playlist, an MPD, a receipt index and a part that are not the state's, a part's
length that is not its body's, the hinted part answered by the digest of its object before the
object arrived, a digest that does not read, and a response that records its body and a digest. It
has `running-compact`, a publisher's running receipts in the compact form (§3.5), the video's one
object each and the sound's two, and beside it a compact receipt never sent (the next skips one),
one whose root is not over its leaves, one that restates an object, three that do not read, and a
group's receipts that contradict a compact run; a part served before the receipt that states it,
and a part's run left without that receipt. It has `running-joined`, an egress that joins a
running room inside its video's and its sound's group, its events track's latest group bringing
the running receipts from object 2 on and a fetch of the group before it the video's first two
after (*Joining inside a group*, §3.5), and beside it the same with a later group whose first
receipt skips object 0; a part of the group joined inside served before its receipts, and a run
taken from inside that group served beside it. A
case with `base`
is that trace with its `edits` applied in order: `set` replaces a member of an entry's
event; `move` takes the entry at `from` out and puts it at `to`, with the time of the entry
then at `to`; `insert` puts an entry at `at`; `remove` drops one.
The corpus's media is synthetic: headers and chunks with the boxes §3.1 reads and a few
payload bytes, signed with a fixed test key. `wai_live_egress_vectors gen` writes it and
`verify` regenerates it; `http_egress_verify.py` is written from this text, shares no code
with them, and also requires every mutated copy of the corpus to fail and each of its own
checks, dropped in turn, to be caught.
A client conforms to §8.5 when it reaches the outcome
[`live-conformance/egress-statements/vectors.json`](../live-conformance/egress-statements/)
states for every case: `ok`, or the reason of §8.5's table. A session there is what a client
of the reference egress was answered, each playlist and MPD with its statement; a case runs a
session whole, or that many steps of it and then one step that plants a fault. The session
`by-reference` names the delegation by digest, and each step at which its client fetched the
delegation carries what the address answered; its faults are names the address does not
resolve (nothing, 404, another delegation of the owner's, a 304 for nothing held, a copy held
past a statement's lifetime and not renewed), a name of another delegation of the owner's, a
name of a delegation another key signed, and the owner's revocation, and another key's,
answered when the client renews its copy.
`wai_live_egress_vectors statements gen` writes it and `statements verify` regenerates it;
`egress_statements_verify.py` is written from §8.5 and http-delivery §8.10, shares no code with
them, and also requires every mutated copy of the corpus to fail and each of its own checks,
dropped in turn, to be caught.
The gateway's own tests run it against a relay with planted faults, check every part and
segment it serves against its receipts over HTTP, check its trace, and state the latency
they measured on the host they ran on (§9).
## 13. Security and privacy considerations
- **An egress is a subscriber, and no more trusted.** It cannot make an object the
publisher did not sign pass a client's check (§8); it can withhold, delay and reorder,
which a gap, a late part or a failed check shows.
- **The catalog is unsigned.** Codecs, bit rates and the header come from the catalog. A
header that is not the publisher's makes the media fail to decode; it does not make other
media pass, since each part is checked against a signed receipt.
- **The anchor is the egress's clock**, and a wrong one moves every date and every
availability time together. It is not evidence of when anything was made.
- **A stitcher's events track carries every session's runs.** The running receipts of a
stitched session's composed tracks are events of the stitcher's chain (§3.5, *A composed
track's run*), on its events track, which every reader of the stitcher's events receives:
each names its session's composed track by its anchor digest, which is over the session key
but does not state it, and states each composed Object's content hash, a creative's among
them. So a reader of the stitcher's events can tell, of a session it knows only by that
digest, which creatives it was given and when; it cannot tell whose the session is. A
deployment that does not want that served pays a group of latency instead: its stitcher signs
no running receipts.
- **A running receipt is the owner's word and no more.** A part held on a run was held to
a digest the owner signed for that object of that group, and a client checks the same
(§8.4). An owner that signs a run and then other receipts for the group has signed both:
the egress refuses the second, keeps the records, and reports it. A relay that withholds
running receipts makes parts wait for their group's receipts; it cannot make a part pass.
A running receipt replayed from the owner's earlier events states the object it always
stated: its anchor names the track, the group and the object.
- **A protected track's protection is the catalog's word.** The catalog is unsigned, and
an egress states what it states: a relay or an egress that names another KID, another
licence URI or a clear header can make a player ask for the wrong key or for none, and
the player then fails to decode. It cannot make protected media play without its key,
and it cannot make other media pass a client's check of the receipts (§8), which are over
the protected bytes. An egress holds no key and can release none; who is released a key
is [key-release](key-release.md)'s, at the key holder.
- **A statement is the egress's word, made the owner's by a delegation.** It binds a
playlist's bytes and makes them attributable: a path that changes them, swaps another
track's in, or replays an older one is refused (§8.5). It does not make what the egress
wrote true: an egress that holds a delegation can still withhold, delay and state its own
clock's dates, as it can without one, and a client then refuses nothing it could not refuse
before. A statement key that is lost can state playlists until its delegation's interval
ends: an owner keeps that interval short and delegates again. A revocation shortens that for
the clients that reach the egress: each fetches the delegation again within a statement's
lifetime and is answered the revocation. It does not shorten it against a path that answers
the delegation's address with the delegation itself, which the path can serve as the egress
did, so the interval stays the bound a revocation cannot lift.
- **A delegation by reference is the owner's word, fetched.** A client acts on no statement
whose delegation it has not resolved to a record whose digest is the one named and whose
signature is the owner's: an egress or a path that serves another record, nothing, or a
revocation that is not the owner's has the statement refused, which withholds, and makes
nothing pass. A client whose clock is more
than six target durations ahead of the egress's refuses every statement as expired; it may
take the egress's `time` (§6.1) as its clock.
- **Withheld bytes stay withheld.** An egress keeps no path, by part, by segment or by
range, that reaches an object's bytes before §3.2 has held it.
- **Blocking requests hold connections.** An egress bounds how many it holds and for how
long (three target durations), and refuses at once those §5.4 names.
- **Who fetches.** A request to an egress shows it the viewer's address and what the viewer
watches, as any HTTP delivery does. This text carries no viewer identity and asks for
none.