WAI Extension: Player Integration
Mirrored from the canonical text at commit c5e7133e ().
Status: Draft. Registers no capability and no record. It says what a player integration must do: code that sits between a player that fetches CMAF media over HLS or DASH and that player’s media pipeline, and holds what the player fetched to the receipts WAI serves beside it. It adds no wire form. What is verified is what packaging, http-delivery and live-http-egress already define; what is reported is what delivery-telemetry already defines. This text states the order (verify, then present), the answer to a mismatch (refuse), what is and is not covered, and what may be reported. The reference integration is
integrations/web-players/: one core, and a plugin for each of three open-source web players, with the checks themselves inwai-web(featureplayer), which iswai-rscompiled to wasm32. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.
1. Scope
A WAI package’s floor is plain CMAF with HLS and DASH presentations (packaging §7, §8), and a room’s CMAF tracks are served live as low-latency HLS and DASH (live-http-egress §5, §6). Any player plays them, and a player that knows nothing of WAI plays them unverified. A player integration is how such a player verifies: it is given each thing the player fetched, checks it, and only then lets the player have it.
This text covers an integration in a web page, where the player loads media itself and gives it to Media Source Extensions. It does not cover a sink that decodes WAI objects (browser-sink), and it does not cover playback a page cannot see into: a media element given an HLS address plays through the user agent’s own code, no script is shown the bytes, and nothing here applies to it.
2. Terms
- Integration: the code between a player’s loading and its media pipeline.
- Handed: bytes are handed to the player when the player’s own code may append them to a media source, decode them or present them.
- Verdict: an integration’s answer for one thing a player fetched: verified, refused with a reason, or uncovered (§5).
- Source: what the page says it plays, and the key it trusts for it: an on-demand package and its signer’s key, or a room’s owner at an egress, the owner’s key and the room’s id. An integration is given these by the page. It does not learn a key from the media it checks.
3. Verify before presenting
3.1 An integration MUST NOT hand a player a media segment, a part or a header of its source before that thing is verified, and MUST NOT hand it one that was refused. Verifying after the fact, beside playback, is not conformance.
3.2 An on-demand package. An integration holds a floor’s files to the package receipt
as packaging §7 (the CMAF form) and §9 bind them: the receipt verifies
against its indexes and its signature (§9); it is signed by the key the page trusts;
policy.json hashes to the receipt’s policy digest (packaging §3); floor.listing hashes
to the digest the policy pins; and a header is the file the listing names for its floor
rung, and a media segment is named by its SHA-256 and is one of the listing’s for that rung.
A package whose policy pins no listing binds its segments’ names only through a verifier
that rebuilds them from the floor objects, which an integration does not do: it refuses
every segment of such a package (floor_not_listed).
3.3 A package’s playlist and MPD. The classical media playlist of a floor rung and the classical MPD list that rung’s header and its segments. An integration holds what they list to the listing: the header named is the rung’s, and the segments named are the listing’s for that rung, all of them, in the listing’s order. A playlist or an MPD that lists anything else, in another order, or by any other addressing than whole files, or an MPD that takes any part of itself from another address, is refused. A multivariant playlist names only playlists of the presentation, each of a floor the package holds. The durations a playlist states, a multivariant playlist’s variants and their bit rates are signed by nothing unless the package states its playlists (§3.9, §5).
3.4 A live part. A part an egress serves is held to its receipt index (live-http-egress §8.1) as http-delivery §8.2 checks a delivery item, over the bytes the player was sent, the group receipt fetched by its name (http-delivery §8.1), under these further conditions:
- the group receipt is signed by the owner’s key the page trusts (
signer_not_expected); - the object receipt’s group and object are those the part was asked for under
(
receipt_other_object): a part’s name states its Location (live-http-egress §4.1), and a receipt of another object, however valid, is not this part’s; - every part of one track verifies under one receipt alias, the one the page gave for the
track or, where it gave none, the first the track’s parts verified under
(
receipt_other_track).
An integration joins (jwp-receipts §3): it anchors each chain at the
first group it verifies, and recovers a missing parent as http-delivery §8.1 states. A
player that starts behind the live edge also asks for a group below every group the chain
holds. What links that group to the chain is above it, so an integration fetches downward
from the lowest group it holds, each group receipt by the link the one above it names, at
most 64 of them, and offers each to the chain, which admits it as history (jwp-receipts §3)
or not at all. A group that neither walk links to the chain is refused (parent_not_held).
3.5 A part held on its run. While a part’s group is open it has no receipt index, and
an egress that took running receipts serves the run beside it
(live-http-egress §8.4). An integration checks it as that section
states, against the room, the owner’s key, the track’s anchor digest and its receipt alias,
which the page gives it from the room’s catalog. A run that holds fewer records than the
part’s position needs does not cover the part (served_uncovered). An integration that
was not given what a run is read against refuses the part (running_not_configured).
3.6 A segment. A closed segment is cut at the lengths its parts list states (live-http-egress §8.3) and each piece is checked as §3.4 or §3.5 checks a part. A segment that is still growing (live-http-egress §6.3) is checked as it arrives: an integration hands the player a part’s bytes once those bytes, whole, verified, and holds back everything after the last verified part. Bytes left when the response ends that are no verified part are refused. An integration bounds what it holds unverified.
3.7 Protected media. An integration verifies the bytes as they were served, before
any decryption, and does not ask for a key. A protected live part is an object the publisher
signed in its protected form, so §3.4 to §3.6 apply to it unchanged. A protected
presentation of a package (packaging §7, the protected floor) is held
through its index, protection.json: the index names the package’s receipt and policy
digest, lists for each floor rung the package’s clear header and segments in the listing’s
order, and pairs each with a protected file; a protected segment is named by its SHA-256 and
is one the index pairs. The index is signed only where the package’s policy has a statements
table (§3.9); otherwise it is not (§5).
3.8 A live playlist or MPD. Every media playlist, multivariant playlist and MPD an
egress serves carries a statement in its response’s WAI-Statement header
(live-http-egress §8.5), signed by the egress’s statement key under the
owner’s delegation of it. An integration checks it as live-http-egress §8.5 has a client check
one, against the room’s id and the owner’s key the page gave, before the player is given the
playlist, and keeps what that section has a client keep: the last statement it accepted for
each playlist, each track’s alias, and the entries of the segments it accepted. So a playlist
changed on its way, another track’s playlist, one older than one the player was given, a delta
update that does not follow from what the player holds, and a segment restated with another
duration are refused before the player acts on them. A track’s alias is the one the page gave,
or the first a statement or a part verified under gave it, and a statement and a part that
disagree on it are refused (statement_other_track, receipt_other_track). An integration
that was not given the room refuses every live playlist (statement_not_configured).
3.9 A stated package. A package whose policy has a statements table carries a statement
beside each playlist, multivariant playlist and MPD, and its protected presentation beside each
of its own and beside protection.json (packaging §8, statements in a
package): <file>.sig, signed by the package receipt’s signer and naming the package receipt.
The policy is signed by the receipt, so an integration that holds the receipt (§3.2) knows the
statements are required. It checks each such file against the statement fetched beside it, as
that item has a verifier check one, before §3.3 and §3.7 hold what the file lists and before the
player is given it: a statement not served (statement_not_served), one that does not read, of
another file or other bytes, not signed by the package’s signer, of another package or stating
other tracks are refused with http-delivery §8.10’s and packaging §13’s reasons. A multivariant
playlist so stated is verified, not uncovered, and its variants are still held to the package’s
floors (§3.3).
4. Refuse on mismatch
4.1 A thing that does not verify is refused: it is not handed to the player, and the integration answers the player’s request as a failed load, through the player’s own failure path, so that the player’s retry and error handling run and the page’s error listener is told. An integration MUST NOT present refused media, in whole or in part, and MUST NOT replace it with other media.
4.2 A thing with no receipts is refused as one whose receipts do not verify: a part with
neither a receipt index nor a run (receipt_not_served), a package without its receipt
(package_not_served), a live playlist or MPD with no statement, or a stated package’s (§3.9),
(statement_not_served). An integration MUST NOT play unverified media by default, and
this text defines no setting that makes it do so.
4.3 An integration is asked about every playlist and every piece of media its player
loads, and refuses one that is not of its source (outside_source): a package’s playlist
binds only the names it lists (§3.3), and a live one’s statement binds its bytes, not where
what they name is served (§3.8), so media from elsewhere would otherwise be presented
unverified. It is not
asked about what is neither: a key, a licence, a clock.
4.4 What the viewer sees when media is refused is the player’s: its retries, its error, a stall. An integration documents it for each player it supports, and MUST make the reason available to the page.
5. What is not covered
A verdict of uncovered says that nothing signs a thing in this revision, and that the integration handed it on. An integration MUST report as uncovered, never as verified:
- a package’s multivariant playlist where its policy has no
statementstable. An integration requires only that it names playlists of the package’s own presentation, each of a floor the package holds; - a live track’s header, and a protected track’s header of each key period, which come from the catalog (live-http-egress §4.5, §13). A page MAY pin one by its SHA-256, and an integration then verifies it.
And these are outside any verdict:
- the durations and timing a package’s playlist or MPD states. A live one’s are bound by its statement to the owner’s word through the delegation (§3.8): what the egress wrote, it wrote, and a duration it restates otherwise is refused; that it is true of the media is still the egress’s word (live-http-egress §8.5). A stated package’s are bound to the package’s key (§3.9), and are the packager’s word in the same way;
- a protected presentation’s index where the package’s policy has no
statementstable, which is then not signed: a party that replaces both a protected segment and the index is not found by §3.7. What §3.7 gives is that every file the page plays is one the index pairs with the receipt’s clear floor; - what the player then does with verified bytes: its choice of rendition, its buffer, its cache, and what its decoder shows (browser-sink §13);
- whether the signer was entitled to sign (jwp-receipts §7): an integration checks against the key the page trusts.
6. What an integration reports
6.1 An integration reports nothing but delivery-telemetry’s
observations (§2 there), with exactly their members: start, a response for each header,
segment and part of its source, the first presentation, stalls, and end or abandon. It
adds no member, and no identifier of a person, a device, a browser or an account: sid is
fresh for each session and derived from nothing (delivery-telemetry §8).
6.2 A response observation’s receipt is the link hash of the receipt the bytes
verified against (the group receipt, or the package’s receipt hash), and is null for bytes
that were refused and for a part held on its run. Its content_hash is the content hash
(jwp-receipts §2.1) of the bytes received. Both are pointers, and neither
is evidence (delivery-telemetry §1). Its url is null unless the page states that its
addresses carry no token.
6.3 An observation has no member for a verdict, and an integration MUST NOT add one. What was verified, refused and left uncovered is the integration’s tally, which it keeps for the page.
6.4 An integration sends nothing anywhere. Where a session’s log goes is the page’s
(delivery-telemetry §8). It signs nothing: a reconstruct-session claim is a sink’s, under
a key its decoder host holds (jwp-receipts §2.4.12), and an integration
holds no key.
7. Reasons
An integration answers with the reasons of the texts it checks under wherever one applies: packaging §13’s for a package receipt, http-delivery §13’s and jwp-receipts’s for a part’s receipts, live-http-egress §3.5’s for a run, its §8.5’s for a live playlist’s statement, and http-delivery §8.10’s and packaging §13’s for a stated package’s (§3.9). It adds these:
| reason | what was refused |
|---|---|
package_not_served | a file the package’s chain needs (its receipt, an index, its policy) is not served |
floor_not_listed | the package’s policy pins no listing of its floor’s CMAF files |
signer_not_expected | the receipt is signed by another key than the one the page trusts |
segment_name_mismatch | a segment’s bytes do not hash to its name |
init_mismatch | a header is not the one the listing, or the page’s pin, names |
not_in_package | media the listing does not name |
playlist_not_listed | a playlist or an MPD lists other files than the listing’s, or in another order; a multivariant playlist names a playlist outside the presentation or of a floor the package does not hold |
playlist_malformed | a playlist or an MPD that addresses media otherwise than by whole files, or is not read |
protection_index_mismatch | a protected presentation’s index is not about this package’s clear floor |
receipt_other_object | a part’s receipt or run is of another object than the part was asked for under |
receipt_other_track | a part’s receipt is under another alias than its track’s |
served_uncovered | a part’s run stops short of it (live-http-egress §11 names the same fault of an egress) |
running_not_configured | a run cannot be read: the page gave no room, anchor or alias |
unverified_overflow | a growing segment sent more bytes than an integration holds before a part verified |
sidecar_too_large | a receipt index, a run or a parts list past what an integration reads |
outside_source | a playlist or media that is not of the source the page gave |
config_invalid | the page’s source cannot be read: a key that is not one, a name the source does not have |
statement_not_configured | a live playlist’s statement cannot be read: the page gave no room |
8. Conformance
A conforming integration meets §3, §4, §5 and §6. The reference integration
(integrations/web-players/) is tested in two ways:
- under Node, with no player (
test/core.test.mjs, run in CI): wai-pack’s packages (pack-conformance/multi/multi-opus-01, its protected presentation, and a package whose floor is not listed) and the traces oflive-conformance/http-egress/, with every reason of §7 that those can raise planted, and the session’s log read by wai-rs’s reader; every case oflive-conformance/egress-statements/, each playlist given with its header, reaching the corpus’s outcome; a stated package (pack-conformance/packs/video-statements-01, every case of itsstatement-mutations/that a client can meet, and its protected presentation with the index changed after signing and a statement stripped), each file given with its.sig; and each plugin attached to a stand-in for its player’s extension point (test/plugins.test.mjs), which must hand on exactly what was verified; - in a browser (
test/run.sh, run by hand on a host that has one): three open-source web players, each at a pinned release fetched from its project’s distribution at test time (players.lock.jsonnames them), each play the package on demand and a room through its egress, low-latency HLS and DASH, with parts held on receipts and on runs, and two of them a protected presentation and a protected room, held over the protected bytes, and each has faults planted: a changed byte, a playlist whose segments changed places, a package without its receipt, a part beside another part’s receipts, a part beside none, and a part served before its run covers it; and, in each player, a live playlist changed after it was signed and an older one served again after newer ones. The run records everything a page appends to its media source and requires the refused bytes absent from it; each fault is also run with the plugin off, where the same bytes must be found. A changed playlist names a decoy for every segment and part, so a player that acted on it asks for one: with the plugin, none is asked for; with it off, one is.
What verifying costs is measured where it is run and stated with its host and date; it is not a property of this text.
9. Security and privacy considerations
- The page chooses the key. An integration is only as right as the key the page gives it. A page that takes the key from the same origin as the media has verified that the media is consistent, not whose it is.
- Refusal is visible. A refused part stops or stalls playback. That is the intended outcome: a party that can change bytes on the path can deny playback, as it could by dropping them, and cannot make changed bytes play.
- What a session’s log reveals. The content hashes and receipt hashes of what a session fetched say what it watched, to whoever is given the log. An integration gives it to the page and to no one else, and carries no identifier (§6).
- Fetching receipts shows nothing new. The receipts are fetched from where the media was, by the same client.
- A growing segment is cut by hashes. Until a part verifies, an integration cannot tell a changed part from one that has not ended, so a changed byte in a growing segment is found when the part after it does not arrive whole, or when the response ends; nothing of it is handed on meanwhile.