Skip to main content

WAI Extension: Native Sink

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

Status: Draft. What a native host, an application that plays a stream through its platform’s own player, does so that the stream is verified where it carries WAI’s receipts and evidenced as stream-audit evidences a page’s playback. The platform’s player decodes on the platform’s media engine and is not changed; the sink sits on the path its requests take and, for one playback session: verifies a media playlist against its playlist receipt and the chain of its live revisions (http-delivery §8.6), holds an object whose name states its content hash to that hash, and tells the host, object by object, whether it may hand the bytes to the player (§4); signs, under a fresh key per session, what stream-audit has a player sign: settlement acknowledgements of every object it received, a presentation claim for each ad the stream marks, and the session record, with the energy of each role and its acquisition class or the unmetered marker (§5, §7); ends a session nobody can see, or whose viewer has gone idle (§6); and hands the host the bundle for the owner’s collector. One C interface serves every native platform (§9). No new record, no new transport and no change to the wire format. The reference implementation is the native_sink module of wai-rs and its C interface ffi_sink (feature native_sink, header include/wai.h); the Swift package wai/wai-sink-swift integrates it with the native player of the platforms whose language it is, and the Kotlin library wai/wai-sink-android with the media player library of the phone and television platform whose applications are written in Kotlin; the corpus is live-conformance/native-sink/, and its second implementation is live-conformance/native_sink_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts this revision relies on: the WAI texts it cites; draft-pantos-hls-rfc8216bis-22 §4.4 (the playlist tags a sink reads: EXTINF, EXT-X-MAP, EXT-X-KEY, EXT-X-SESSION-KEY, EXT-X-PROGRAM-DATE-TIME, EXT-X-DATERANGE, EXT-X-ENDLIST, EXT-X-STREAM-INF) and §4.2 (attribute lists); RFC 3339 §5.6 (date-times); RFC 3986 §5.2 and §6.2.2 (resolution and normalization); RFC 6454 §6.2 (the ASCII serialization of an origin); RFC 9110 §8.4 (content codings); RFC 8032; FIPS 180-4; and the BLAKE3 specification. A section number in this text is this extension’s unless it names another document.

1. Scope

In scope: the session and its configuration (§3); what a sink reads of a playlist, how it verifies a playlist and an object, and what a host does with the answer (§4); the observations a host gives and the log they make (§5); the rule that ends a session nobody can see (§6); the energy a native host states (§7); the reasons (§8); the C interface (§9); and conformance (§10).

Out of scope: a transport; the records the sink signs, their verification and the channel report, which stream-audit defines; the playlist receipt and its chain, which http-delivery §8.6 defines; the player itself and its decoder; the collector; and how a host routes its player’s requests through the sink, which each platform’s integration states (§4.4).

Relationship to the core spec: this extension registers nothing. It is a profile of stream-audit for a native host, with verification added from http-delivery and packaging, and the C interface that every native integration shares.

2. Terms

3. The session

3.1 Configuration

A host opens a session with a JSON object of these members; channel and owner are required, every other has the default shown, and a member not listed, or a value of another type, is refused (native_config_invalid):

membervaluedefault
channelthe channel’s name, as stream-audit §3.1 has it—
ownerthe owner’s key, 64 lowercase hexadecimal digits—
presentation"live", "on_demand", or null to take it from the first media playlist taken: on demand when it has EXT-X-ENDLISTnull
window_usthe acknowledgement window of stream-audit §4.2, at least 1 000 00010 000 000
ad_classesEXT-X-DATERANGE classes the owner treats as ads (stream-audit §6.1)[]
content_addressedtrue when the owner’s packager names segments by the SHA-256 of their bytes (packaging §7), so that such names hold under a playlist with no receipt (§4.3)false
require"nothing", or "signed": every media playlist sealed by its receipt, and every object listed by one (§4.2)"nothing"
unseen_grace_mshow long a video stream stays unseen before the session ends as hidden, 0 for never (§6.1)10 000
idle_mshow long with no interaction before it ends as idle, 0 for never (§6.1)0

3.2 The session key

A sink MUST make a fresh Ed25519 key for each session from its platform’s random generator, as stream-audit §3.2 requires, and MUST NOT derive it from an identifier of the viewer, the device or an account. The C interface takes a 32-byte seed in its place for conformance vectors and tests only (§9); a host MUST NOT pass one for a viewer’s session. The session is stream-audit §3.2’s, over the channel id and the key.

3.3 Lifetime

