549 lines
34 KiB
Markdown
549 lines
34 KiB
Markdown
# 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.
|