open-standards/wai/extensions/stream-termination.md
Transaction Science f7d55b28a5 wai: the refresh rate a page measures, stated exactly; wording
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 13:53:23 -04:00

549 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Stream Termination
> 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](capability-constraints.md) §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](energy-measurement.md) §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/`](../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](../SPEC.md) §5):
a signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
Relationship to the other extensions:
- **[capability-constraints](capability-constraints.md) §4.** The display declaration, the
match and the admissible renditions. A segment receipt records the match (§3.4), and §6.1
step 6 refuses a segment of a rendition that was not admissible.
- **[energy-measurement](energy-measurement.md) §2.** Each hop figure's class, its tag and its
evidence (§2.7), and the unmetered marker, which is never a figure of zero joules.
- **[live-metering](live-metering.md).** A hop's own record of a room session is per session
and per hop; a stream log is per stream at one sink. The two are separate records, and
neither restates the other.
- **[ad-presentation](ad-presentation.md) §3, §10.** The session key is made as that
extension's is, and a sink that measures a fraction in view for an ad claim reads the same
observations this extension records.
- **[delivery-telemetry](delivery-telemetry.md).** Its observations are unsigned and
aggregated; a stream log is signed and is one stream's.
## 2. Terms
- **Sink**: the software that receives a stream and presents it: a page, or a native host.
- **Stream**: one sink's reception of one media track: a MoQ subscription to a track, or an
HTTP client's requests for the segments of one media type of a presentation. Its **media**
is video or audio.
- **Segment**: what a sink receipts as one unit of a stream: one HTTP segment, or the objects
of a MoQ track the sink received over a span of media time it chooses (§4.5).
- **Session key**: an Ed25519 key (RFC 8032) the sink makes for one playback session (§4.1).
- **Unseen**: a video stream is unseen while its document is hidden, its presentation area is
occluded or its display is off (§4.2).
- **Hop**: a party whose energy a segment's delivery and presentation cost, by its **role**:
1 origin, 2 relay, 3 sink decode, 4 display.
- **Unmetered marker**: a zero figure with class tag 0 (energy-measurement §2), never a figure
of zero joules. A hop the sink states nothing about is **absent**, which is not the marker
either: the marker says the hop took place and was not measured, and absence says nothing.
- *u64*, *u32*, *u8*: little-endian unsigned integers of that many bits; the class's evidence
is big-endian, as energy-measurement §2.7 writes it. A digest is SHA-256 (FIPS 180-4),
32 bytes. Times `t` are microseconds since 1970-01-01T00:00:00Z on the sink's clock; media
times are microseconds of the stream's media timeline.
## 3. The stream log
### 3.1 Records
A **stream log** is a sequence of records, each:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WSL1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `kind` | 1 | 1 open, 2 segment, 3 observation, 4 display, 5 terminate |
| `sink` | 32 | the session key, which signs the record |
| `seq` | 4 | u32: the record's index in the log, from 0 |
| `prev` | 32 | the digest of the record before it; 32 zero bytes for the first |
| `t` | 8 | u64: when the sink made the record |
| `body` | … | the kind's body (§3.2 to §3.7) |
| `sig` | 64 | §3.8 |
### 3.2 The open record
The first record of a log, and the only one of kind 1:
| field | size | meaning |
|---|---|---|
| `stream` | 32 | the stream: for a MoQ track, its track digest ([live-metering](live-metering.md) §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 |
| `media` | 1 | 1 video, 2 audio |
| `start` | 1 | 1: a user activation started it; 2: it started with none (autoplay) |
| `autoplay` | 1 | the viewer's autoplay setting at the sink: 1 on, 2 off (§4.4) |
| `ua_policy` | 1 | the platform's answer for autoplay (§4.4): 0 not asked, 1 `allowed`, 2 `allowed-muted`, 3 `disallowed` |
| `unseen_grace_ms` | 4 | u32: how long a video stream stays unseen before the sink terminates it (§4.3) |
| `idle_ms` | 4 | u32: how long with no interaction before it terminates the stream as idle; 0 for never |
| `autoplay_grace_ms` | 4 | u32: how long an autoplayed stream with no interaction runs before it terminates it; 0 for never |
| `display` | 22 | the display declaration (§3.3) |
| `ladder` | 1 + 14 × *n* | a 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:
| field | size | meaning |
|---|---|---|
| `long` | 4 | u32 |
| `short` | 4 | u32 |
| `refresh_milli` | 4 | u32 |
| `bit_depth` | 1 | u8 |
| `hdr` | 1 | 0 false, 1 true |
| `long_mm` | 4 | u32; 0 not known |
| `short_mm` | 4 | u32; 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):
| field | size | meaning |
|---|---|---|
| `segment` | 32 | the segment: for an HTTP segment, SHA-256 over its bytes as received; for a MoQ segment, SHA-256 over the content hashes ([jwp-receipts](jwp-receipts.md) §2.1) of its objects in the order received, concatenated |
| `objects` | 4 | u32: the objects it holds, at least 1; 1 for an HTTP segment |
| `bytes` | 8 | u64: their payload bytes as received |
| `media_start` | 8 | u64: the media time of its first sample |
| `media_end` | 8 | u64: the media time its last sample ends at, above `media_start` |
| `rendition` | 1 | for video, its rendition's index in the open record's ladder; for audio, 255 |
| `match` | 1 | for 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 |
| `hops` | 1 + … | a u8 count, at most 4, then each hop entry (§3.5) in strictly ascending order of `role` |
### 3.5 Hop entries
| field | size | meaning |
|---|---|---|
| `role` | 1 | 1 origin, 2 relay, 3 sink decode, 4 display |
| `joules` | 8 | u64: the hop's energy for this segment, in microjoules; 0 is the unmetered marker |
| `class` | 1 + … | 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
| field | size | meaning |
|---|---|---|
| `signal` | 1 | 1 visibility, 2 occlusion, 3 display power, 4 interaction |
| `value` | 1 | visibility: 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
| field | size | meaning |
|---|---|---|
| `reason` | 1 | 1 `hidden`, 2 `occluded`, 3 `display_off`, 4 `idle`, 5 `autoplay_unattended`, 6 `user`, 7 `ended` (§4.3) |
| `media` | 8 | u64: the media time at which the sink stopped presenting the stream |
| `segments` | 4 | u32: the segment receipts of the log |
| `totals` | 1 + 14 × *m* | a 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`) |
| `unmetered` | 1 + 5 × *k* | a 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](../SPEC.md) §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.
| signal | what it is | where a page reads it | where a native host reads it |
|---|---|---|---|
| 1 visibility | whether the document, or the application, is shown at all | the 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 occlusion | whether the area the video is presented in can be seen, while the document is visible | the 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 power | whether the display is on | a 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 visibility | the platform's notice that the display slept or woke |
| 4 interaction | that the viewer interacted | an 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:
- **MUST** end its request for the stream: unsubscribe from the MoQ track, or request no
further segment of it;
- **MUST NOT** sign any record of the log after the terminate record. A segment that arrives
after it, already in flight, is discarded and not receipted. A stream terminated as hidden
sends no further segment receipt;
- **MAY** open a new log for the same stream when the condition ends (the document visible
again, an interaction), with a new open record; a log is never resumed.
The reasons, each with the state the log must show at the terminate record (§6.2):
| reason | when |
|---|---|
| 1 `hidden` | a video stream whose document has been hidden for at least `unseen_grace_ms` |
| 2 `occluded` | a video stream whose presentation area has been occluded for at least `unseen_grace_ms` |
| 3 `display_off` | a video stream whose display has been off for at least `unseen_grace_ms` |
| 4 `idle` | `idle_ms` is above 0, and the last interaction, or the open record where there was none, is at least `idle_ms` before |
| 5 `autoplay_unattended` | a 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 `user` | the viewer stopped it |
| 7 `ended` | anything 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):
- **origin**: the figure the segment's object receipts carry in their labelled leaves
(jwp-receipts §2.2), composed over the segment's objects as jwp-receipts §2.4.1 composes a
group total; the unmetered marker where every object's figure is the marker; absent where a
figure is unlabelled, since an unlabelled figure has no class;
- **relay**: a figure a relay states for the segment, where its deployment gives the sink
one; this revision defines no such statement, so a relay hop is usually absent;
- **sink decode**: the sink's own figure for decoding the segment, measured as
energy-measurement §3 and §4 have it, or the unmetered marker where the sink decoded it with
no meter;
- **display**: a figure the sink's platform states for presenting the segment, or the
unmetered marker. This revision computes none.
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
- A sender that holds a sink's display declaration **MUST NOT** send it a rendition that is
not admissible (capability-constraints §4.2).
- A sender that receives a terminate record, or the end of the request that goes with it
(§4.3), **MUST** stop sending the stream to that sink; it **MAY** keep sending the same
presentation's other streams, its audio among them.
- A sender **MAY** verify a log it is given (§6) and keep it with its own records of the
session. A log is the sink's statement: its observations and figures rest on the sink's key
(§9).
## 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`:
- `hidden` is supported when visibility is hidden and `T` − since ≥ `unseen_grace_ms` × 1000;
- `occluded` when occlusion is occluded, and `display_off` when display power is off, each
with the same bound;
- `idle` when `idle_ms` is above 0 and `T` − (the last interaction, or `T0` where there is
none) ≥ `idle_ms` × 1000;
- `autoplay_unattended` when `start` is 2, `autoplay_grace_ms` is above 0, there is no
interaction observation, and `T` − `T0` ≥ `autoplay_grace_ms` × 1000;
- `user` and `ended` always.
### 6.3 The outcome
- **refuse**: the reason and the record's index (§6.1).
- **accept**: the number of records and of segment receipts; whether the log is open or
terminated, with the reason and `media` of its terminate record; the **totals** and the
**unmetered** counts, by role and class, recomputed from the segment receipts; the roles
**absent** from every segment receipt; for video, the segment receipts whose rendition was
within the declaration and those served under `none_within`; and, for video, what was
delivered while the stream was unseen:
- **unseen time**: the microseconds from the open record's `t` to the last record's during
which it was unseen: the sum, over each run of observations in which it became unseen, of
the time from the observation that made it unseen to the next observation that made it
seen, or to the last record's `t`;
- **unwatched segments**: the segment receipts at which the stream was unseen, with `t` −
unseen since ≥ `unseen_grace_ms` × 1000: made after the sink's own grace had run out, with
their totals and unmetered counts by role and class as above.
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:
| reason | raised 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_panel` | capability-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/`](../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:
- **Accepted**: a video stream terminated as hidden after its grace, with a segment received
past the grace counted unwatched, its hops metered under two classes and one unmetered;
terminated as occluded and as display off; as idle, after interactions; as autoplayed with
no interaction, its setting on and the platform's answer `allowed-muted`; stopped by the
viewer; an audio stream that goes on while its document is hidden and ends; a log still
open; a panel smaller than every rendition, served the least; display records that change
the declaration mid-stream, down and back up, and the sink moving its rendition with them; a
hidden document observed hidden twice, its grace running from the first; and a
high-dynamic-range panel served a PQ rendition.
- **Refused**: each reason of §7 at least once, each log a control of one defect, among them a
segment receipt after a termination as hidden (`after_termination`), a termination as hidden
before the grace and one while visible (`reason_unsupported`), a rendition above the panel
while one within it was offered, and one kept after a display record shrank the panel
(`rendition_above_panel`), and a total summed across two classes (`totals_mismatch`); and
the codes of reading and of the signature.
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
- **A log is the sink's statement.** Its signature and its chain make it attributable and
ordered, and keep a record from being removed, reordered or added after the termination
without the key. They do not make an observation true: a sink can state that a document was
hidden when it was not. A verifier checks that the reason agrees with the observations the
log holds, which binds the sink's statements to one another, not to the screen.
- **What a log reveals.** When a viewer's tab was in the background, when the viewer
interacted (at most once a second, nothing of the input), the panel's size and depth, the
autoplay setting, and the stream. A service that collects logs learns that much about each
playback session it collects from. The session key is fresh and derived from nothing that
identifies the viewer (§4.1), so logs of two sessions do not link through it; a sink that
makes a key per stream keeps two streams of one session apart as well. Where an operator
needs to record a legal basis for collecting logs, jwp-receipts §2.4.6's `lawful-basis`
claim is the place.
- **A display declaration is a fingerprint surface.** Its members are among what a page
already exposes (CSSOM View, Media Queries), and its refresh rate is measured, not read.
Physical size is not available to a page and is 0 there.
- **Energy figures.** A per-segment figure measured at the sink is a side channel on what the
sink did, as browser-sink §13 says of a page's figures; a page receives one only from a meter
its host attached. A figure's class rests on the session key alone: no key log lists that key
with an energy authority (jwp-receipts §7.5), so a hardware class in a log is the sink's
statement and nothing confirms it.
- **Not claimed.** A termination saves the energy of the segments it stopped, which this
record does not state: what was not sent was not measured. The unwatched totals are what
was delivered and not seen, by the sink's account, per class.