The session’s log opens with a start observation at the time the session opens (§5.2). The session ends when the host ends it (§6), and then signs its records once. Every call after that is refused (native_session_ended), so no record can be signed twice for one session.

4. Verification

4.1 Reading a playlist

A sink reads a playlist as a player does, line by line (a line ends at LF; a CR, space or tab before it is not part of it), and refuses (playlist_malformed) one that is not UTF-8, is longer than 16 MiB, or whose first line, after an optional byte order mark, does not begin #EXTM3U. A playlist with an EXT-X-STREAM-INF or EXT-X-I-FRAME-STREAM-INF line is a multivariant playlist, and the sink takes nothing from it but that. Of a media playlist it reads:

An attribute list is read as draft-pantos-hls-rfc8216bis-22 §4.2 writes it: a name of upper-case letters, digits and -, =, and a quoted string or a value up to the next comma. A date-time is RFC 3339 §5.6’s date-time with an upper-case or lower-case T and Z, its fraction truncated to the millisecond. These readings are the ones stream-audit’s integration reads, so that the page and the native host name the same segments and the same ads.

4.2 A playlist and its receipt

Before it hands a media playlist to the player, the host fetches the playlist’s receipt file, mr/<BLAKE3 of the playlist's bytes>.json resolved against the playlist (http-delivery §8.6), and gives the sink both; a receipt that is not served is given as none. Then the sink, in order:

  1. reads the playlist (§4.1);
  2. with a receipt: checks the playlist against it as http-delivery §8.6 checks one (playlist_receipt_malformed, playlist_receipt_mismatch, playlist_receipt_invalid), and then takes it into the chain of receipts it holds for the playlist’s URL key, as that section takes a live revision (playlist_receipt_unchained, playlist_receipt_parent_missing, receipt_name_mismatch, playlist_receipt_walk_exceeded, playlist_listing_rebound). Where the chain needs a parent receipt the sink does not hold, it answers with that parent’s name, mr/<hash>.json, and takes nothing: the host fetches it relative to the playlist, gives it to the sink (or says it was not served), and gives the playlist again;
  3. with no receipt: refuses it (playlist_unsigned) when the configuration requires signing;
  4. takes it: the playlist is signed when its receipt verified and chained, and listed otherwise. Its segments, its initialization section, its key URIs and its marked ads join the session, each segment and initialization section listed by its URL key, as signed or listed.

A refused playlist joins nothing, and the host MUST NOT hand it to the player. A multivariant playlist is listed, never signed: http-delivery §8.6 signs media playlists only.

4.3 An object

The host gives the sink every response body the player’s requests received, with its content codings removed (RFC 9110 §8.4), its URL after redirects, the time its body was complete, its status, and whether it is media where it knows (an initialization or media segment; otherwise the playlists it took say so). The sink:

  1. keys: an object whose URL key is a key URI a playlist named is a key or licence response: it is not acknowledged, not logged and not checked (stream-audit §4.1);
  2. what its name states: the object’s content hash is checked against the hash its URL states, and an object whose bytes are another’s is refused (object_digest_mismatch):
    • a cid: the one the playlist line that lists the object states, its fragment’s cid (http-delivery §3.1) or else a last path segment <64 hex>.wai (http-delivery §5.2); else the one the object’s own URL states in the same way; against the object’s jwp-receipts §2.1 content hash;
    • a SHA-256: a last path segment of 64 lowercase hexadecimal digits, a dot and an extension of letters and digits other than wai, the name a floor fragment has in packaging §7 (floor/<id>/<SHA-256>.m4s), against the SHA-256 of the bytes, where a signed playlist lists the object or the configuration says content_addressed. Under a listed playlist and without it, such a name binds nothing: a stream another service packages is never refused for what its names look like;
  3. signing required: under a configuration that requires signing, an object that no signed playlist lists, and that is not itself a playlist the session took, is refused (object_unsigned);
  4. a playlist’s body: an object that is the body of a playlist the session was given (by URL key and BLAKE3) is listed as that playlist was, and refused when it was refused;
  5. received: every object but a key, refused or not, is acknowledged and logged as stream-audit §4 and §7 say. An acknowledgement states what was received, not what was presented.

The answer states the object’s SHA-256; its cid, where its URL states one; how it is listed (signed, listed or unlisted); what bound its bytes (cid, sha256 or none); whether it is media; whether it is a key; and the refusal, or none.

4.4 What a host does with an answer

