Skip to main content

WAI Extension: Stream Termination

Mirrored from the canonical text at commit 260cbd09 ().

Status: Draft. Registers the record wai.live.termination: a sink’s signed account of one stream it received, from its start to the moment it stopped, and why it stopped. The sink signs a stream log: a chain of records, WSL1, each naming the one before it by digest, under a key it makes for its playback session. The log opens with the stream, how it started (by a user’s activation, or by autoplay, with the viewer’s autoplay setting recorded beside the platform’s answer), the sink’s termination policy and its display declaration (capability-constraints §4); it then holds a receipt for each segment the sink received, with the rendition and how it matched the panel and each hop’s joules with their acquisition class (energy-measurement §2); the observations of whether anyone could be watching (the document’s visibility, occlusion where the platform reports it, the display’s power and the viewer’s interaction); and it ends with a termination: the reason (hidden, occluded, display off, idle, autoplayed with no interaction, stopped by the viewer, or ended), the media time, and the joules of each hop up to that point, a total per hop and class and never one across classes. A verifier checks the chain, that nothing follows the termination, that the reason is one the log’s own observations support, that every segment’s rendition was admissible under the panel, and the totals; it reports how much was delivered while nobody could see it. No new transport. The reference implementation is the stream_termination module of wai-rs (feature stream_termination), which wai/wai-live-web builds for a page; the page’s own code is joule-code/joule-code-rs/crates/joule-moq-web/web/room/termination.js; the corpus is live-conformance/termination/, and its second implementation is live-conformance/stream_termination_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision, each pinned to the version named: the HTML Standard, Living Standard last updated 20 October 2026, §6.2 (page visibility: a document’s visibility state, “update the visibility state” and the visibilitychange event) and §6.4 (user activation: the activation-triggering input events and the UserActivation interface); Page Visibility Level 2, W3C Discontinued Draft 23 June 2022, which defers to the HTML Standard and was read for its note on when a document is hidden; Autoplay Policy Detection, W3C Working Draft 4 September 2025, §2.1 (getAutoplayPolicy() and its three values); Intersection Observer, W3C Working Draft 18 October 2023, §2.3 (isIntersecting); and the texts capability-constraints §4.1 names. A section number in this text is this extension’s unless it names another document.

1. Scope

In scope: the stream log, its records, their signature and their rules (§3); what a sink observes, when it terminates a stream, the autoplay setting, the segments it receipts and the hop figures it states (§4); what a sender does with a termination and a display declaration (§5); verification, the state a log shows at each instant and what an accepted log states (§6); and the reasons (§7).

Out of scope: a transport, and how a log reaches a sender or an operator, which a deployment carries as it carries its other receipts; how a meter measures, which energy-measurement governs; a figure for the display’s own energy, which this revision does not compute; and attention, which nothing here measures: a stream nobody can see is in scope, one nobody looks at is not.

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

Relationship to the other extensions:

2. Terms

3. The stream log

3.1 Records

A stream log is a sequence of records, each:

fieldsizemeaning
magic4WSL1 (ASCII)
version11 in this revision
kind11 open, 2 segment, 3 observation, 4 display, 5 terminate
sink32the session key, which signs the record
seq4u32: the record’s index in the log, from 0
prev32the digest of the record before it; 32 zero bytes for the first
t8u64: when the sink made the record
body…the kind’s body (§3.2 to §3.7)
sig64§3.8

3.2 The open record

The first record of a log, and the only one of kind 1:

fieldsizemeaning
stream32the stream: for a MoQ track, its track digest (live-metering §2: SHA-256 over the namespace as live-rooms §6.2 renders it, one zero byte, and the name); for an HTTP stream, SHA-256 over the URI of the content description as the sink requested it
media11 video, 2 audio
start11: a user activation started it; 2: it started with none (autoplay)
autoplay1the viewer’s autoplay setting at the sink: 1 on, 2 off (§4.4)
ua_policy1the platform’s answer for autoplay (§4.4): 0 not asked, 1 allowed, 2 allowed-muted, 3 disallowed
unseen_grace_ms4u32: how long a video stream stays unseen before the sink terminates it (§4.3)
idle_ms4u32: how long with no interaction before it terminates the stream as idle; 0 for never
autoplay_grace_ms4u32: how long an autoplayed stream with no interaction runs before it terminates it; 0 for never
display22the display declaration (§3.3)
ladder1 + 14 × na u8 count n, then each rendition the sender offers: width, height and frame_rate_milli (u32 each), bit_depth (u8) and transfer (u8), the parameters of capability-constraints §3.1

