open-standards/wai/extensions/live-http-egress.md
Transaction Science 771130f924
Some checks failed
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 29s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 31s
wai-pack / wai-pack · does this change touch it (push) Successful in 19s
wai-ledger service / does this change touch it (push) Successful in 20s
wai-sink-swift native sink package / does this change touch it (push) Successful in 13s
wai-sink-android native sink library / does this change touch it (push) Successful in 16s
WAI Studio / does this change touch it (push) Successful in 11s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m54s
wai-quantum studio and desktop / desktop app · windows (push) Waiting to run
wai-app and Create / wai-app · does this change touch it (push) Successful in 2m59s
WAI operation catalogue / does this change touch it (push) Successful in 2m59s
wai-fleet service / does this change touch it (push) Successful in 2m56s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 3m10s
wai-audit collector and player integration / does this change touch it (push) Successful in 3m0s
wai-quantum for JavaScript / does this change touch it (push) Successful in 2m48s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 3m5s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 14s
WAI examples / WAI examples · does this change touch them (push) Successful in 9s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m18s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m3s
wai-pack / wai-pack · linux (push) Successful in 4m17s
wai-ledger service / service · linux (push) Successful in 4m13s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 7m46s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 3m36s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 4m36s
wai-audit collector and player integration / collector · linux (push) Successful in 4m41s
wai-quantum studio and desktop / studio · browser (push) Successful in 6m45s
wai-fleet service / service · linux (push) Successful in 5m45s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 7m40s
wai-quantum for JavaScript / javascript · linux (push) Successful in 6m53s
wai-pack / wai-pack · the floor through a media prober (push) Successful in 3m50s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 9m19s
wai-pack / wai-pack · linux-arm64 (push) Successful in 5m13s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 12m18s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 10m22s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 11m29s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 11m14s
WAI Studio / Studio in WebKit · linux (push) Successful in 15m11s
wai-pack / wai-pack · dependency licences (push) Successful in 5m31s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m33s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 7m5s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 14m46s
WAI examples / WAI examples · portable · linux (push) Successful in 14m27s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 13m56s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 48s
wai-ledger service / service · macos (push) Successful in 1m1s
wai-pack / wai-pack · independent verifier (Python) (push) Successful in 22m16s
wai-pack / wai-pack · macos (push) Successful in 3m4s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m34s
wai-fleet service / service · macos (push) Successful in 43s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 5m54s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 56s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m58s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 6m52s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 13m25s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 14m27s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 17m58s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 19m19s
Live HTTP egress: a protected track's playlists and MPD state its protection, from the catalog alone
live-http-egress §4.5 (new). An egress served a track under Common Encryption
(moq-streaming-format §4.9) with playlists that did not say so. It now states the protection,
with no key: it reads the catalog's entries for the track and the header's own sample entry,
and reads no sample.

- A key period is the catalog's entries for a track, its wai_key_period and its header. It
  reads when the header gives a scheme (cenc or cbcs) and a KID, is the track's first in all
  but its protection, and every entry states that scheme and KID with a system ID. A track
  whose header states protection its catalog does not is not served (protection_unstated),
  and neither is a group before the first key period. A later key period is taken, or
  refused with one of four reasons and reported.
- HLS: at each key period's first segment, EXT-X-DISCONTINUITY where the header changes, an
  EXT-X-KEY for each entry (SAMPLE-AES for cbcs, SAMPLE-AES-CTR for cenc, the key format the
  entry's system ID) and an EXT-X-MAP naming that period's header, init.<index>.mp4; the
  playlist carries EXT-X-DISCONTINUITY-SEQUENCE.
- DASH: a Period for each key period; in each, a protected track's set carries a
  ContentProtection of urn:mpeg:dash:mp4protection:2011 with the scheme and cenc:default_KID,
  and one for each entry's system ID with its pssh and licence URI; its SegmentTemplate names
  the period's header and numbers its segments from the Period's first.
- The trace gains a track's protection and the key_period event; its check gains
  protection_omitted, protection_not_catalogs, key_period_header and clear_stated_protected.
- Reference: wai-rs live_egress::protection. The corpus gains eight traces and nineteen fault
  cases, 114 in all; every earlier trace and case keeps its bytes. http_egress_verify.py, the
  second implementation, reaches all 114, fails 68 mutated copies and has each of its 110
  checks caught when dropped. The corpus regenerates byte for byte on x86_64 and aarch64.
- Planted faults, each refused with its own reason by both: a playlist and an MPD that omit
  the protection of a protected track, a KID that is not the catalog's, a key period's first
  segment without its new header, a clear track stated as protected.
- joule-moq-http reads the protection from the owner's catalog and each later catalog's key
  period from the catalog track. tests/protected.rs sends a room under cbcs with three key
  periods through a relay: every part served verifies against the publisher's receipts,
  which are over the protected bytes, and decrypts under its own period's key alone, with
  keys the test holds and the gateway does not; the gateway's traces pass both checkers.
- wai-pack's cmsf module gains Live, a track protected object by object, and entry.

Not done, and stated: an egress does not check that an object is encrypted, having no key;
the multivariant playlist carries no session key; a break after a key period's first boundary
is carried in the playlists and not in the MPD; any system's licence exchange is that system's.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-11 09:33:49 -05:00

63 KiB
Raw Permalink Blame History

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 §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 gives them (§7). 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). 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/, and its second implementation is live-conformance/http_egress_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). 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); 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 §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: 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: 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.

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 §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_ids 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 §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);
  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)

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;
  • 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 §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.

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 (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 the running receipts taken for it, in position order; it covers positions 0 to its length less one, and its root is its last receipt's root, 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 and whose run is not broken, is, with n the run's length and i the anchor's object:

  • where i < n: ignored when its four digests are those of the run's receipt at i (a group of an events track repeats records at its start, live-events §8.2); otherwise the owner has stated one object twice (running_restated);
  • where i > n: it skips an object (running_skips_object);
  • where 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 its own leaf (running_root_mismatch);
  • otherwise taken: it is the run's next.

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, or of a group whose run is broken, is ignored. An egress that joins inside a group takes no run for it, since the run starts at object 0.

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, and an owner whose tracks together send more than 50 objects a second needs a policy that says so.

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)
<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)
wai.m3u8, wai.mpd the multivariant playlist and the MPD of the envelope tracks (§4.4)