A host routes the player’s requests through the sink: a server on the loopback interface that the player is given in place of the origin, or the platform’s hook for loading a player’s resources, whichever the platform lets the player use for every request of an HTTP presentation. For an answer that refuses, the host MUST NOT hand the bytes to the player; it answers the player’s request with a failure (the loopback server with status 502), and the player reacts as it reacts to a failed request. A host MAY fetch the object again from another origin the presentation names; the second response is an object of its own. A host MUST NOT answer the player with bytes the sink was not given.

What a verification establishes is http-delivery §11.6’s: an object listed by a signed playlist and bound by its name is what the playlist’s signer published; one listed, only what the origin that served the playlist named. Nothing here makes a third party’s stream signed: such a stream is acknowledged, and evidenced, as stream-audit says.

5. Observations and the log

5.1 Observations

While the player presents, the host gives the sink an observation at least every 200 ms, and on each change of playback, visibility, audibility, rate or position, as JSON:

membervalue
t_uswhen it observed
playingpresenting at normal speed: not paused, not stalled, not ended, rate 1
visible_bpthe fraction of the player’s area in view, in basis points (0 to 10 000, a number rounded to an integer): 0 while the application is not shown (§6.2)
audibleunmuted at a volume above 0
program_msthe program time of the position, or null
seekedthe position moved by a seek since the last observation
interactionthe viewer interacted since the last observation (§6.2)

The sink builds the record’s spans from them as stream-audit §5.2 does, and each marked ad’s claim spans as ad-presentation §10 and stream-audit §6.3 do. An observation with a member not listed, or t_us or visible_bp missing, is refused (native_input_invalid).

5.2 The log

The sink keeps stream-audit §7.1’s observation log. It opens with start when the session opens (§3.3); the host logs present when the player shows its first frame, stall-start and stall-end around each stall, and end or abandon when it stops. Where the host logged no present, the first observation that is playing logs it; where it logged neither end nor abandon, ending the session logs end for a session that ended ended and abandon for any other. An event of another name is refused (native_input_invalid). A log’s times never decrease and nothing follows its end or abandon (delivery-telemetry §7.1): an entry made at a time before the last entry’s, which a host’s player and its requests, on threads of their own, make, is logged at the last entry’s time; and after end or abandon the log takes nothing, while an object received then is still verified and acknowledged. The host sends the log’s Event-mode CMCD reports to the owner’s collector as it plays (stream-audit §7.2).

6. Ending

6.1 The termination rule

A sink ends a session that nobody can see or that nobody attends to, by the host’s configuration:

The answer to the observation that meets a rule names it. The host then MUST stop the player’s requests for the stream (it stops the player or removes its item, so no further segment is fetched) and end the session with that reason (stream-audit §8.2). A host that keeps the audio of a presentation playing while its video is hidden ends the video’s session and keeps the audio’s.

6.2 What a native host observes

whatwhere a native host reads it
shownthe platform’s state of the application and its window: in the foreground and active, not minimised, not behind the lock screen; visible_bp is 0 otherwise. A window the platform shows without the input focus (one of a split screen, a picture-in-picture window, one under a translucent window) is shown: “active” is the platform’s state of being presented to the viewer, not of holding the focus
in viewthe player’s area against the window’s visible area, and the platform’s report that the window is fully covered, where it makes one
displaythe platform’s notice that the display slept, which makes the application not shown
interactionthe platform’s input events to the application; at most one observation with interaction per second, carrying nothing of the input
audiblethe player’s mute state and volume, and the platform’s audio route where it reports none

A host states what its platform reports and claims nothing it does not: a platform that does not report occlusion has its window’s place on the screen, not what other windows cover.

7. Energy

A session opens with the unmetered marker for the sink decode (role 3) and the display (role 4), and no entry for the origin (role 1) or the delivery (role 2): stream-audit §8.1’s rule for a sink with no meter. A host states a role’s figure with its acquisition class only when a counter or a model covers that role’s work (energy-measurement §2), replaces a marker with it, or removes a role’s entry where it knows nothing of the role. A figure of 0 with a class, or above 0 with none, and a class energy-measurement §2.7 refuses, are refused (native_input_invalid).

The platform’s player decodes on the platform’s fixed-function decoder, in a process of the platform’s, which no energy counter an application can read covers. So a native host that plays through it states the sink decode as the unmetered marker, never as a figure of its own process’s counter: that counter covers the host’s own work (its requests, its hashing, its signing), not the decode. A host that decodes in its own process, a WAI-native capability decoded through wai-rs for example, states the reading of a counter that covers that process, with its class and evidence. The display is the marker unless the host holds a figure for the panel with its class.

