Skip to main content

WAI Extension: HLS Windows and Clips

Mirrored from the canonical text at commit 0d3f0d7c ().

Status: Draft. Registers the record wai.delivery.hls_clip: what a holder of a classical HLS presentation kept of it, signed as it kept it, and the clips it cut from what it kept. A window is the recent span of one media playlist of fragmented MPEG-4 segments that a holder keeps for a player to seek back into; each time what it lists changes, a segment admitted, a segment evicted or the stream ended, the holder signs a window revision (wai-hls-window, §5): the segments it adds, each by its SHA-256 and length with its duration, its date and its header, how many it evicts, the state that results, and a link to the revision before it. A clip is a span of the window turned into an on-demand playlist; its clip record (wai-hls-clip, §6) states every segment and header the clip presents and the revision it was cut from. Both playlists are written from the records by one rendering (§4), so a verifier renders the playlist from the record and compares the bytes. A viewer of the live window holds the chain of revisions and each segment it lists at signed (§7.2); a clip verifier holds the clip’s files to its record and the record to the window it was cut from (§7.3); and what a window or a clip lists is the listing a holder’s storage snapshots state (receipt-ledger §4.13), the same objects by the same hashes (§9). No new container, no new playlist tag and no transport. The reference implementation is the hls_clip module of wai-rs (feature hls_clip); the corpus is live-conformance/hls-clips/, and its second implementation is live-conformance/hls_clips_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: the WAI texts this one cites; RFC 8216 whole (August 2017, read 2026-11-11, SHA-256 954910a5ad73506cfdb5abe11571f6b859d49d1f82191ffe616595a40687e58e of the text the RFC Editor serves), its §3.3, §4.1, §4.3.1.2, §4.3.2.1, §4.3.2.3, §4.3.2.5, §4.3.2.6, §4.3.3.1 to §4.3.3.5, §4.3.5.1, §6.2.1, §6.2.2 and §7 in particular; and its draft successor, draft-pantos-hls-rfc8216bis-22 (1 May 2026, the latest revision in the IETF archive on 2026-11-11, SHA-256 5e8ce0e44266757dc0ab44061f3030fb3cf6de32fb217555aebff3507912b6f2), §4.4.1.2, §4.4.2.1, §4.4.3.1 to §4.4.3.5 and §4.4.4.1 to §4.4.4.6, to hold that every tag the rendering writes keeps its meaning there. Every tag written is RFC 8216’s; the draft’s only change to them that a rendering meets is that a target duration is at least 1 (its §4.4.3.1), which §5.2 and §6.2 require. A section number of the draft in this text is that revision’s.

1. Scope

In scope: how an object of a window or a clip is named and what a set of them lists (§3); the rendering of a window’s live playlist and a clip’s on-demand playlist (§4); the window revision, its rules and the state a chain of revisions gives (§5); the clip record and its rules (§6); the verification of a window’s chain, of a live window by a viewer, and of a clip (§7); where each is served (§8); and what a holder’s storage snapshots of a window and a clip state (§9).

Out of scope: how a holder ingests, packages or keeps the presentation; which segments a window keeps and for how long, which is its holder’s policy (a 24-hour age and a byte budget are typical, as live-recording §8.2 bounds a relay’s); the multivariant playlist that names several renditions’ windows or clips, each of which is a window or a clip of its own here; byte ranges, low-latency parts, encrypted segments and I-frame playlists, which a rendering never writes; and what is in an object’s bytes, which a record names and never reads. A room’s recording and a clip of a take are live-recording’s, a WAI playlist and its receipt are http-delivery’s (§8.6 there), and a room’s tracks served over HTTP with statements beside their playlists are live-http-egress’s.

Relationship to the core spec: wai.delivery.hls_clip is a record (SPEC §5). It names no payload format and is not dispatchable.

2. Terms

3. Objects

3.1 Names

An object is named by its content hash, the SHA-256 of its bytes, and its length. A segment’s entry is a JSON object with exactly these members:

membervalue
sha256its content hash
bytesits length, at least 1
duration_msits duration in milliseconds, at least 1: what EXTINF states (RFC 8216 §4.3.2.1)
date_msthe instant of its first sample: what EXT-X-PROGRAM-DATE-TIME states (RFC 8216 §4.3.2.6), at most 253 402 300 799 999 (9999-12-31T23:59:59.999Z)
discontinuitytrue when there is a discontinuity between it and the segment before it (RFC 8216 §4.3.2.3)
initthe content hash of its init

An init is stated as {"bytes": <length>, "sha256": <content hash>}. A playlist names an init as init/<content hash>.mp4 and a segment as seg/<content hash>.m4s, each resolved against the playlist’s URI (RFC 8216 §4.1), so an object’s name is its bytes’. No two objects one window revision lists, and no two a clip lists, have one content hash: a segment carries its own sequence number and decode time, so two segments with the same bytes are one segment listed twice, and an init is never a segment.

3.2 The listing

What a window revision or a clip lists is its segments and the inits they are read with. Its listing is that of receipt-ledger §4.13 over those objects: objects, how many there are; bytes, their lengths summed; and listing, the SHA-256 over the 19 bytes wai:ledger-listing and 0x01 followed by each object, in strictly ascending order of content hash, as its content hash (32 bytes) and its length (u64). A playlist and a record are the holder’s statements of the objects and are not listed, as a package’s receipt is not one of its objects.

4. The rendering

A playlist is rendered from: a target duration t; a media sequence number m; a discontinuity sequence number d; whether it is on demand; whether it ends; whether its segments are independent; and its segments, at least one, in order. Each line ends in LF (0x0A), integers are written in decimal without leading zeros, a duration is written as seconds with three fractional digits (2.002 for 2002 ms), and a date as YYYY-MM-DDThh:mm:ss.sssZ: the proleptic Gregorian date and UTC time of its instant, the year in four digits and every field zero-padded (RFC 3339 §5.6, as RFC 8216 §4.3.2.6 asks of ISO 8601). The lines are:

  1. #EXTM3U; #EXT-X-VERSION:6, the version RFC 8216 §7 requires of EXT-X-MAP without EXT-X-I-FRAMES-ONLY and above which a server writes none (§6.2.1 there); #EXT-X-TARGETDURATION:<t>; #EXT-X-MEDIA-SEQUENCE:<m>; #EXT-X-DISCONTINUITY-SEQUENCE:<d> when d is not 0; #EXT-X-PLAYLIST-TYPE:VOD when it is on demand; and #EXT-X-INDEPENDENT-SEGMENTS when its segments are independent.
  2. Each segment, in order: #EXT-X-DISCONTINUITY when its entry states one and it is not the first; #EXT-X-MAP:URI="init/<init>.mp4" when it is the first or its init is not the one of the segment before it; #EXT-X-PROGRAM-DATE-TIME:<date>; #EXTINF:<duration>,; and seg/<sha256>.m4s.
  3. #EXT-X-ENDLIST when it ends.

So a segment’s discontinuity sequence number is d plus the EXT-X-DISCONTINUITY lines before it (RFC 8216 §6.2.1), and the first segment’s is d: a discontinuity before the first segment is written into d, never as a line (§5.3). Every segment has its date, so the mapping of dates to segments is never ambiguous (RFC 8216 §6.2.1), and each has its header. A clip’s playlist is on demand and ends, with m and d 0 (§6.1); a window’s is never on demand, and ends once the window has ended (§5.3).

A playlist reads as a rendering when some inputs render to exactly its bytes. A reader splits it at LF and reads, in order: the header lines of step 1, EXT-X-DISCONTINUITY-SEQUENCE and the two that follow it being optional; then segments until #EXT-X-ENDLIST or the end, each its optional discontinuity, its optional map (not optional on the first), its date, its duration and its URI, every value of the form the rendering writes; and, where it holds that, the inputs those lines give. It then renders them and requires its bytes. The inputs read back are the ones rendered, but for the first segment’s discontinuity, which is in d, and every length, which a playlist does not state: a reader of a playlist gives each length as unknown.

5. A window revision

5.1 Form