A video stream has a ladder of 1 to 32 renditions and a display declaration; an audio stream has a ladder of none, and its declaration is 22 zero bytes.

3.3 The display declaration

capability-constraints §4.1’s members, 22 bytes:

fieldsizemeaning
long4u32
short4u32
refresh_milli4u32
bit_depth1u8
hdr10 false, 1 true
long_mm4u32; 0 not known
short_mm4u32; 0 not known

It is the body of a display record (kind 4) as well, which a sink signs when its panel changes: the declaration in effect at a record is the one in the last display record before it, or the open record’s where there is none.

3.4 The segment record

A segment receipt (kind 2):

fieldsizemeaning
segment32the segment: for an HTTP segment, SHA-256 over its bytes as received; for a MoQ segment, SHA-256 over the content hashes (jwp-receipts §2.1) of its objects in the order received, concatenated
objects4u32: the objects it holds, at least 1; 1 for an HTTP segment
bytes8u64: their payload bytes as received
media_start8u64: the media time of its first sample
media_end8u64: the media time its last sample ends at, above media_start
rendition1for video, its rendition’s index in the open record’s ladder; for audio, 255
match1for video, the match of that rendition (capability-constraints §4.2) against the declaration in effect: bits 0 to 3 its exceedances, bit 4 none_within, every other bit 0; for audio, 0
hops1 + …a u8 count, at most 4, then each hop entry (§3.5) in strictly ascending order of role

3.5 Hop entries

fieldsizemeaning
role11 origin, 2 relay, 3 sink decode, 4 display
joules8u64: the hop’s energy for this segment, in microjoules; 0 is the unmetered marker
class1 + …its acquisition class: energy-measurement §2.7’s tag byte, then that tag’s evidence

A hop with no entry is absent (§2).

3.6 The observation record

fieldsizemeaning
signal11 visibility, 2 occlusion, 3 display power, 4 interaction
value1visibility: 1 visible, 2 hidden; occlusion: 1 not occluded, 2 occluded; display power: 1 on, 2 off; interaction: 1

What each signal is, and where a sink reads it, is §4.2.

3.7 The terminate record

fieldsizemeaning
reason11 hidden, 2 occluded, 3 display_off, 4 idle, 5 autoplay_unattended, 6 user, 7 ended (§4.3)
media8u64: the media time at which the sink stopped presenting the stream
segments4u32: the segment receipts of the log
totals1 + 14 × ma u8 count m, then for each hop role and class among the segments’ metered hop entries: role (u8), tag (u8), figures (u32, the entries of that role and tag) and joules (u64, their sum), in strictly ascending order of (role, tag)
unmetered1 + 5 × ka u8 count k, then for each role with entries that are the unmetered marker: role (u8) and count (u32), in strictly ascending order of role

The totals are the joules of each hop up to the termination: one per role and class, never one across two classes or two roles (live-metering §4.5 gives the reason). A role in neither list is absent from every segment, and a verifier names it so (§6.3).

3.8 Digest and signature

The digest of a record is SHA-256 over its bytes before sig. Its signature is Ed25519 by sink over the 15 bytes wai:stream-log (ASCII) and 0x01, followed by the bytes the digest covers, verified strictly, as SPEC §7 Signatures requires: a key that fails step 1 or 2 is bad_key, and a signature that fails a later step is bad_signature.

3.9 Reading

A reader reads exactly the bytes it is given as one record, as live-events §3.3 reads a record with WSL1 for WEV1: fewer than 4 bytes, or a magic of WSL1 with fewer than 5, is truncated; another magic is malformed; a version other than 1 is unknown_version; a kind other than 1 to 5 is malformed; running out of bytes before sig ends is truncated, and bytes after it are malformed. The class’s evidence is read as energy-measurement §2.7 writes it, and a tag other than 4 and 5 is followed by nothing; for tag 5 a reference length above 255 is malformed.