<g>, <o> and <i> are decimal with no leading zero. A name that is none of these, 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. Two 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).

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 §3), and its receipts are over the envelopes. An egress MAY serve such a track as Mode F (http-delivery §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:

  1. 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);
  2. 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, native-sink), 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 §4.9): the samples of each object are encrypted, the catalog states the protection as CMSF §4 has it, and the track's key changes with its key period, each stated by a catalog of its own. An egress serves such a track with no key. It holds each object as §3.2 holds any, against receipts that are over the protected bytes; it reads nothing of a sample, of a senc or of a pssh; and it writes, from the catalog alone, what a player needs to ask for a key: which scheme, which KID, which systems, and which header, for every segment it lists. It names systems by their system IDs, as the catalog does, and takes part in no system's licence exchange.

What a header states. A header states protection when its one sample entry (the first entry of the stsd in the stbl in the minf of the mdia) is encv or enca. It then gives a scheme and a KID when that entry, after its own fields (78 bytes of a visual entry, 28 of an audio one), holds exactly one sinf, with one schm, whose scheme type is the scheme, and one schi holding one tenc, whose default KID (the 16 bytes after its version, flags and four more bytes) is the KID. A header with no such path, or another sample entry, is clear.

A key period. What a catalog states of a track's key period is: the period's index and from_group, the track's wai_key_period (0 and 0 where a protected track's catalog carries none); the entries, the contentProtections entries the track's contentProtectionRefIDs name, in that order; and the period's header, which the track's initRef names. It reads when all of these hold:

  1. the header gives a scheme and a KID, and the scheme is cenc or cbcs;
  2. the header reads (§3.1) with the track ID, timescale, handler and trex defaults of the track's first header;
  3. there is at least one entry, and every entry is an object whose scheme is the header's, whose defaultKID is an array of UUID strings that holds the header's KID, and whose drmSystem is an object with a systemID that is a UUID string;
  4. where a drmSystem has a pssh, it is a non-empty string of Base64 in the standard alphabet, padded; where it has a laURL, that is an object whose url, and whose type where it has one, are strings of printable ASCII with no space, ", < or >.

An egress carries a pssh and a licence URI through as the catalog gives them and reads neither; it does not hold a pssh to the KID.

A track's first key period is the one its catalog states when the egress reads it. A track whose header states protection and whose catalog names no entry for it, or states a key period that does not read, is not served: every object that arrives on it is withheld protection_unstated, its receipts are not read, and every resource under it is answered 404. So an egress never offers protected bytes under a playlist that calls them clear. An object of a group before the first key period's from_group is withheld protection_unstated too, and its group's receipts are not read. A track whose header is clear and whose catalog names no entry for it is a clear track, served as §3 to §6 say.

Later key periods. A catalog published later states the track's next key period (moq-streaming-format §4.9 has it published before the period's first group). The egress takes it, and from then on a group is of the last key period taken whose from_group is at most its number. It is refused, and changes nothing, at the first of these:

reason the key period
key_period_not_protected is of a track that has no first key period: a clear track, an envelope track, or one that is not served
key_period_malformed does not read
key_period_out_of_order has an index or a from_group that is not above the last taken period's, or another scheme
key_period_late has a from_group at or below a group the egress has closed or holds a part of