A window revision is one JSON object in RFC 8785’s form, at most 65 536 bytes, with exactly these members:

membervalue
kind"wai-hls-window"
version1
windowthe window’s name, of the holder’s choosing: 1 to 128 bytes of A–Z, a–z, 0–9, ., _, - and /
revisionits number: 0 for the window’s first, and each later one the number after its parent’s
parentthe identity of the revision before it, or null for revision 0
at_msthe instant the holder made it, on its clock
target_duration_sthe target duration, 1 to 86 400 (RFC 8216 §4.3.3.1)
independent_segmentswhether its playlist states EXT-X-INDEPENDENT-SEGMENTS (RFC 8216 §4.3.5.1)
evictedhow many segments it removes from the start of what its parent listed
initsthe inits it lists that what its parent listed after the eviction does not, at most 16
addedthe entries of the segments it appends at the end, in order, at most 256
endedwhether the window’s stream has ended: its playlist then ends
media_sequencethe media sequence number of the first segment it lists
discontinuity_sequencethe discontinuity sequence number of the first segment it lists
objects, bytes, listingwhat it lists (§3.2)
playlist_sha256the SHA-256 of the playlist it renders (§5.3)
signerthe holder’s key
sigEd25519 by signer over the 15 bytes wai:hls-window (ASCII) and 0x01, followed by the RFC 8785 form of every member but sig, verified as SPEC §7 Signatures requires

A revision is served as exactly its RFC 8785 form, and its identity is the SHA-256 of those bytes. A reader reads the form whole: I-JSON (RFC 7493), its one canonical form (a member named twice, at any depth, white space, another order of members or another escape is not that form), every member and no other, each of its form; otherwise hls_record_malformed.

5.2 Rules

A reader then refuses, the first that applies:

  1. revision is 0 and parent is not null, or it is not 0 and parent is null; revision 0 evicts any or adds none; or its inits are not distinct, each used by an added segment, in the order of the first added segment that uses each: hls_record_invalid;
  2. two of its added segments, or an added segment and an init, have one content hash: hls_object_repeated;
  3. revision 0’s first added segment states a discontinuity: hls_discontinuity_first;
  4. an added segment’s duration, rounded to the nearest second, half a second up, is above the target duration (RFC 8216 §4.3.3.1: ⌊(duration_ms + 500) / 1000⌋ > target_duration_s): hls_segment_too_long;
  5. an added segment’s date is not after the one before it: hls_time_not_increasing.

Then its signature: bad_key, bad_signature.

5.3 The state a chain gives

A window’s state after a revision is: the segments it lists, in order, each with its entry; the lengths of the inits they use; its media sequence number m and discontinuity sequence number d; whether it has ended; and its target duration and whether its segments are independent. Its playlist is the rendering (§4) of its target duration, m, d, not on demand, ending exactly when it has ended, its independence and its segments.

Revision 0’s state lists its added segments, with the media_sequence and discontinuity_sequence it states: a window may begin in the middle of a stream and keep its numbers. A later revision’s state is its parent’s with the first evicted segments removed and the added ones appended. With T the parent’s listed segments followed by the added ones, T₀ the parent’s first, and k the number evicted:

The revision states that state’s media_sequence, discontinuity_sequence, objects, bytes, listing and the SHA-256 of its playlist. Its inits are exactly those its added segments use that the segments the parent lists after the eviction do not use, in the order of first use; an init no listed segment uses any longer has left the listing, and is stated again by the revision that lists it again. A revision changes the window: it adds a segment, evicts one or ends it. A window that has ended adds nothing more and stays ended; it may still evict, as RFC 8216 §6.2.1 lets a server remove segments from a playlist’s start. Every revision lists at least one segment and at most 65 536.

RFC 8216 §6.2.2 asks a server not to leave a live playlist shorter than three target durations; that is the holder’s eviction policy, and a revision that does is not refused here. What a window’s playlist states of its segments never changes while it lists them: a revision appends and removes, and nothing else, as RFC 8216 §6.2.1 permits.

6. A clip record

6.1 Form