8. Reasons

reasonrefused when
native_config_invalidthe configuration does not read (§3.1)
playlist_malformeda playlist is not UTF-8, is longer than 16 MiB, or does not begin #EXTM3U (§4.1)
playlist_receipt_malformedthe receipt file does not read (http-delivery §8.6)
playlist_receipt_mismatchthe playlist is not the one its receipt renders and hashes (http-delivery §8.6)
playlist_receipt_invalidthe receipt does not verify (http-delivery §8.6)
playlist_receipt_unchaineda live revision’s receipt does not chain to the head (http-delivery §8.6)
playlist_receipt_parent_missinga parent receipt the chain needs is not served (http-delivery §8.6)
receipt_name_mismatcha parent receipt’s hash is not the name it was fetched by (http-delivery §8.1, §8.6)
playlist_listing_rebounda revision lists another object under a URL or sequence number the chain bound (http-delivery §11.5)
playlist_unsigneda media playlist has no receipt where the configuration requires one (§4.2)
object_digest_mismatchan object’s bytes are not the content hash its URL states (§4.3)
object_unsignedan object no signed playlist lists, where the configuration requires one (§4.3)
native_input_invalidan observation, an event, an object’s description or an energy entry does not read (§4.3, §5, §7)
native_session_endeda call after the session ended (§3.3)

A sink gives every other refusal of http-delivery §8.6 by its code (playlist_receipt_walk_exceeded among them). A refusal of a playlist or an object is an answer, not an error: the call succeeds and the answer says the bytes are not to be presented.

9. The C interface

include/wai.h declares it; libwai built with feature native_sink exports it, for every native target, with no codec library (cargo build --release --no-default-features --features native_sink). A session is an opaque wai_sink_t. Inputs are bytes, NUL-terminated UTF-8 strings and JSON; outputs are JSON or bytes in a wai_buffer_t the caller frees with wai_buffer_free. Every function returns a status: WAI_OK (0); WAI_ERR_INVALID_ARG (1) for a null pointer or a length that does not fit; WAI_ERR_PANIC (4), never an unwinding across the boundary; WAI_ERR_SINK_INPUT (8) for an input that does not read, wai_last_error beginning with its reason; and WAI_ERR_SINK_ENDED (9) after the session ended. A session is safe to call from any thread: each call holds its lock. WAI_SINK_ABI_VERSION is 1 in this revision; wai_sink_abi_version() returns the library’s, and a host checks it before it opens a session. A later revision adds members to the JSON it writes and functions to the interface, and does not change those of a version.

functiondoes
wai_sink_open(config_json, seed, seed_len, start_us, &out)opens a session (§3); seed null for a fresh key
wai_sink_free(s)frees it, signing nothing
wai_sink_info(s, &out){"channel", "owner", "player", "session", "start_us", "presentation", "ended"}
wai_sink_receipt_name(playlist, len, &out)mr/<BLAKE3>.json, the receipt file to fetch beside a playlist
wai_sink_playlist(s, url, bytes, len, receipt, receipt_len, &out)§4.2; receipt null for none. {"kind", "listing", "receipt", "receipt_name", "step", "need_parent", "refused", "present", "segments", "ads"}
wai_sink_playlist_parent(s, url, name, bytes, len)hands over the parent need_parent named; bytes null when it was not served
wai_sink_object(s, meta_json, bytes, len, &out)§4.3; meta_json {"url", "t_us", "media", "status", "cmsd_static", "cmsd_dynamic"}. {"sha256", "cid", "listing", "bound", "media", "key", "acknowledged", "refused", "present"}
wai_sink_event(s, name, t_us)§5.2
wai_sink_observe(s, obs_json, &out)§5.1; {"end": null} or {"end": "hidden" or "idle"} (§6.1)
wai_sink_energy(s, entry_json)§7; {"role", "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref"}, the class as jwp-receipts §2.4.5 writes it, or {"role", "absent": true}
wai_sink_attach(s, name, bytes, len)attaches a record (stream-audit §8.3)
wai_sink_take_cmcd(s, &out)the Event-mode CMCD batch of the log since the last call
wai_sink_end(s, ended, t_us, &out)ends the session (§6): the bundle of stream-audit §9
wai_audit_verify_bundle(bundle, len, owner, channels_json, &out)stream-audit §10’s outcome, as the owner’s collector reaches it
wai_sink_now_us()the host’s clock, for a host with none of its own to hand

10. Conformance

11. Security and privacy considerations