It then refuses, in this order:

  1. as reason_unknown, a terminate record whose reason is not 1 to 7;
  2. as energy_invalid, a hop entry whose class breaks live-metering §3.6’s energy_invalid rules over its joules: the tag is 0 exactly when joules is 0; the tag is 0, 2, 3, 4 or 5, never the legacy 1 (energy-measurement §2.6); for tags 4 and 5, relative_ppm and window_us are above 0 and joules × relative_ppm ≥ 288 676 (energy-measurement §2.3), tag 4’s filtering byte is 0, 1 or 2, and tag 5’s reference is UTF-8 and not only white space;
  3. as display_invalid, an open record of video or a display record whose declaration breaks capability-constraints §4.1: short from 1 to long, refresh_milli at least 1, bit_depth 1 to 16, hdr 0 or 1; and an open record of audio whose declaration is not 22 zero bytes;
  4. as record_invalid, a record that breaks any of these:
    • open: media, start and autoplay are 1 or 2, ua_policy 0 to 3; a video stream’s ladder holds 1 to 32 renditions, each with width, height and frame_rate_milli at least 1 and bit_depth 1 to 16; an audio stream’s holds none;
    • segment: objects at least 1; media_start below media_end; no bit of match above bit 4; at most 4 hop entries, each role 1 to 4, in strictly ascending order;
    • observation: signal 1 to 4; value 1 or 2 for signals 1 to 3, and 1 for signal 4;
    • terminate: each total’s role 1 to 4, tag 2 to 5 and figures at least 1, in the order §3.7 gives; each unmetered entry’s role 1 to 4 and count at least 1, in its order.

and then checks the signature (§3.8).

4. What a sink records

4.1 The session key

A sink MUST make a fresh Ed25519 key for each playback session, from its platform’s random generator, and MUST NOT derive it from an identifier of the viewer, the device or an account; it signs every log of that session with it. It presents the key’s public part to the senders of its streams with its first request, by a means this revision does not fix. It MAY make a key per stream instead, so that no two of its logs link.

4.2 The attention state

A sink records an observation when a signal changes, at the time it learns of the change, and one for each signal whose value at the open record is not the initial value below, with the open record’s t. Before any observation, a stream is visible, not occluded, its display on, and it has seen no interaction.

signalwhat it iswhere a page reads itwhere a native host reads it
1 visibilitywhether the document, or the application, is shown at allthe document’s visibility state, document.visibilityState, on each visibilitychange event (HTML §6.2)the platform’s state of its window or application: minimised, in the background, behind the lock screen
2 occlusionwhether the area the video is presented in can be seen, while the document is visiblethe element that presents the video does not intersect the viewport: an IntersectionObserver on it reports isIntersecting false (Intersection Observer §2.3)the platform’s report that the window is fully covered, where it makes one
3 display powerwhether the display is ona page has no interface for it; a display that goes off makes a document hidden on the platforms the HTML Standard’s note describes, which the page records as visibilitythe platform’s notice that the display slept or woke
4 interactionthat the viewer interactedan activation-triggering input event (HTML §6.4: a trusted keydown, mousedown, pointerdown, pointerup or touchend of the kinds that section lists)the platform’s input to the application

A sink records at most one interaction observation per second: the rules of §4.3 read only the time of the last one, and a finer record says more about the viewer than they need. An interaction observation carries nothing of the input.

The HTML Standard has a document hidden when its window is minimised, its tab is in the background, or a system element such as a task switcher covers it; Page Visibility Level 2’s note adds the lock screen. Neither requires a user agent to report a window that other windows cover, and Page Visibility Level 2 allows one to report it visible. A page records occlusion only for what it can see through Intersection Observer, which is the element’s place against the viewport and not what other content covers (its §1 sets pixel-accurate visibility outside its goals). A sink records what its platform reports and claims nothing it does not.

4.3 Termination

A sink that terminates a stream signs a terminate record, with the reason, and then:

The reasons, each with the state the log must show at the terminate record (§6.2):

reasonwhen
1 hiddena video stream whose document has been hidden for at least unseen_grace_ms
2 occludeda video stream whose presentation area has been occluded for at least unseen_grace_ms
3 display_offa video stream whose display has been off for at least unseen_grace_ms
4 idleidle_ms is above 0, and the last interaction, or the open record where there was none, is at least idle_ms before
5 autoplay_unattendeda stream that started by autoplay, with autoplay_grace_ms above 0, no interaction since it opened, and open for at least autoplay_grace_ms
6 userthe viewer stopped it
7 endedanything else: the stream ended, the session closed, the sink moved to another point of the stream or to another stream

A sink SHOULD terminate a video stream that has been unseen for unseen_grace_ms, and a stream that meets its own idle or autoplay rule, and MUST state as the reason one that the log supports. Hidden, occluded and display-off apply to video alone: a hidden document still plays audio someone may hear, so a sink that terminates the video of a presentation as hidden keeps its audio, whose own log goes on. The grace periods and timeouts are the deployment’s choice; a value of the open record is the sink’s statement of the policy it applied.

4.4 The autoplay setting

autoplay is the viewer’s setting at the sink, which a service offers its viewers (a player’s “autoplay off”). ua_policy is the user agent’s answer to navigator.getAutoplayPolicy for media elements (Autoplay Policy Detection §2.1), or 0 where the sink did not ask or has no such interface. A sink MUST NOT start a stream by autoplay when its viewer’s setting is off or the platform answered disallowed; a log that states such a start is refused autoplay_against_setting.

4.5 Segments and the panel

A sink signs a segment receipt for each segment it received and verified, as it receives it. For a MoQ track it chooses the span: the objects of one group, or of a span of media time at most 10 seconds long, ending at a group boundary. It states the rendition the segment is of, and the match of that rendition against the declaration in effect (capability-constraints §4.2), computed over the open record’s ladder.

The ladder is what the sender offers: an HLS multivariant playlist’s variant streams with their RESOLUTION, FRAME-RATE and VIDEO-RANGE (SDR as transfer 1, HLG as 18, PQ as 16; rfc8216bis-22 §4.4.6.2), a DASH MPD’s video representations, or the video tracks of a MoQ catalog with their width, height and framerate. Where the content description does not state a parameter, the sink states what it reads from the decoder’s configuration: the coded size, the bit depth of the codec string’s profile, and transfer 1 where the stream carries no colour description.

4.6 Hop figures

A segment receipt states, for each hop the sink holds a figure for, that figure with its class (energy-measurement §2):

A hop the sink knows took place but did not measure is the unmetered marker; it MUST NOT be written as a figure of zero joules, and a reader MUST NOT total it as one.

A hop figure is a figure energy-measurement governs (its §1): its class is inside the record’s signature with its evidence, and §3.9 step 2 holds it to that extension’s rules. This revision names no figure profile for it (energy-measurement §6.1), so no measurement claim can be made about one, as about a figure of live-metering’s per-hop records, which have none either. A sink that measured a figure keeps its bracket as energy-measurement §3 and §6 ask, and a revision that adds the profile adds its row to that table.

5. What a sender does

6. Verification

6.1 Steps

A verifier is given the records of one log, in order. For each record, in order, it runs these steps and stops at the first refusal, which it reports with the reason and the record’s index:

  1. Reading (§3.9): truncated, malformed, unknown_version, reason_unknown, energy_invalid, display_invalid, record_invalid, bad_key, bad_signature.
  2. After the termination: a record after a terminate record is after_termination.
  3. Order: the first record is an open record and no other is; each seq is the record’s index; each prev is the digest of the record before it, and 32 zero bytes for the first; each t is at least the t before it. Otherwise order_invalid; a log of no record is refused order_invalid at index 0. Each record’s sink is the first’s: otherwise sink_changed.
  4. The open record: start is 2 while autoplay is 2 or ua_policy is 3: autoplay_against_setting.
  5. A display record in the log of an audio stream: display_invalid.
  6. A segment receipt:
    1. its media_start is at least the media_end of the segment receipt before it, where there is one: otherwise media_order_invalid;
    2. for video, its rendition is below the ladder’s count, and for audio it is 255: otherwise rendition_unknown;
    3. for video, its match is the match capability-constraints §4.2 gives its rendition against the declaration in effect over the ladder, and for audio it is 0: otherwise match_mismatch;
    4. for video, its rendition is admissible: otherwise rendition_above_panel.
  7. The terminate record:
    1. where a segment receipt precedes it, its media lies from the last one’s media_start to its media_end, both included: otherwise media_order_invalid;
    2. its reason is hidden, occluded or display_off in the log of an audio stream: reason_not_applicable;
    3. the state of §6.2 at its t does not support its reason by the table of §4.3: reason_unsupported;
    4. its segments, totals and unmetered are not those the segment receipts before it give (§3.7): totals_mismatch.