A clip record is one JSON object in RFC 8785’s form, at most 16 777 216 bytes, with exactly these members:

membervalue
kind"wai-hls-clip"
version1
windowthe name of the window it was cut from (§5.1)
cut_fromthe identity of the window revision whose listing it was cut from, or null for a clip of a window that signs no revisions
firstthe media sequence number, in that window, of its first segment
from_ms, to_msthe span it presents, from the instant from_ms (included) to to_ms (excluded)
cut_at_msthe instant the holder cut it, on its clock
target_duration_sthe target duration, 1 to 86 400
independent_segmentswhether its playlist states EXT-X-INDEPENDENT-SEGMENTS
initsthe inits it lists, 1 to 256, each used, in the order of the first segment that uses it
segmentsthe entries of its segments, in order, 1 to 65 536
playlist_sha256the SHA-256 of its playlist
signerthe holder’s key
sigEd25519 by signer over the 13 bytes wai:hls-clip (ASCII) and 0x01, followed by the RFC 8785 form of every member but sig, verified as SPEC §7 Signatures requires

Its playlist is the rendering (§4) of its target duration, m 0 and d 0, on demand, ending, its independence and its segments. Its media sequence starts at 0, so it is an on-demand presentation of its own; first places it in the window. A record is served as exactly its RFC 8785 form, and its identity is the SHA-256 of those bytes. It reads as a window revision does (§5.1), with its own members (hls_record_malformed).

6.2 Rules

A reader then refuses, the first that applies:

  1. from_ms is not below to_ms: hls_record_invalid;
  2. a segment’s init is not one of its inits: hls_init_unlisted;
  3. its inits are not exactly the distinct inits its segments use, in the order of the first segment that uses each: hls_record_invalid;
  4. two of its segments, or a segment and an init, have one content hash: hls_object_repeated;
  5. its first segment states a discontinuity: hls_discontinuity_first (a clip’s first segment follows nothing in it);
  6. a segment’s duration is above the target duration as §5.2 rule 4 rounds it: hls_segment_too_long;
  7. a segment’s date is not after the one before it: hls_time_not_increasing;
  8. its segments are not exactly the ones that present some of the span: the first’s date is at or before from_ms and its end, its date plus its duration, after it; and the last’s date is before to_ms and its end at or after it (hls_span_not_covered). So a clip carries no segment wholly outside its span, and leaves out none of it;
  9. playlist_sha256 is not the SHA-256 of its playlist: hls_record_inconsistent.

Then its signature: bad_key, bad_signature.

A holder cuts a clip of the span [a, b) from a revision’s state by taking the listed segments from the one whose date is at or before a and whose end is after it to the one whose date is before b and whose end is at or after it, each entry as listed but for the first’s discontinuity, which a clip does not state; the inits they use, in the order of first use, with their lengths; first the first one’s media sequence number; and cut_from the revision’s identity.

7. Verification

7.1 A window’s chain

A verifier given the holder’s key and a window’s revisions in order, from revision 0, and for any of them the playlist served with it, checks each in order and stops at the first refusal, which it reports with the revision’s position:

  1. it reads and holds to its rules (§5.1, §5.2);
  2. its signer is the holder’s key (hls_signer_unknown);
  3. its revision is its position, and, past the first, its parent is the identity of the one before it (hls_revision_unchained);
  4. past the first, its window, target_duration_s and independent_segments are the first’s (hls_window_changed), and its at_ms is not below its parent’s (hls_revision_time);
  5. past the first, it adds no segment, and states ended, once its parent has ended (hls_window_ended); and it adds, evicts or ends (hls_revision_empty);
  6. it evicts no more segments than its parent lists, and lists at least one and at most 65 536 (hls_record_inconsistent);
  7. no added segment is an object the parent lists after the eviction, and no init it states is a segment the parent lists then (hls_object_repeated); each added segment’s init is one of its inits or one that the segments the parent lists then use (hls_init_unlisted); and none of its inits is one of the latter (hls_record_inconsistent);
  8. its first added segment’s date is after that of the last segment the parent lists after the eviction (hls_time_not_increasing);
  9. the state it gives (§5.3) has the media_sequence, discontinuity_sequence, objects, bytes, listing and playlist_sha256 it states (hls_record_inconsistent);
  10. the playlist served with it, where one is given, is its playlist (hls_playlist_mismatch).

