open-standards/wai/extensions/live-recording.md
Transaction Science 673554abf0 live-recording: a record with the link record's magic, WLK1, is not a clip record
The link corpus refuses a part that bears the clip record's magic; this adds the reverse.
The live-recording corpus gains the clip link_record_magic: chunk 4's clip record beginning
WLK1 and signed under it, so the magic is its one defect, refused malformed at the clip
(§5.2, which now names WLK1 among the other magics). wai_live_record_vectors writes it;
the README lists it.

live_record_verify.py requires the same refusal, malformed at the clip, of a copy of each
accepted clip given WLK1, so a verifier that reads past the magic to the signature fails.
The CHANGELOG entry for WLK1 says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 05:19:53 -05:00

53 KiB
Raw Permalink Blame History

WAI Extension: Live Recording

Status: Draft. Registers the record wai.live.recording: a finished room session as one WAI package, and clips of it that prove what they are. The package is a directory of files and its listing, as a package of packaging §11 is; its components are the room (its descriptor, grants and key-log head, live-rooms), its events (each principal's chain whole, live-events §8.6), one component for each participant's take (capture-binding), and, optionally, the composite; and a signed recording manifest names each component by its digest, with the signer, the room and the session's span on the room clock. A recording verifier gives complete and publishable for each purpose their verdicts. A clip is a span of one take, or one window of several takes aligned on the room clock; its signed clip record carries, for each take, the descriptors of the chunks it covers and a range proof that they are leaves of a commitment the device made while it captured, so a verifier holding the clip, the take's commitments, the device's key and the room descriptor knows the clip's bytes are exactly those bytes of that take, unedited, without the take. The HLS and LL-CMAF presentation of a recording (§7) and the addressing of its chunks for a MoQ FETCH (§8.1) are mappings. A relay's DVR window of a room's tracks (§8.2 to §8.5) keeps them, with their receipts, for a viewer to seek back into by FETCH. No new transport and no new container. The reference implementation is the live_record module of wai-rs (feature live_record); the corpus is live-conformance/recordings/, and its second implementation is live-conformance/live_record_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: RFC 6962 §2.1 and RFC 9162 §2.1.1, §2.1.3 and §2.1.4 (the Merkle tree hash, inclusion proofs and their verification); RFC 8216 §3.3, §4.3.2.2, §4.3.2.5, §4.3.2.6, §4.3.3.1, §4.3.3.5 and §7; draft-pantos-hls-rfc8216bis-22 §3.1.2, §4.4.3.7, §4.4.3.8, §4.4.4.5 and §4.4.4.9; and draft-ietf-moq-transport-22 (MOQT) §2.4.1, §3.2, §3.2.1, §8.2, §8.7, §9.11 and §9.20.9. The two drafts are Internet-Drafts, and a section number of either in this text is that revision's.

1. Scope

In scope: the recording package, its files and its components (§3); the recording manifest and recording verification, with the verdicts of complete and publishable (§4); the clip record, the range proof it carries, clip verification, and cutting a clip by chunks, by bytes or by a window of the room clock (§5); a clip across takes, its time correspondence and its bound (§6); the mapping of a recording onto HLS and LL-CMAF (§7); and the addressing of a recording's chunks for a MoQ FETCH, with the DVR window a relay keeps of a room's tracks and serves to FETCH (§8).

Out of scope: a transport; how a relay lays its DVR window out on disk; a muxer for a master that is not CMAF; the composition itself, which wai.live.compose defines (live-compose §8, a later extension); and what a party does with a verdict.

Relationship to the core spec: wai.live.recording is a record (SPEC §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.

2. Terms

  • Room, room clock, principal, event, chain, grant, room key: as in live-events §2 and live-rooms §2.
  • Take, device, master, chunk, descriptor, take tree, commitment, offset, purpose: as in capture-binding §2 and §4. I and J are the room's commit interval and jitter in microseconds (capture-binding §2).
  • Session span: the room-clock instants a recording covers and its signer vouches for, from to to, both included.
  • Package: the directory of files a recording is (§3).
  • Component: a part of a package named by a kind, an id and a sha256 (§3.2).
  • Head: the commitment of a take a clip's proof is made against: one of the take's take_commit events (capture-binding §5.1), its size and its root.
  • Part (of a clip): the span of one take a clip covers (§5.1).
  • Cutter: whoever makes a clip and signs its record.

Integers are little-endian. A digest is SHA-256 (FIPS 180-4), 32 bytes. Ed25519 is RFC 8032's, verified strictly (SPEC §7 Signatures).

3. The recording package

3.1 Files and the listing

A recording is a directory of these files and no other:

recording.wrm                          the recording manifest (§4)
room/descriptor.wrd                    the room descriptor
room/grant-<k>.wrg                     each room grant
room/key-log.json                      the head of the room authority's key log
events/<principal>.wev                 each principal's chain
takes/<take>/manifest.wtm              a take's manifest
takes/<take>/chunk-<index>.bin         each chunk of the take the package holds
takes/<take>/receipt-<k>.wur           the store's upload receipts
takes/<take>/consent-<k>.wcn           the consent records
composite/receipt-<digest>.wcr         the composite's receipts, optionally (§3.5)
composite/…                            and the objects they name
LISTING

LISTING is the listing (SPEC §3.1) of every other file, and its SHA-256 is the package digest, as a package of packaging §11 has its own. Every path is one a listing can carry. <principal> and <take> are a key and a take id as 64 lowercase hexadecimal digits, <index> a chunk's index as ten decimal digits, and <k> a position as four decimal digits from 0000.

3.2 Components

A package has these components, each the files below one directory:

kind component id directory
1 the room room room/
2 the events events events/
3 a take the take id, 64 lowercase hexadecimal digits takes/<take>/
4 the composite composite composite/

A component's sha256 is the SHA-256 of the listing (SPEC §3.1) of its files, each by its path below the component's directory. That is SPEC §7.1's component identity for a component of several files: an id, of SPEC §7.1's form, and the sha256 of its bytes, here serialised as the listing that SPEC §3.1 gives a set of several files, as a pin in the form wai.prior.set covers one. No other per-component digest is defined.

3.3 The room and the events

The room component holds descriptor.wrd, the room descriptor (live-rooms §3); one grant-<k>.wrg for each room grant (live-rooms §4), numbered from 0000 without a gap in the order a room verifier is given them (live-rooms §9.1); and key-log.json, the head of the room authority's key log that the room verifier is given (live-rooms §5), when the descriptor names an origin. It holds no other file.

The events component holds one file for each principal whose chain the recording holds, named by the principal's key: its chain whole, its records concatenated in its stream order (live-events §8.6). A record's length follows from its own fields (live-events §3.1), so the file splits into records with no framing; a file that does not split into records that read (live-events §3.3), each of that principal, is not a chain. A room verifier is given the chains in the order of their files' paths, ascending order of the principals' keys, and that order is the chain index by which live-rooms §9.2's merged order breaks ties.

3.4 A take

A take component's directory is takes/<take>/, its id the take id (capture-binding §3.2). It holds manifest.wtm, the take manifest (capture-binding §7); one chunk-<index>.bin for each chunk of the master the package holds, the chunk's bytes; the store's upload receipts in their order as receipt-<k>.wur (capture-binding §6.2); and the consent records as consent-<k>.wcn (capture-binding §8.1). It holds no other file.

The chunk files are the take's parts (capture-binding §6.1), given to the take verifier in the order of their indexes. A chunk the store never received has no file: the package holds what was uploaded, and the take's complete verdict says whether that is every committed chunk. The master is its chunks concatenated in index order (capture-binding §4.1).

3.5 The composite

A recording MAY hold the room's composite: the objects of a composed track, each with the composition receipt WCR1 its compositor signed for it, which wai.live.compose defines (live-compose §8). The component composite holds each receipt as receipt-<digest>.wcr, <digest> the receipt's digest (SHA-256 over its bytes before its signature, live-compose §8.1) in 64 lowercase hexadecimal digits, and the objects the receipts name beside them. The recording manifest names the component by its sha256 like any other, so the listing binds each receipt's bytes and its name its digest. A recording verifier of this revision checks that digest and reads nothing in the component: verifying a composition receipt is live-compose §8.3's, a later extension, and this extension depends on nothing of it but the name and digest of its record.

4. The recording manifest

4.1 Layout

When the session is over, the room authority, or a principal that held the record power over the whole span (§4.3), signs a recording manifest, WRM1:

field size meaning
magic 4 WRM1 (ASCII)
version 1 1 in this revision
room 32 the room id
signer 32 the key that signs the manifest
from 8 u64: the session span's first instant, room clock in microseconds
to 8 u64: its last instant
claims 1 the claims the manifest makes: bit 0 complete
purposes 1 the purposes it claims every take publishable for (capture-binding §2)
region 2 ISO 3166-1 alpha-2: where the package is kept
components 2 + … a u16 count, then each component: kind (1), the length of its id (1), the id, and its sha256 (32)
sig 64 Ed25519 by signer

The digest of a manifest is SHA-256 over its bytes before sig; its signature is over the 19 bytes wai:live-recording (ASCII) and 0x01, followed by the bytes the digest covers.

4.2 Reading and rules

A reader reads exactly the bytes it is given, as live-events §3.3 reads a record with WRM1 for WEV1 (truncated, malformed, unknown_version), and refuses a manifest that breaks any of these rules as recording_manifest_invalid:

  • claims sets no bit above bit 0, and purposes none above bit 3; from is at most to; region is two capitals A–Z;
  • the first component is the room (kind 1, id room) and the second the events (kind 2, id events); then come the takes, each of kind 3 with an id of 64 lowercase hexadecimal digits, in strictly ascending order of id; then, last, at most one composite (kind 4, id composite); and no other component.

It then refuses one whose signature does not verify as bad_key or bad_signature. Each id the rules admit is of SPEC §7.1's form, and the order makes the manifest of one package one sequence of bytes.

4.3 Who signs

The room authority (the descriptor's authority, live-rooms §3.1) may sign any span. Any other signer MUST hold the record power (live-rooms §3.3) at every instant of the span, from from to to inclusive.

A principal holds the power at room-clock instant t when, in the room state replayed to t (live-rooms §9.3: every join, grant_exchange and leave of it that the room verifier accepted with its t at or below t, in the merged order), it is present, and its grant in effect has the record power, is not revoked (live-rooms §5), and covers t: nbf ≤ ⌊(t0 + t) / 10^6⌋ < exp. So the grant took effect, by a join or a grant_exchange, at or before from, and nothing takes the power away before to: no leave, no grant_exchange to a grant without it, and no expiry.

A signer that does not hold it throughout is refused recording_signer_not_permitted, and the verifier states the first instant of the span at which it does not hold it: from when it held no grant with the power there; otherwise the t of the leave or the grant_exchange that took the power away, or the first instant its grant does not cover. One code with the instant, not one code per cause: the power over the span is one property, each way it lapses is already a record of the room (a lifecycle event, or a grant's window) that the instant points at, and the instant is what a signer needs to know to seal a span it did hold the power over. A span that fails several ways fails first where the instant says.

The record power is the power to start and stop a recording, and sealing the session is the end of that act. Held only at the end, it would let a principal granted it just before the end seal a session it never held the power to record. A recorder that gained the power partway through signs a manifest whose span starts no earlier than its grant took effect; the package still holds the room's chains whole (§3.3), and §4.4 step 6 holds every take it holds to that span. A device needs no record power to commit to its take, and a device whose grant has none cannot seal the room's recording.

A revocation in the key log is not bounded by a time (live-rooms §5): the room verifier refuses every use of a revoked grant, so a head that revokes the signer's grant refuses the recording at the room (§4.4 step 4), before this rule is reached.

4.4 Verification

A recording verifier is given a package's files by path, and an evaluation instant at in seconds since 1970-01-01T00:00:00Z, which it gives each take verifier (capture-binding §9.1). It runs these steps and stops at the first refusal, which it reports with the reason and where:

  1. The listing. LISTING is present and is the listing of every other file: otherwise listing_mismatch, at listing.
  2. The manifest. recording.wrm is present, otherwise recording_manifest_invalid, and it reads (§4.2); at manifest.
  3. The components. For each component of the manifest, in its order: its directory holds a file, otherwise component_invalid, and the SHA-256 of its listing is its sha256, otherwise component_digest_mismatch; at that component. Then every file is LISTING, recording.wrm, or a file below a component's directory: otherwise component_unlisted, at listing.
  4. The room. The room component holds the files of §3.3 and no other, and each file of the events component is a chain of the principal it names (§3.3): otherwise component_invalid, at that component. The room verifier (live-rooms §9) is given the descriptor, the grants, the key-log head (or none) and the chains: a refusal is the recording's, at room with the room verifier's own place. A gap is not a refusal: the steps go on, and §4.5 says what a gap fails. The manifest's room is the room id: otherwise wrong_room, at manifest.
  5. The recording policy: the room's recording policy (live-rooms §3.7) has a mode other than 0, otherwise recording_not_permitted, capture-binding §12's code, at manifest: a room that is never recorded has no recording, takes or no takes. The signer (§4.3): otherwise recording_signer_not_permitted, at signer with the first instant of the span at which it does not hold the record power. The region: where the room's recording policy pins regions (live-rooms §3.7), region is one of them, otherwise store_outside_region, capture-binding §6.3's code, at manifest.
  6. The takes. For each take component, in the manifest's order: it holds the files of §3.4 and no other, otherwise component_invalid; the take verifier (capture-binding §9) is given the room as in step 4, the take manifest, the parts, the receipts, the consents and at, and its refusal is the recording's, at take with the component's index and the take verifier's own place; the take id it reads is the component's id, otherwise component_invalid; and the take lies within the span, its start (the record_start it answers) at or after from and its last held commitment at or before to, otherwise outside_span; at that component. The records before from or after to that the package holds are the room's chains whole, which the room verifier replays the room from; the manifest states nothing of them.
  7. The verdicts (§4.5).

4.5 Verdicts and outcome

complete holds when the recording holds every take of the session that starts within the span, each whole, and every event the room verifier was offered: the first reason below that applies.

  1. delivery_gap: the room verifier holds a gap (live-rooms §9.4).
  2. take_omitted: a take of the session whose record_start lies within the span has no component. A take of the session is the take id (capture-binding §3.2) that a record_start the room verifier accepted (live-events §4.9) names for its target, where the room holds a take_commit of that target whose payload reads (capture-binding §5.2) and names that take: a start its device answered.
  3. For each take component, in the manifest's order: its single-take verdict fails with delivery_gap, commit_chain_broken or take_not_closed, so its commitments are not one unbroken chain from the opening commitment to the closing one: that reason; or its complete verdict fails: that reason (capture-binding §9.4).

publishable for purpose p holds when every take is publishable for p: for the first take component, in the manifest's order, whose publishable for p fails, that reason.

A recording verification gives one outcome:

  • refuse: the reason and where (§4.4);
  • verdicts: the package digest; the manifest's digest; the room id; the verdicts of complete and of publishable for each of the four purposes; each take's id and verdicts (capture-binding §9.5); and whether the manifest stands: every claim it makes, and publishable for every purpose it names, holds.

5. Clips

5.1 The clip record

A clip is a span of one take, or one window of several takes aligned on the room clock (§6). A cutter signs a clip record, WCL1:

field size meaning
magic 4 WCL1 (ASCII)
version 1 1 in this revision
room 32 the room id
window_in 8 u64: the first room-clock instant the clip presents, in microseconds
window_out 8 u64: the instant it presents up to (excluded)
parts 1 + … a u8 count k, then each part as below
signer 32 the cutter's key
sig 64 Ed25519 by signer

Each part:

field size meaning
device 32 the device's key
take 32 the take id
head 32 the digest of the take_commit event whose commitment the proof is against
size 8 u64: that commitment's size
root 32 that commitment's root
first 8 u64: the index of the first chunk the part covers
count 4 u32: the number of chunks it covers
trim_in 8 u64: the bytes of the first chunk before the clip's bytes
trim_out 8 u64: the bytes of the last chunk after them
chunks 64 × count the descriptor of each chunk it covers (capture-binding §4.1), in index order
proof_len 1 the number of nodes in proof
proof 32 × proof_len the range proof (§5.3)

The digest of a clip record is SHA-256 over its bytes before sig; its signature is over the 14 bytes wai:live-clip (ASCII) and 0x01, followed by the bytes the digest covers. A part's bytes are its chunks' bytes concatenated, less trim_in bytes at the start and trim_out at the end; the clip's bytes are its parts'.

5.2 Reading and rules

A reader reads exactly the bytes it is given, as live-events §3.3 reads a record with WCL1 for WEV1 (truncated, malformed, unknown_version; another magic is malformed, live-contribution §6.2's link record, WLK1, among them), and refuses a record that breaks any of these rules as clip_record_invalid:

  • k is 1 to 16, and no take is named by two parts;
  • in each part, count is at least 1; each chunk's index is first plus its position, and each len is at least 1; proof_len is at most 128; trim_in is below the first chunk's len, and trim_out below the last chunk's; and, with one chunk, trim_in + trim_out is below its len;
  • a part with a trim that is not 0 is the clip's only part;
  • window_in is below window_out.

It then refuses one whose signature does not verify as bad_key or bad_signature.

Partial chunks. A clip whose range begins or ends inside a chunk carries that whole chunk and states the bytes it leaves out, as trim_in and trim_out. A leaf commits to a chunk's digest over all its bytes (capture-binding §4.2), so a part of a chunk can never be proved on its own: with the whole chunk the proof stays exact, and the trims say which of the proved bytes the clip presents. Refusing a range that does not fall on chunk boundaries would make a byte range of the master, or a range request, unprovable unless it happened to land on them. A trim is in bytes, which have no committed time within a chunk, so a trimmed clip's window is its chunks' span (§5.4 step 3j), and trims are a single take's: an aligned clip (§6) presents a window of time, not of bytes. A cutter that must not disclose the bytes outside the range cuts on chunk boundaries, since the trimmed bytes travel with the clip.

5.3 The range proof

A part proves its chunks by one proof over the run of leaves they are, against the head. With D[n] the take tree's leaves (capture-binding §4.2) under the head's size n, MTH RFC 9162 §2.1.1's Merkle tree hash with the take tree's node, and k the largest power of two below n, the proof of leaves a to b − 1 (0 ≤ a < b ≤ n) is:

RANGE(a, b, D[n]) = {}                                         a = 0 and b = n
                  = RANGE(a, b, D[0:k]) : MTH(D[k:n])           b ≤ k
                  = RANGE(a − k, b − k, D[k:n]) : MTH(D[0:k])   a ≥ k
                  = RANGE(a, k, D[0:k]) : RANGE(0, b − k, D[k:n])   otherwise

where : appends. A run of one leaf is never split, so RANGE(i, i + 1, D[n]) is RFC 6962 §2.1.1's PATH(i, D[n]), the inclusion proof RFC 9162 §2.1.3.1 generates, node for node. A proof of a whole tree is empty. A proof has at most 2⌈log₂ n⌉ nodes, at most 128 for any size a commitment can state, and every node of it is a node of the inclusion proof of a leaf in the run.

A verifier given the leaves L of the run (b − a of them), the size n and the nodes, used in order from the first, computes ROOT(a, b, n, L):

ROOT(a, b, n, L) = MTH(L)                                      a = 0 and b = n
                 = node(ROOT(a, b, k, L), next)                 b ≤ k
                 = node(next, ROOT(a − k, b − k, n − k, L))     a ≥ k
                 = node(ROOT(a, k, k, L[0 : k − a]),
                        ROOT(0, b − k, n − k, L[k − a :]))      otherwise

where next is the next unused node, and node is the take tree's (staged-measurement Appendix C). The proof verifies when no next is asked for past the last node, every node is used, and ROOT is the head's root. For a run of one leaf this is RFC 9162 §2.1.3.2's check of an inclusion proof, and gives its answer.

The size is the head's, which the device signed, never the cutter's: as for an inclusion proof, the same nodes can reach one root under two sizes (staged-measurement Appendix C), so a size the cutter chose would let it choose the leaves' positions. Under the head's size the recursion fixes which subtree each node stands for; the leaf and node domains differ, so no node passes for a leaf; and each leaf binds its chunk's index, capture range, length and digest (capture-binding §4.2). A proof that verifies therefore shows, short of a collision of SHA-256, that the run's descriptors are the head's leaves a to b − 1.

5.4 Verification

A clip verifier is given the room descriptor; the clip record; and, for each part in order, the device's key, the take's commitments (its take_commit events, from its opening one to the head, in the order of the take's chain), and the bytes of each chunk the part covers, each present or absent. It is not given the take manifest, any other chunk, the upload, or the room's other events. It runs these steps and stops at the first refusal, which it reports with the reason and where:

  1. The descriptor reads (live-rooms §3.4): its refusal, at descriptor. The room id is its digest, and I and J are its recording policy's (live-rooms §3.7).
  2. The record reads (§5.2), at clip. Its room is the room id, otherwise wrong_room; and the verifier is given one input for each part, otherwise clip_record_invalid; at clip.
  3. Each part, in order, steps (a) to (k):
    • (a) The key given is the part's device: otherwise clip_device_mismatch, at the part.
    • (b) Each commitment given reads (live-events §3.3), names the room (wrong_room), is the device's (clip_device_mismatch), verifies (bad_key, bad_signature), is a take_commit whose payload reads (capture-binding §5.2, commit_malformed), and names the part's take (clip_take_mismatch); at that commitment.
    • (c) The commitments are a chain (capture-binding §5.3): the first is an opening one, and each later one has the prev, first and n that follow its predecessor, which is not a closing one, and is itself not an opening one. A later one whose n is more than one above its predecessor's is delivery_gap; any other break is commit_chain_broken. Then each extends its predecessor (capture-binding §4.3), otherwise root_not_extending. At the first commitment that fails, the chain checked over all of them before any extension, as capture-binding §9.4 orders them. None given is commit_chain_broken at the part.
    • (d) The last commitment given is the head: its digest is head, and its size and root are the part's size and root. Otherwise tree_head_not_committed, at the part: a tree the take never committed proves nothing of it.
    • (e) first + count is at most size: otherwise clip_out_of_range, at the part.
    • (f) The proof (§5.3) shows the part's descriptors are the head's leaves first onward, under size: otherwise clip_not_included, at the part.
    • (g) Every commitment's offset is within J of the opening one's, o (capture-binding §9.4 single-take 6): otherwise clock_unstable, at that commitment.
    • (h) The chunks tile their capture, each start below its end and each start the previous chunk's end (capture_discontinuous), and none spans more than I (chunk_too_long) (single-take 7, 8); at the part.
    • (i) With each chunk mapped by o (capture-binding §9.3) and t the room-clock instant of the commitment that adds it (the one whose first ≤ index < size): no chunk has t + J below its mapped end (commit_early), and then none has t above its mapped end plus I plus J (commit_late) (single-take 9, 10); at the part.
    • (j) The window: with the part's span the first chunk's mapped start to the last one's mapped end, the span covers the window, the first chunk ends after window_in and the last begins before window_out; and a trimmed part's window is its span. Otherwise clip_window_invalid, at the part.
    • (k) The bytes: for each chunk in order, its bytes are given (clip_chunk_missing) and are that chunk's, their digest and length its descriptor's; bytes that are another covered chunk's are chunk_reordered, any others chunk_altered; at that chunk. Bytes given past the last chunk are chunk_extra, at the chunk past it.

Steps (b) to (k) are single-take over the span: the rules of capture-binding §9.4 single-take that hold of the chunks the clip covers, checked from the commitments up to the head. A clip does not need rule 4 (closed), since it may be cut while the take runs (§8); nor rule 5 (the manifest), since inclusion binds its chunks to the head instead; nor rule 9's clauses on the start and the stop, whose events it is not given. A take whose single-take fails only at chunks outside a span, as by a late commitment, still gives clips of that span that verify.

A clip that passes is accepted, and the verifier states: the record's digest and signer; the window; the bound (§6.2); and for each part its take, first, count, the take's offset o, its span, the window on its device's clock (§6.1), the length and SHA-256 of the bytes it presents, and its FETCH address (§8). Where the take's single-take holds, an accepted clip's descriptors are its manifest's at those indexes, since rule 5 binds the manifest's chunks to every commitment's root; the clip verifier does not need the manifest. A clip over chunks a moderation decision restricts is verified as live-moderation §8.3 has it: step (k) passes over the restricted chunks, whose descriptors the proof still covers.

5.5 Cutting a clip

A cutter holds a take's manifest, its commitments and the chunks it covers. It picks a head: the take's closing commitment, or, while the take runs, any commitment whose size covers the range. It cuts:

  • by chunks: first and count, with no trim;
  • by bytes: a byte range [x, y) of the master, with chunk i at the master's bytes from the sum of the len of the chunks before it: the chunks from the one holding byte x to the one holding byte y − 1, trim_in x less the first one's offset, and trim_out the last one's end less y;
  • by a window [w₀, w₁) of the room clock: in each take, the chunks from the one whose mapped capture holds w₀ to the one whose mapped capture holds w₁ − 1, with no trim.

A clip cut by chunks or bytes presents its chunks' span as its window. The proof is §5.3's over the manifest's descriptors up to the head's size; the cutter signs the record.

6. Across takes

6.1 Time correspondence

An aligned clip has a part for each take it spans, over one window of the room clock. Each take is placed on the room clock by its own mapping, the offset o of its opening commitment (capture-binding §9.3): a room-clock instant t in the window is the device-clock instant t − o_p in part p, in the chunk whose capture range holds it, at its position in that chunk and in the master (capture-binding §10). Two parts p and q correspond at t: device instant d in p is d + o_p − o_q in q. The verifier states each part's window on its device's clock, [window_in − o_p, window_out − o_p).

6.2 The bound

A part's mapping is held, over every commitment up to its head, to within J of the offset the device measured at that commitment (step 3g), and each chunk it covers was committed within [−J, I + J] of its mapped end on the room clock (step 3i). The clip states its bound, bound_us: J for a clip of one take, its mapping's to the room clock; and 2J for an aligned clip, between any two of its parts, each strays by J from the room clock as its device measured it. A sink that presents the parts of an aligned clip together aligns them within that bound of what the devices measured. The measurement itself, and each commitment's t, are their authors' statements (live-events §11), held by the room's other records as capture-binding §14 says.

7. HLS and LL-CMAF

This section is a mapping: what a standard HLS client plays of a recording, with no new format. Where a take's master is CMAF (ISO/IEC 23000-19), cut as capture-binding §4.1 cuts it, chunk 0 is the CMAF header and the first CMAF segment, and each later chunk one CMAF segment, so the take's chunk files are already an HLS presentation of fragmented MPEG-4 segments (RFC 8216 §3.3; draft-pantos-hls-rfc8216bis-22 §3.1.2):

  • One Media Playlist per take. EXT-X-VERSION is 6 or higher, as EXT-X-MAP without EXT-X-I-FRAMES-ONLY requires (RFC 8216 §4.3.2.5, §7; the draft's §4.4.4.5). EXT-X-TARGETDURATION is the longest chunk's end − start, in seconds rounded up (RFC 8216 §4.3.3.1), never above I by single-take rule 8. EXT-X-PLAYLIST-TYPE is VOD for a closed take and EVENT while it runs, segments added only at the end (RFC 8216 §4.3.3.5), with EXT-X-ENDLIST once the take is closed.
  • The initialization section is EXT-X-MAP with URI chunk 0's file and BYTERANGE <h>@0 (the draft's §4.4.4.5), h the length of chunk 0's top-level boxes (ISO/IEC 14496-12) before its first styp or moof. Chunk 0's segment is then EXT-X-BYTERANGE:<len − h>@<h> of the same file (RFC 8216 §4.3.2.2), and each later chunk its own file whole.
  • Durations and dates. Each segment's EXTINF is its chunk's end − start in seconds. The first segment carries EXT-X-PROGRAM-DATE-TIME (RFC 8216 §4.3.2.6), the instant t0 + start + o of the room clock in UTC; a gap-free take needs no other.
  • Events. The room's events ride the playlist as live-events §8.3 maps them, as EXT-X-DATERANGE, so a marker lands at its instant in the master (capture-binding §10).
  • Several takes. Each take is its own Media Playlist. An HLS client presents one rendition of a medium at a time; one that shows two takes together aligns them by EXT-X-PROGRAM-DATE-TIME, within §6.2's bound.
  • LL-CMAF. A CMAF segment holds CMAF chunks, each a moof and its mdat. A low-latency playlist of a running take lists them as EXT-X-PART with BYTERANGE into the chunk's file (the draft's §4.4.4.9), with EXT-X-PART-INF and EXT-X-SERVER-CONTROL (§4.4.3.7, §4.4.3.8). The take tree commits to whole chunks, so a part is proved only as part of its chunk, once the commitment that adds the chunk is held, within I + J of the chunk's end (single-take 10): a sink that verifies presents parts ahead of that and holds its verdict until the chunk is committed.

A master that is not CMAF is played this way only through a muxer for its codec, which this extension does not define. What this revision implements of this section: nothing. The corpus's masters are not CMAF, the reference has no muxer for a device's master (packaging §7's CMAF writer writes a floor's AV1 fragments), and it writes no playlist.

8. DVR over MoQ FETCH

This section fixes how a recording's chunks are addressed for a FETCH (§8.1), and what a relay that keeps a window of a room's tracks, a DVR window, does with it: what it keeps and evicts (§8.2), how it answers a FETCH from it (§8.3), what moderation withholds from it (§8.4), and how it keeps it across a restart (§8.5); and the window's presentation over HTTP (§8.6).

8.1 Addressing a take

  • The track. A recorded take is one track. Its Track Namespace (MOQT §2.4.1) has three fields: the room id and the device's key, each as 64 lowercase hexadecimal digits, and take; its Track Name is the take id as 64 lowercase hexadecimal digits. Every byte is one of a–z and 0–9, as in live-rooms §6.1's names, and its fields' 196 bytes are within MOQT §8.7's limit.
  • Groups and objects. Group g is chunk g, and its one object, Object ID 0, is the chunk's bytes whole: the unit a leaf commits to. A group's Location (MOQT §8.2) is (g, 0).
  • A clip's part is fetched by a FETCH (MOQT §3.2, §9.11) of that track whose LOCATION_FILTER (MOQT §9.20.9) has StartGroup first, StartObject 0, EndGroupDelta count − 1 and no EndObject, so every object of the end group: the range is inclusive (MOQT §3.2), and the groups arrive in the requested order (MOQT §3.2.1). The clip verifier states this address for each part (§5.4).
  • The commitments are events of the device's events track (live-rooms §6.1), whose group is the minute of room clock an event falls in (live-events §8.1). A FETCH of its groups from the opening commitment's to the head's holds them, among the device's other events; the clip verifier is given the take's take_commit events alone.
  • A running take. The groups below the latest commitment's size are committed; a group at or past it is a chunk no commitment has added yet, which a sink holds unverified until one does (§5.5: a clip of a running take is proved against the commitment that covers it).

A take's track kept in a DVR window is kept and answered as any other track (§8.2, §8.3), so a clip's part is a FETCH the window answers.

8.2 The window

A relay MAY keep a window of the tracks of a room: its media tracks with their receipts tracks, its events tracks, its catalogs and its takes.

  • What it keeps. A group, once the relay holds it whole: its first object arrived (or a Prior Object ID Gap says the objects before do not exist, MOQT §10.9), none of its subgroup streams is open or was reset, and its end is known. The objects are kept with every field a fetch stream carries (MOQT §11.4.1).
  • Its bounds. An age, counted from when a group's first object reached the relay, of at most 86 400 seconds: 24 hours, the start-over buffer a live-to-VOD harvest clips from; and a byte budget over the window's tracks together. Past either, the oldest group goes first, whole. Group g of a media track and group g of its receipts track (moq-streaming-format §4.1) go together, so that every group the window serves comes with its receipts and a viewer checks a recorded object exactly as it checks a live one (jwp-receipts §4.1). A group that becomes whole after the window has moved past its group number is not kept.
  • Where. The window is media and events the relay keeps beyond delivery, so the room's region pin (live-rooms §3.7) binds where it is stored.
  • Who reads it. A FETCH or fill below a track's live edge is served only to a grant with the seek power (live-rooms §10.6).
  • A room it never keeps. A room with a confidentiality record is never recorded, and a relay keeps nothing of its tracks beyond delivery (confidentiality §9). A relay knows such a room by its first sealed object, one whose Immutable Properties carry SECURE_OBJECT_KEY_ID (confidentiality §4.3), which it reads without a key. It MUST NOT keep that object's group. From then on it keeps nothing of the room's tracks, deletes what it kept of them before (the room's events and receipts tracks are in the clear, and can be whole before its first sealed group), and remembers the room across a restart. It still honours the moderation decisions it reads on the room's events tracks for what it delivers, and does not keep them.
  • A minors' room. A room in which a minor publishes is never kept either (age-assurance §8.4): from the moment a session proves a minor's publishing grant of the room, the relay keeps nothing of its tracks, deletes what it kept, and remembers the room across a restart, as for a room whose objects are sealed.

8.3 Answering a FETCH

  • Where from. A FETCH whose range the relay's cache holds is answered from the cache. Otherwise, when every group from its start to its end is held whole by the cache or the window, is known not to exist (MOQT §10.8), or was evicted from the window, the relay answers it from them; otherwise it asks upstream (MOQT §3.2.3). Groups come in the requested order (MOQT §3.2.1), objects within a group in Object ID order. A FETCH of the receipts track over the same range brings each group's receipts the same way.
  • What was evicted is no longer known to the relay. The relay marks the evicted groups of the range with an End of Unknown Range (MOQT §11.4.1.2, Serialization Flags 0x10C) through the last of them, then sends the groups it holds: a range partly evicted gets what remains and the marker, and a range wholly evicted gets FETCH_OK and the marker alone. It uses neither an unmarked gap nor an End of Non-Existent Range: an evicted object existed, an unmarked gap would say it did not (MOQT §3.2.2), and MOQT §10.3 makes an object that has left a cache an object of unknown state (§2.1 there).
  • Inside a group. An End of Range moves to a later group than the entry before it: its Group ID is coded as an object's Group ID Delta (MOQT §11.4.1.1, §11.4.1.2), the reading the joule-moq codec and the MOQT -22 vectors of joule-moq take. So when objects of a group cannot be served (withheld, §8.4; dropped, §8.5), one End of Unknown Range from the group's start through the last of them goes first, and the group's objects after it follow. The objects before the last such one are not sent.
  • Across relays. A window one relay keeps is fetched through another relay as any FETCH that relay cannot answer alone is (MOQT §3.2.3): forwarded once, on the hop link between them (live-rooms §10.7), and its fetch stream piped through, groups in the order asked. The relay the viewer's session reaches holds the request to live-rooms §10.6 against the live edge it learns from the relay it asks, and the relay that keeps the window serves the hop link as the link's token allows; each answers the same range with the same objects and the same End of Unknown Range, so a viewer on either relay is given the same window.
  • A window whose end lies ahead. A live-to-VOD harvest clips a window of at most 24 hours whose end may lie in the future. Its past is a FETCH. The whole is one SUBSCRIBE whose LOCATION_FILTER is the window, as an AbsoluteRange (MOQT §9.20.9), with FILL_PARAMETERS (MOQT §3.4): the fill is answered from the cache and the window as a FETCH is, and the subscription delivers the rest up to the window's end.

8.4 Moderation

What live-moderation withholds is withheld from the window, as §6.3 there has a relay withhold it in answer to a FETCH:

  • How the relay learns of decisions. It reads the decision events (live-events §4.11) on the room's events tracks, which it already forwards. It honours a decision whose record verifies under its principal (live-events §3.2), whose principal owns the events track it rides (live-rooms §6.1), and whose principal is a moderator it knows: a room key, or the principal of a grant with the moderate power (live-rooms §3.3) that its publishing session proved (live-rooms §10.5). The last decision it honoured on a target stands (live-moderation §4.5), and the honoured decisions are kept with the window.
  • Deny and redact withhold the target from every answer to every session but a moderator's; redact also erases it from the window. Allow restores what deny withheld. A withheld media object is the one whose receipt names it by its digest (live-moderation §4.1); its receipts stay where they are (live-moderation §8.1). A withheld event is left out of the events-track object that carries it, the object's other records sent byte for byte; an object left with none is withheld whole. Either is withheld, never replaced: each is an End of Unknown Range in its place (§8.3), and the decision that restricts it says why.

8.5 Across a restart

A window kept across a restart is checked again before it is served. Every object is checked against its receipt: a media object of a track with a receipts track against group g's receipts, as jwp-receipts §4.1 checks one at the door (the group receipt's signature under the trust the relay holds for the namespace, the object's receipt at its position, its Merkle proof to the signed root, its content hash); an events-track object's records each under its principal (live-events §3.2). An object that does not verify, or whose stored bytes do not hold, is dropped and counted, and never served: a FETCH that reaches it is told End of Unknown Range (§8.3). How a relay lays the window out on disk is its own; the reference relay's layout and its durability rule are in its README.

8.6 Over HTTP

The window is a time-shift buffer. In DASH (ISO/IEC 23009-1) its age is the MPD's timeShiftBufferDepth. In HLS a live Media Playlist keeps the window's segments and removes them from its start as the window evicts their groups; EXT-X-PLAYLIST-TYPE EVENT (RFC 8216 §4.3.3.5), whose segments are added only at the end, fits only a window that holds the session from its start and evicts nothing.

What this revision implements of §8.2 to §8.5: the reference relay, joule-moq-relay (joule-code/joule-code-rs/crates/joule-moq-relay), keeps a window per namespace prefix and its tests show each rule above. §8.6 is a mapping, and nothing writes it.

9. Reasons

The codes this extension adds, each with where it is raised:

reason raised at
listing_mismatch §4.4 step 1
recording_manifest_invalid §4.2, §4.4 step 2
component_invalid §4.4 steps 3, 4 and 6
component_digest_mismatch §4.4 step 3; SPEC §7.1's code, for a component of a recording
component_unlisted §4.4 step 3
recording_signer_not_permitted §4.3, §4.4 step 5, with the first instant of the span without the power
outside_span §4.4 step 6
take_omitted §4.5 complete 2
clip_record_invalid §5.2, §5.4 step 2
clip_device_mismatch §5.4 steps 3a, 3b
clip_take_mismatch §5.4 step 3b
tree_head_not_committed §5.4 step 3d
clip_out_of_range §5.4 step 3e
clip_not_included §5.4 step 3f
clip_window_invalid §5.4 step 3j
clip_chunk_missing §5.4 step 3k

take_omitted is a verdict; the rest are refusals. A recording verifier also raises every code a room verifier or a take verifier raises (live-rooms §11, capture-binding §12, and the live-events codes they list), at the room or the take; truncated, malformed, unknown_version, bad_key and bad_signature for a manifest that does not read or whose signature fails; wrong_room for a manifest of another room; recording_not_permitted for a package of a room never recorded; store_outside_region for a package kept outside the room's region pin; and delivery_gap and every verdict of capture-binding §12 as a verdict. A clip verifier also raises the codes of a descriptor that does not read (live-rooms §11); truncated, malformed, unknown_version, bad_key, bad_signature and wrong_room for its record and its commitments; and commit_malformed, delivery_gap, commit_chain_broken, root_not_extending, clock_unstable, capture_discontinuous, chunk_too_long, commit_early, commit_late, chunk_reordered, chunk_altered and chunk_extra at §5.4's steps, each with the meaning capture-binding §12 gives it.

10. Conformance

live-conformance/recordings/ holds the corpus, in one file, vectors.json, written from fixed seeds by wai_live_record_vectors gen. Its recordings are packages, each with its files and an evaluation instant and the outcome §4.5 gives them; its clips are each a room descriptor, a clip record and, per part, the device's key, the commitments and the chunks' bytes, with the outcome §5.4 gives them.

  • The session. A lesson room with two takes: the performer's, which is capture binding's lesson take byte for byte (its manifest, chunks, receipts, consent and commitments), and the accompanist's, captured beside it in chunks of another length on a device whose clock runs ahead of the room's. The recording sealed by the room authority, and by the director, stands, and so does one with a composite component, whose file stands for a composed object and whose digest alone the verifier checks.
  • Recordings that fail a claim: a take left out (take_omitted); capture binding's missing_chunk take, whose chunk 6 was never uploaded (chunk_missing); a commitment the relay did not deliver (delivery_gap); a take without its consent (consent_missing).
  • The record power over the span: a director that joins at 1 s seals the span from 1 s and stands; sealing from 0 is refused at 0; and a director removed at 18 s, exchanged at 17 s to a grant without the power, or whose grant expires at 18 s, is refused at that instant; one whose grant the key log revokes is refused at the room.
  • Recordings refused: a file changed after sealing (listing_mismatch) and with the listing rewritten (component_digest_mismatch), a take's or the composite's; a file of no component; a component with a file §3 does not define; a manifest that breaks §4.2, of another room, or with a bad signature; a manifest signed by a principal without the record power; a package kept outside the room's region pin; a package of a room whose recording policy has mode 0; a span that ends before a take's closing commitment; a take, and a grant, whose signature fails.
  • Good clips: a single chunk, a byte range with trims, a whole take, the first chunk, the last chunk, a span of the second take, a span proved against a commitment made while the take was running, a two-take clip aligned over one window, and a span of capture binding's late_commit take that its late commitment does not touch.
  • Clips refused, one for each reason of §9 a clip raises and for each single-take rule of §5.4: a proof that does not reach the head, a range past the head's size, a span over the chunk missing from capture binding's missing_chunk take, a clip spliced from two takes, a reordered, altered or extra chunk, a tree head the take never committed, a commitment left out or malformed, another take's or another device's commitments, a record of another room, with a bad signature, cut short, or with a trim in an aligned clip, a window its chunks do not cover, and spans of capture binding's late_commit, commit_early, root_not_extending, clock_unstable, capture_discontinuous, chunk_too_long and commit_chain_broken takes.

A conforming verifier gives every vector the outcome, the reason, the place and the data the corpus states. wai_live_record_vectors verify and the test tests/live_record.rs check that the file regenerates byte for byte, that wai-rs gives every vector its outcome, that every reason of §9 appears in the corpus, that §9 lists exactly the reference's codes, and that every take the corpus says it reuses is capture binding's, byte for byte, against live-conformance/takes/vectors.json. live-conformance/live_record_verify.py is the second implementation, written from this text: it shares no code with wai-rs; it checks range proofs by §5.3's recursion and, for a run of one chunk, by RFC 9162 §2.1.3.2's algorithm as that RFC states it; it verifies signatures with WAI's strict rule (claim-conformance/_ed25519.py), each take with the capture-binding corpus's second implementation (capture_take_verify.py) and the room with the live-rooms corpus's (live_rooms_verify.py); it gives every vector the same outcome; and it mutates copies of the corpus and requires every copy to fail.

11. Security and privacy considerations

  • The recording manifest is the signer's statement of which components the session has and of its span; it adds nothing to a take's verdicts, which the take verifier gives from the room's events. Its signer held the record power over the whole span, so a participant who can only publish or watch cannot seal a recording of the room, and one granted the power late cannot seal what came before it. The package digest names the whole; the manifest names each component, so a component can be fetched and checked alone.
  • A recording is personal data of everyone it records: the masters, and the events, whose chats, markers and captions are principals' words. The room's region pin and retention_days (live-rooms §3.7) apply to the package as they apply to the takes: the manifest states where the package is kept, and the verifier holds it to the pin; and a take's consents bound what publishable admits.
  • A clip proves that its bytes are those its take's device committed while it captured, at those indexes, under a commitment the device signed. Its trust rests on the device's key: a verifier takes it from the device's grant or the take manifest, never from the clip. The cutter's signature binds the cutter's choice of span, window and head and nothing more.
  • A range proof's nodes are digests of digests of other chunks' descriptors; they disclose no byte of another chunk. A trimmed clip carries its boundary chunks whole, so the bytes it trims travel with it; a cutter that must not disclose them cuts on chunk boundaries.
  • The time a clip states is the devices' measurement of the room clock, held to J at every commitment; the bytes are proved exactly, the instants to within §6.2's bound.
  • A clip cut while a take runs is proved against the commitment that covers it, which the device can no longer rewrite: any later commitment must extend it (capture-binding §4.3), so the clip stays a span of the take as it is finally committed.