A log that passes is accepted. One with no terminate record is accepted as open: the stream is still running, or its sink stopped without saying why.

6.2 The state at an instant

The state of a log at a record is read from the observation records before it. For each of visibility, occlusion and display power it is the value of the last observation of that signal, or the initial value of §4.2, and its since is the t of the earliest observation of the run of observations of that signal, ending with the last, that all state the same value, or the open record’s t where there is none. The last interaction is the t of the last interaction observation, or none.

A video stream is unseen at a record when visibility is hidden, occlusion occluded or display power off there. Reading the observation records in order from the initial state, its unseen since is the t of the observation after which it became unseen, from a state in which it was not, and has stayed unseen at every observation since.

At a terminate record at time T, with the open record’s t written T0:

6.3 The outcome

A sink that terminates at its grace has no unwatched segments, and one that never terminates has every segment it received past its grace among them. The figure is the log’s own account: it rests on the sink’s observations (§9), and its totals are per class, as every total here is.

7. Reasons

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

reasonraised at
reason_unknown§3.9 step 1
display_invalid§3.9 step 3, §6.1 step 5
record_invalid§3.9 step 4
after_termination§4.3, §6.1 step 2
order_invalid§6.1 step 3
sink_changed§6.1 step 3
autoplay_against_setting§4.4, §6.1 step 4
media_order_invalid§6.1 steps 6.1 and 7.1
rendition_unknown§6.1 step 6.2
match_mismatch§6.1 step 6.3
rendition_above_panelcapability-constraints §4.2, §6.1 step 6.4
reason_not_applicable§4.3, §6.1 step 7.2
reason_unsupported§4.3, §6.2, §6.1 step 7.3
totals_mismatch§3.7, §6.1 step 7.4

A verifier of this extension also raises truncated, malformed and unknown_version for a record that does not read, bad_key and bad_signature for its signature (the codes of live-events), and energy_invalid for a hop entry’s class (live-metering §9).

8. Conformance

live-conformance/termination/ holds the corpus, in one file, vectors.json, written from fixed seeds by wai_stream_termination_vectors gen. It has matches, each a display declaration, a ladder and a rendition with the match and the admissible set capability-constraints §4.2 gives them: every exceedance alone and together, both orientations, a panel smaller than every rendition, a tie for the least, and a high-dynamic-range panel with PQ and HLG renditions. And it has logs, each a list of records with the outcome of §6.3:

A conforming verifier gives every match its match and every log its outcome, reason, index and facts. wai_stream_termination_vectors verify and the test tests/stream_termination.rs check that the file regenerates byte for byte, that wai-rs gives every case its stated outcome, that every reason of §7 refuses a log, and that §7 lists exactly the reference’s codes. live-conformance/stream_termination_verify.py is the second implementation, written from this text and capability-constraints §4: it shares no code with wai-rs, verifies signatures with WAI’s strict rule written from RFC 8032 (claim-conformance/_ed25519.py), gives every case the same outcome, and requires every mutated copy of the corpus to fail. The page runs the corpus through its wasm build, and its own records are verified on the host, in joule-moq-web’s end-to-end scenario termination: a page whose tab goes to the background ends the video it receives with a terminate record, keeps the audio, and opens a new log when it is in front again.

9. Security and privacy considerations