A chain that passes states, for each revision, its identity, its media sequence and discontinuity sequence numbers, how many segments it lists and its listing; and the window’s subject (§9).

7.2 A viewer of the live window

A viewer holds the holder’s key and a head: the latest revision it accepted, and the state it holds for it. It is given, each time it reloads, the playlist and the revision served at rev/<SHA-256 of the playlist>.json (§8), and fetches a parent by its identity. It checks:

  1. The first revision it is given is its anchor: it reads, holds to its rules and verifies (§5.1, §5.2), and its signer is the holder’s key (hls_signer_unknown); the playlist’s SHA-256 is its playlist_sha256 (hls_playlist_mismatch); the playlist reads as a rendering (§4), not on demand (hls_playlist_malformed); and its target duration, independence, whether it ends, m and d are the revision’s (hls_record_inconsistent). The viewer holds the state the playlist reads as, every length unknown, and the revision as its head (anchored).
  2. The head again (a reload with no new revision): the playlist is its playlist (hls_playlist_mismatch), and nothing changes (unchanged).
  3. A later revision becomes the head only when it chains to the head. From it the viewer follows parent, fetching each parent from rev/<parent>.json, until it reaches the head’s identity. Each fetched parent is served (hls_revision_parent_missing), its SHA-256 is the identity it was fetched by and its number one below its child’s (hls_revision_unchained), and it reads, holds to its rules, verifies and is the holder’s (§7.1 steps 1 and 2). A walk that reaches a revision at or below the head’s number, or with no parent, before the head does not chain (hls_revision_unchained), so a revision older than the head never does. Then, oldest first, each revision walked and the new one are held to §7.1 steps 4 to 8 against the state the viewer holds, and step 9 as far as a state of unknown lengths allows: its media_sequence, discontinuity_sequence and playlist_sha256. The playlist is the new head’s (hls_playlist_mismatch). The viewer then holds the new state and head (accepted).
  4. A head too far back. A viewer fetches at most 64 parents for one revision. When the walk needs more, and the revision is ahead of the head (its m plus the segments it lists is above the head’s), the viewer anchors again at it, as in step 1 (reanchored): the revisions between were not walked, and no segment they alone listed is held at signed. One that is not ahead is refused (hls_revision_walk_exceeded). Before it anchors again, every segment the new state lists at a media sequence number the old one lists is the same content hash, or the revision is refused (hls_listing_rebound): a holder that signs two windows under one name is caught where they meet.

A revision refused leaves the head and the state as they were. A segment is held at signed (the word of http-delivery §11.6) when the head’s state lists it at its media sequence number and its bytes’ SHA-256 is its content hash; the viewer presents it at that number and no other. So a viewer of the live window holds each segment it plays to the holder’s signed word, and holds the same word for a clip cut later from a revision it accepted (§7.3 step 5).

7.3 A clip

A clip verifier is given the holder’s key; the clip record; the clip’s files, clip.m3u8 and each init/<content hash>.mp4 and seg/<content hash>.m4s, each present or absent; and, where it holds one, the window: the states of the revisions of a chain it verified (§7.1), or a viewer’s (§7.2). It stops at the first refusal, which it reports with where:

  1. the record reads and holds to its rules (§6.1, §6.2), at record;
  2. its signer is the holder’s key (hls_signer_unknown), at record;
  3. clip.m3u8 is given (hls_file_missing) and is its playlist (hls_playlist_mismatch), at playlist;
  4. each init, then each segment, in order, is given (hls_file_missing), and its bytes’ SHA-256 is its content hash and their length its bytes; bytes whose SHA-256 is another object’s of the record are hls_object_swapped, and any others hls_object_altered; at inits or segments and its position;
  5. with the window: cut_from names a revision of it made at or before cut_at_ms (hls_revision_unknown); and the clip’s window, target_duration_s and independent_segments are the window’s, and its segments are the ones that revision’s state lists from media sequence number first on, each with the same content hash, length, duration, date and init, the same discontinuity but for the first, and each init with the same length (hls_clip_not_in_window); at window.

