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]>
1590 lines
111 KiB
Markdown
1590 lines
111 KiB
Markdown
# 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 `&`.
|
||
|
||
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.
|