WAI Extension: Live Recording
Mirrored from the canonical text at commit f871af55 ().
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 givescompleteandpublishablefor 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 thelive_recordmodule ofwai-rs(featurelive_record); the corpus islive-conformance/recordings/, and its second implementation islive-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; anddraft-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,
fromtoto, both included. - Package: the directory of files a recording is (§3).
- Component: a part of a package named by a kind, an
idand asha256(§3.2). - Head: the commitment of a take a clip’s proof is made against: one of the take’s
take_commitevents (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:
claimssets no bit above bit 0, andpurposesnone above bit 3;fromis at mostto;regionis two capitalsA–Z;- the first component is the room (kind 1,
idroom) and the second the events (kind 2,idevents); then come the takes, each of kind 3 with anidof 64 lowercase hexadecimal digits, in strictly ascending order ofid; then, last, at most one composite (kind 4,idcomposite); 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:
- The listing.
LISTINGis present and is the listing of every other file: otherwiselisting_mismatch, atlisting. - The manifest.
recording.wrmis present, otherwiserecording_manifest_invalid, and it reads (§4.2); atmanifest. - 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 itssha256, otherwisecomponent_digest_mismatch; at that component. Then every file isLISTING,recording.wrm, or a file below a component’s directory: otherwisecomponent_unlisted, atlisting. - 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, atroomwith 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’sroomis the room id: otherwisewrong_room, atmanifest. - The signer (§4.3): otherwise
recording_signer_not_permitted, atsignerwith 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),regionis one of them, otherwisestore_outside_region, capture-binding §6.3’s code, atmanifest. - 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 andat, and its refusal is the recording’s, attakewith the component’s index and the take verifier’s own place; the take id it reads is the component’sid, otherwisecomponent_invalid; and the take lies within the span, its start (therecord_startit answers) at or afterfromand its last held commitment at or beforeto, otherwiseoutside_span; at that component. The records beforefromor aftertothat the package holds are the room’s chains whole, which the room verifier replays the room from; the manifest states nothing of them. - 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.
delivery_gap: the room verifier holds a gap (live-rooms §9.4).take_omitted: a take of the session whoserecord_startlies within the span has no component. A take of the session is the take id (capture-binding §3.2) that arecord_startthe room verifier accepted (live-events §4.9) names for its target, where the room holds atake_commitof that target whose payload reads (capture-binding §5.2) and names that take: a start its device answered.- For each take component, in the manifest’s order: its
single-takeverdict fails withdelivery_gap,commit_chain_brokenortake_not_closed, so its commitments are not one unbroken chain from the opening commitment to the closing one: that reason; or itscompleteverdict 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
completeand ofpublishablefor each of the four purposes; each take’s id and verdicts (capture-binding §9.5); and whether the manifest stands: every claim it makes, andpublishablefor 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), 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,
countis at least 1; each chunk’sindexisfirstplus its position, and eachlenis at least 1;proof_lenis at most 128;trim_inis below the first chunk’slen, andtrim_outbelow the last chunk’s; and, with one chunk,trim_in+trim_outis below itslen; - a part with a trim that is not 0 is the clip’s only part;
window_inis belowwindow_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:
- 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). - The record reads (§5.2), at
clip. Itsroomis the room id, otherwisewrong_room; and the verifier is given one input for each part, otherwiseclip_record_invalid; atclip. - Each part, in order, steps (a) to (k):
- (a) The key given is the part’s
device: otherwiseclip_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 atake_commitwhose payload reads (capture-binding §5.2,commit_malformed), and names the part’stake(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,firstandnthat follow its predecessor, which is not a closing one, and is itself not an opening one. A later one whosenis more than one above its predecessor’s isdelivery_gap; any other break iscommit_chain_broken. Then each extends its predecessor (capture-binding §4.3), otherwiseroot_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 iscommit_chain_brokenat the part. - (d) The last commitment given is the head: its digest is
head, and its size and root are the part’ssizeandroot. Otherwisetree_head_not_committed, at the part: a tree the take never committed proves nothing of it. - (e)
first+countis at mostsize: otherwiseclip_out_of_range, at the part. - (f) The proof (§5.3) shows the part’s descriptors are the head’s leaves
firstonward, undersize: otherwiseclip_not_included, at the part. - (g) Every commitment’s offset is within J of the opening one’s, o (capture-binding
§9.4
single-take6): otherwiseclock_unstable, at that commitment. - (h) The chunks tile their capture, each
startbelow itsendand eachstartthe previous chunk’send(capture_discontinuous), and none spans more than I (chunk_too_long) (single-take7, 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-take9, 10); at the part. - (j) The window: with the part’s span the first chunk’s mapped
startto the last one’s mappedend, the span covers the window, the first chunk ends afterwindow_inand the last begins beforewindow_out; and a trimmed part’s window is its span. Otherwiseclip_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 arechunk_reordered, any otherschunk_altered; at that chunk. Bytes given past the last chunk arechunk_extra, at the chunk past it.
- (a) The key given is the part’s
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:
firstandcount, 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
lenof the chunks before it: the chunks from the one holding byte x to the one holding byte y − 1,trim_inx less the first one’s offset, andtrim_outthe 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-VERSIONis 6 or higher, asEXT-X-MAPwithoutEXT-X-I-FRAMES-ONLYrequires (RFC 8216 §4.3.2.5, §7; the draft’s §4.4.4.5).EXT-X-TARGETDURATIONis the longest chunk’send−start, in seconds rounded up (RFC 8216 §4.3.3.1), never above I bysingle-takerule 8.EXT-X-PLAYLIST-TYPEisVODfor a closed take andEVENTwhile it runs, segments added only at the end (RFC 8216 §4.3.3.5), withEXT-X-ENDLISTonce the take is closed. - The initialization section is
EXT-X-MAPwithURIchunk 0’s file andBYTERANGE<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 firststypormoof. Chunk 0’s segment is thenEXT-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
EXTINFis its chunk’send−startin seconds. The first segment carriesEXT-X-PROGRAM-DATE-TIME(RFC 8216 §4.3.2.6), the instantt0+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
moofand itsmdat. A low-latency playlist of a running take lists them asEXT-X-PARTwithBYTERANGEinto the chunk’s file (the draft’s §4.4.4.9), withEXT-X-PART-INFandEXT-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-take10): 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 ofa–zand0–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 StartGroupfirst, StartObject 0, EndGroupDeltacount− 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_commitevents 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.
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-moqcodec and the MOQT -22 vectors ofjoule-moqtake. 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. - 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_FILTERis the window, as an AbsoluteRange (MOQT §9.20.9), withFILL_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
decisionevents (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; 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
lessontake 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’smissing_chunktake, 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 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_committake that its late commitment does not touch. - Clips refused, one for each reason of §9 a clip raises and for each
single-takerule 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’smissing_chunktake, 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’slate_commit,commit_early,root_not_extending,clock_unstable,capture_discontinuous,chunk_too_longandcommit_chain_brokentakes.
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 whatpublishableadmits. - 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.