A key period whose index, first group, entries and header are those of one already taken is the same catalog again and is ignored. An egress counts what it refused and reports it. A refused period's groups stay of the period before: where the publisher did change the key, its players find segments their key does not open, and the report says why. An egress cannot see that from the bytes.

The headers. <track>/init.<i>.mp4 is the header of the key period whose index is i, for every period taken, and 404 before. A protected track has no <track>/init.mp4: it is answered 404, so a client that ignores the protection finds no header to start with.

In a media playlist (§5.1) of a protected track:

  • the header lines end with #EXT-X-DISCONTINUITY-SEQUENCE:<n> in place of #EXT-X-MAP:URI="init.mp4", n the number of segments before the first one the whole playlist lists, from the first the egress held, that began a key period: a segment begins a key period when the segment before it on the track is of another. A gap segment is of the key period of the segment before it;
  • before the #EXT-X-PROGRAM-DATE-TIME of a closed segment, and of the open group, that begins a key period: #EXT-X-DISCONTINUITY; and there, and before the first segment the playlist writes (the first after an EXT-X-SKIP, in a delta update), the period's key lines, one for each entry in order, and #EXT-X-MAP:URI="init.<i>.mp4".

A key line is #EXT-X-KEY:METHOD=<m>,URI="<u>",KEYFORMAT="urn:uuid:<system ID>",KEYFORMATVERSIONS="1": m is SAMPLE-AES for cbcs and SAMPLE-AES-CTR for cenc; the system ID is in lowercase; and u is data:text/plain;base64, and the entry's pssh where it has one, else its licence URI where it has one, else urn:uuid: and the KID as a UUID. The HLS second edition defines the identity key format alone, and CMSF maps no entry to a key line: this mapping is this text's. No IV is written: under cbcs the constant IV is in the header's tenc. A client that knows none of the key formats ignores every line, as the HLS second edition has it do, and does not play the track.

In the MPD (§6.1), where a track of the owner is protected:

  • the MPD element declares xmlns:cenc="urn:mpeg:cenc:2013" and xmlns:dashif="https://dashif.org/CPS" after its own namespace, and its publishTime is the later of §6.1's instant and the instant a key period after a track's first was last taken;
  • there is a Period for each boundary: 0, and each instant a key period after its track's first begins, which is its from_group times its track's grid duration. A Period's id is its place among the boundaries, from 0, and its start the boundary, as PT<seconds with three decimals>S (the first is PT0S). A boundary that is not a whole number of some track's segments from 0 leaves that track no first segment there, and the MPD is answered 404 from then on. A Period that ends at or before the start of the first segment every track still keeps is left out; the others keep their id. The room's event streams (§7) are written in the Period whose id is 0 and in no other: this revision carries a break after the first boundary in the playlists alone;
  • in each Period a track's SegmentTemplate has, as startNumber, the number of the segment that starts at the boundary and, where that is above 0, presentationTimeOffset, that segment's start in ticks, after it; its initialization is <track>/init.<i>.mp4, i the index of the key period that segment's group is of, or of the track's first where the Period starts before it;
  • a protected track's AdaptationSet begins, before its ProducerReferenceTime, with that key period's descriptors, each on a line: <ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="<scheme>" cenc:default_KID="<the KID as a UUID>"/>, then for each entry in order <ContentProtection schemeIdUri="urn:uuid:<system ID>"> holding <cenc:pssh>, the entry's pssh, where it has one, and <dashif:Laurl licenseType="<type>">, its licence URI, where it has one (licenseType only where the laURL has a type), and written as an empty element where it has neither. & is written &amp;.

A clear track beside a protected one has no descriptor and no key line, and its segments are numbered through the Periods as a protected track's are.

What is not here. An egress does not check that an object is encrypted, that it is under the key period's key, or that a pssh names the KID: it has no key, and moq-streaming-format §4.9's verification, which has, is the publisher's and a verifier's. The multivariant playlist carries no session key. No EXT-X-PRELOAD-HINT names a header. Output protection and any system's licence format are that system's.

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.

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, with F the number the first will have: at once when M < F; with N, when M = F and the open group has more than N parts.

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 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), or a protected track's key period is taken (§4.5, which gives such an owner's MPD a Period for each key period 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;
  • 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, 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, and none where a group left no segment.

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.

7. Breaks

A room's breaks are live-ads'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 §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 for positions 0 to o, each in lowercase hexadecimal. 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 Locations (g, 0) to (g, o), takes them in order into a run as §3.5 does, and requires its part's bytes to hash to the last record's content_hash. 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.

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. A measurement states whether running receipts were used.

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.

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 the request was answered: its status and its whole body

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 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 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 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, the reason of rule 5 where one applies.
  5. A media playlist or an MPD 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).

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.

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

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 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). 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.

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 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's, at the key holder.
  • 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.