A clip that passes is accepted, and the verifier states its identity, how many segments it presents and their duration, its listing (§3.2) and its subject (§9). A clip accepted without a window is held to its holder’s word alone: a segment the holder left out and signed for is found only against the window it was cut from, whose revisions the holder signed before the cut.

8. Where they are served

9. Storage

A holder that bills a window’s or a clip’s storage under a tariff of version 8 of receipt-ledger (§4.13 there) keeps it as one stored set. The objects that ledger’s snapshots list are the ones a revision and a clip list (§3.2), named the same way, by their content hashes and lengths, so a snapshot’s objects, bytes and listing are a revision’s or a clip’s figures, which anyone holding its record recomputes; a segment a window and a clip both list is one object with one content hash in both listings, kept once or twice as the holder keeps it, and listed by each set. Nothing in receipt-ledger changes.

Record kinds 39 (a window revision) and 40 (a clip record), each named by its identity (§5.1, §6.1), are registered in receipt-ledger §5.2 and cited here, in the subject.

10. Reasons

reasonraised at
hls_record_malformed§5.1, §6.1: a record that does not read as its form
hls_record_invalid§5.2 rule 1, §6.2 rules 1 and 3
hls_object_repeated§5.2 rule 2, §6.2 rule 4, §7.1 step 7
hls_discontinuity_first§5.2 rule 3, §6.2 rule 5
hls_segment_too_long§5.2 rule 4, §6.2 rule 6
hls_time_not_increasing§5.2 rule 5, §6.2 rule 7, §7.1 step 8
hls_init_unlisted§6.2 rule 2, §7.1 step 7
hls_span_not_covered§6.2 rule 8
hls_record_inconsistent§6.2 rule 9, §7.1 steps 6, 7 and 9, §7.2 steps 1 and 3
bad_key§5.2, §6.2: a signer that is not a key SPEC §7 Signatures admits
bad_signature§5.2, §6.2: a signature that does not verify
hls_signer_unknown§7.1 step 2, §7.2 steps 1 and 3, §7.3 step 2
hls_revision_unchained§7.1 step 3, §7.2 step 3
hls_window_changed§7.1 step 4
hls_revision_time§7.1 step 4
hls_window_ended§7.1 step 5
hls_revision_empty§7.1 step 5
hls_playlist_mismatch§7.1 step 10, §7.2 steps 1 to 3, §7.3 step 3
hls_playlist_malformed§7.2 step 1
hls_revision_parent_missing§7.2 step 3
hls_revision_walk_exceeded§7.2 step 4
hls_listing_rebound§7.2 step 4
hls_file_missing§7.3 steps 3 and 4
hls_object_altered§7.3 step 4
hls_object_swapped§7.3 step 4
hls_revision_unknown§7.3 step 5
hls_clip_not_in_window§7.3 step 5

11. Conformance

live-conformance/hls-clips/ holds the corpus, in one file, vectors.json, written from fixed seeds by wai_hls_clip_vectors gen. Its objects are short box-shaped stand-ins for CMAF headers and segments, each a ftyp and moov, or a styp, moof and mdat, of generated bytes: a record names bytes and never reads them, so no decodable media is needed, and none is a recording.

A conforming verifier gives every vector the outcome, the reason, the place and the figures the corpus states. wai_hls_clip_vectors verify and the test tests/hls_clip.rs check that the file regenerates byte for byte, that wai-rs gives every vector its outcome, and that §10 lists exactly the reference’s codes. live-conformance/hls_clips_verify.py is the second implementation, written from this text: it shares no code with wai-rs, renders and reads playlists, computes listings and dates itself, verifies signatures with WAI’s strict rule (claim-conformance/_ed25519.py), gives every vector the same outcome, fails every mutated copy of the corpus and catches each of its own checks dropped, with the standard library alone.

12. Security and privacy considerations