open-standards/wai/extensions/stitched-presentation.md
Transaction Science 4623548d63
Some checks failed
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
wai: stitched-presentation §7.2 step 5 states its refusal plainly
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 21:56:23 -04:00

43 KiB
Raw Permalink Blame History

WAI Extension: Stitched Presentation

Status: Draft. Registers the record wai.video.stitched_presentation: a player's signed claim, WSC1, that it presented one ad another party wrote into the stream, either stitched into the segments of an HLS or DASH presentation or served as an HLS interstitial, beside the pod-input record, WSI1, that the stitcher or a proxy in front of the ad server signs for each ad decision. A claim names its ad by the marker the stream carries for it (an EXT-X-DATERANGE tag, an asset of an interstitial's asset list, or an Event of a DASH EventStream) and the ad identity that marker carries, in the manifest whose SHA-256 it states; it carries ad-presentation's spans of visible and audible play, its viewability rules and quartile, and its device attestation; and, for an ad shown in a composited frame (a squeeze or a double box of live-compose §5.9), the share of the picture the ad's region holds. The pod-input record states the digest of the ad server's response, the ads it chose with their markers, creatives and prices, the avail, the frame and the measurement terms. A verifier recomputes, from the claims that verify, each ad's presentations, the quartiles they reached and the spend that counts; the choice stays the ad server's. No new transport. The reference implementation is the stitched_presentation module of wai-rs (feature stitched_presentation); the corpus is live-conformance/stitched/, and its second implementation is live-conformance/stitched_presentation_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision, each at its source:

  • Video Ad Serving Template (VAST) 4.3, December 2022: §3.1 to §3.4.4, §3.4.8, §3.6 to §3.7.1 and §3.8 to §3.8.1, beside the sections ad-presentation read.
  • Server-Side Ad Insertion and OTT Guidance, August 2021, §1.2, §2 and §3.
  • For HLS and DASH, the texts live-ads read, at the revisions it names: draft-pantos-hls-rfc8216bis-22 §4.2 (attribute lists), §4.4.5.1 (EXT-X-DATERANGE) and Appendix D (interstitials and the asset list); ISO/IEC 23009-1:2026's MPD schema (its EventStream and Event). Nothing here depends on a part of them live-ads did not read.
  • RFC 8259 (JSON) and XML 1.0, for the subset of §3.5.

A section number of one of these texts is that revision's.

1. Scope

In scope: the marker of an ad in a stream, its body and digest, and how each is read from a playlist, an asset list or an MPD (§3); the pod-input record, its rules and signature, and a form-1 wai.video.ssai receipt as its response (§4); the stitched claim, its session, its rules, its signature and its attestation challenge (§5); composited frames (§6); verification: each record's and each claim's verdict, each ad's presentations and quartiles, and the spend that counts, by currency (§7); and the reference producer, the tool and the service (§8).

Out of scope: a transport; how a player's session key reaches the signer (§4.6 says what the signer does with it, not how it arrives); the ad decision itself, which stays the ad server's; how a stitcher conditions a creative into segments; the contents of a winning bid's document and of a VMAP document, which a record binds by digest and this revision does not read; the SCTE-35 messages a stream may carry; a billing period, an invoice or a payment, which an operator's system makes from what §7 states; and the detection of invalid traffic, which stays statistical and the measurer's (ad-presentation §13).

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

Relationship to ad-presentation: that extension's claim names an impression of a break-form receipt of a room; it puts the stitched form of wai.video.ssai out of its scope, since such receipts name no session of a room. This extension brings stitched and interstitial ads into scope with a claim of its own. It takes from ad-presentation, by reference: the creative digest (§4 there); the span and its rules (§5.2, §5.4 rules 4 to 6); the viewability rules, the qualifying run and the quartile (§6); the token, its challenge and its issuer's verifier (§7); and the presentation fields of a policy (§8), here carried in the record. It changes nothing in ad-presentation.

Relationship to live-ads and wai.video.ssai: a room's breaks keep live-ads' policy and break-form receipts. A stitcher that runs wai.video.ssai's own deterministic decision seals a form-1 receipt as before; a record whose response is that receipt (§4.5) lets a verifier set the receipt's stated quartiles and billable spend beside the ones the players' claims give. Every receipt of either form keeps its bytes.

Relationship to live-compose: a record's frame (§4.1) is live-compose §5.9's ad frame, and the ad's region and its share are that section's.

2. Terms

  • Stream: an HLS presentation or a DASH presentation, live or on demand.
  • Stitcher: the server that writes ads into a stream: into its segments (server-side ad insertion), or as interstitials a player fetches (server-guided ad insertion).
  • Ad server: the party whose response decides which ads fill a break.
  • Response: the ad server's answer, as the signer received it: a VAST document, a VMAP document, or a form-1 wai.video.ssai receipt (§4.5).
  • Signer: the key that signs a pod-input record: the stitcher's, or that of a proxy that forwards the stitcher's ad requests to the ad server and their responses back.
  • Break, avail: a span of the stream that ads fill, and its duration.
  • Manifest: a document a player reads a marker from: a Media Playlist, an asset list or an MPD (§3).
  • Marker: the fields that place one ad in a stream (§3).
  • Ad identity: the string a marker carries for its ad (§3), 1 to 255 bytes of UTF-8.
  • Player, session key, creative, span, geometry, token, issuer, basis point: as in ad-presentation §2.
  • Record: a pod-input record (§4). Claim: a stitched claim (§5).
  • Presented: the verdict of a claim that passes every step of §7.3.

Integers are little-endian unless this text says otherwise. A digest is SHA-256 (FIPS 180-4), 32 bytes. Ed25519 is RFC 8032's, verified strictly (SPEC §7 Signatures): a key that fails step 1 or 2 is bad_key, and a signature that fails a later step is bad_signature. A short field is a u8 length and that many bytes; a long field is a u16 length and that many bytes. A text field is a short field of 1 to 255 bytes of UTF-8.

3. Markers

3.1 The marker, its body and its digest

A marker is one of three kinds, by its carriage:

carriage where the stream carries it its fields
1 an EXT-X-DATERANGE tag of a Media Playlist (§3.2) the tag's ID and START-DATE
2 an asset of an HLS interstitial's asset list (§3.3) the interstitial date range's ID and START-DATE, and the asset's index in its asset list
3 an Event of a DASH EventStream (§3.4) the id of its Period, the schemeIdUri and value of its EventStream, and its own id

A marker's body is its fields in that order, each a long field of its bytes as the manifest writes them (an attribute value with its quotes removed; an XML attribute value with its references replaced, §3.5), with an absent Period id or EventStream value empty; and, for carriage 2, the asset's index as a u16 after the two long fields. A body of a carriage reads when it is exactly those fields, with no byte after them.

The marker digest is SHA-256 over wai:stitched-marker (ASCII) and 0x01, the carriage (one byte) and the body.

An HLS date range's ID names it within its playlist, and its START-DATE is fixed for the ID (draft-pantos-hls-rfc8216bis-22 §4.4.5.1); together they place an ad on the stream's timeline, and stay the same across the reloads of a live playlist. A DASH event is named by its scheme, its value and its id within its Period.

3.2 Carriage 1: a date range

A Media Playlist is read as its lines: its bytes are UTF-8, split at each line feed with a carriage return before it dropped, and its first line is #EXTM3U. Each line that begins #EXT-X-DATERANGE: is a date range tag, and the rest of the line its attribute list: NAME=VALUE pairs separated by single commas, each name of A–Z, 0–9 and - and given once; a value is a quoted string, " then bytes other than ", CR and LF then ", or else one or more bytes other than ", , and white space. A tag whose list does not read so, or whose ID or START-DATE is not a quoted string, names no ad. (These are §4.2's rules for an attribute list, as live-ads reads them.)

An ad of carriage 1 is a date range tag that carries the attribute the record's id_attr names (§4.1). Its marker is the tag's ID and START-DATE, and its ad identity is that attribute's value: a quoted string's contents, or the value as written. A stitcher writes the ad identity in an attribute of the date range of each ad it stitches; the attribute's name is the stitcher's (a client attribute, X- and a name, by §4.4.5.1), and a record names it.

3.3 Carriage 2: an interstitial's asset

An interstitial date range is a date range tag (§3.2) whose CLASS is the quoted string com.apple.hls.interstitial (Appendix D.2, as live-ads §5.1 writes it). Its asset list is the JSON object (RFC 8259) Appendix D.2 defines: its ASSETS member is an array of objects, each with a string member URI (live-ads §7.2). An ad of carriage 2 is the asset at index k of the asset list a player was served for an interstitial date range. Its marker is the date range's ID and START-DATE and k, and its ad identity is the asset's URI.

The asset list is the manifest a claim of carriage 2 names (§5.1), since each viewer may be served another one under the same date range; the playlist that carries the date range is held beside it (§7.3 step 6).

3.4 Carriage 3: an event

An MPD is read as XML by the subset of §3.5. Its root element is MPD; each child Period of the root, each child EventStream of a Period that has a schemeIdUri, and each child Event of such an EventStream that has an id is considered. An ad of carriage 3 is such an Event that carries the attribute the record's id_attr names. Its marker is its Period's id, its EventStream's schemeIdUri and value, and its own id; its ad identity is the value of that attribute.

3.5 The XML subset

An XML document (an MPD of §3.4, a VAST document of §7.2) is read over this subset of XML 1.0, and one outside it does not read:

  • its bytes are UTF-8 with no byte order mark;
  • before and after its one root element there are only white space, comments and processing instructions (an XML declaration among them); a document type declaration, or any other markup that begins <! other than a comment or a CDATA section, is outside the subset;
  • an element is a start tag, its content and its end tag with the same name, or an empty-element tag; a tag's attributes each follow white space and are a name, = and a value in double or single quotes holding no <, each name once;
  • content is character data, CDATA sections, comments, processing instructions and elements, nested at most 64 deep, the root counted as 1;
  • in character data and attribute values, the references &lt;, &gt;, &amp;, &quot;, &apos;, &#n; (decimal) and &#xh; (hexadecimal) are replaced by the character they name; any other & is outside the subset. An attribute value is otherwise taken as written: no white space in it is normalised.

Elements and attributes are named by their local name: the part after the last :. An element's text is its character data and CDATA children, joined in order, with white space (space, tab, CR, LF) removed from both ends.

4. The pod-input record

4.1 Layout

size field meaning
4 magic WSI1 (ASCII)
1 version 1 in this revision
1 carriage how the stream marks its ads, 1 to 3 (§3.1)
16 session the session the decision was made for (§5.2); 16 zero bytes for a shared break, one every viewer of the stream is shown (§4.6)
short avail_id the break, as the signer names it: a text field
4 avail_ms u32: the avail the ad request asked to fill, in milliseconds, at least 1
1 response_format 1 a VAST document; 2 a VMAP document; 3 a form-1 wai.video.ssai receipt (§4.5)
32 response formats 1 and 2: SHA-256 of the response's bytes as the signer received them; format 3: the receipt's identity (its receipt hash)
1 id_kind what an ad's serving_id is: 1 the AdServingId of its InLine (VAST 4.3 §3.4.3); 2 the id of its Ad (VAST 4.3 §3.3.2); 3 a form-1 impression's ad_id
short id_attr the attribute that carries the ad identity: for carriage 1 an HLS attribute name of A–Z, 0–9 and -; for carriage 3 an XML name (§4.4); for carriage 2 no bytes
3 currency the prices' currency: an ISO 4217 alphabetic code, three of A–Z
1 exponent its minor unit, as ISO 4217 lists it: 0 to 4
13 frame the frame the break is shown in (§4.3)
4 + long measurement terms presentation, viewability, token_type (u16) and issuer (long), as ad-presentation §8 defines them
1 n the ads' count, 1 to 16
… ads n ads, §4.2
32 signer the signer's Ed25519 key
64 sig §4.4

4.2 An ad

size field meaning
1 slot its place in the pod: 0, 1, 2, … in order
32 marker its marker digest (§3.1)
short ad_id the ad identity its marker carries (§3): a text field
short serving_id its identity in the response, as id_kind names it: a text field
short creative_id its creative's universal identifier, a UniversalAdId's text (VAST 4.3 §3.7.1), or no bytes
short registry that identifier's idRegistry; no bytes exactly when creative_id has none
32 creative_hash the creative digest (ad-presentation §4) of the media objects the stream serves for it
4 duration_ms u32: the sum of those objects' durations, at least 1
8 price_micros u64: the price of one presented impression, in millionths of a minor unit of currency, at most 10^12
1 price_source 1 the response's Pricing (VAST 4.3 §3.4.8) of model CPM, checked (§7.2); 2 a winning bid's price; 3 a price the signer states
32 evidence for price_source 2, SHA-256 of the winning bid's document, which this revision binds and does not read; otherwise 32 zero bytes

A price is per presented impression, so that the spend of §7.4 is a count of presentations times a price. A CPM price P in a currency of exponent e is P × 10^(e + 3) millionths of a minor unit per impression: P × 10^e minor units a thousand impressions, times 10^6 over 1000. CPM 25.00 in a currency of two decimals is 2 500 000.

4.3 The frame

size field meaning
1 kind 0 the full picture; 3 a squeeze; 4 a double box (live-compose §5.9)
2 width u16: the picture's width in pixels
2 height u16: its height
1 corner live-compose §5.9's corner: a squeeze's program window's corner, 1 to 4; a double box's program side, 1 left, 2 right
2 window_w u16: a squeeze's program window's width; otherwise 0
2 window_h u16: its height; otherwise 0
2 gap u16: a double box's gap; otherwise 0
1 aspect a double box's tile aspect, live-compose §3.1's aspect, 0 to 3; otherwise 0

A frame of kind 0 has every other field 0. A squeeze or a double box has a width and a height live-compose §3.4 admits (each even, 16 to 1920, at most 2 073 600 pixels), and fields live-compose §3.3 admits for its layout: a squeeze's window_w from 1 to width − 1 and window_h from 1 to height − 1, with gap and aspect 0; a double box's corner 1 or 2, window_w and window_h 0, and gap + 2 at most width.

4.4 Digest, signature and rules

The digest of a record is SHA-256 over its bytes from magic through signer. The signature is Ed25519 by signer over wai:stitched-pod-input (ASCII) and 0x01, followed by the bytes the digest covers.

A reader reads exactly the bytes it is given as one record, in the order of §4.1, and refuses it pod_input_malformed when the bytes end before a field the layout requires or go on after sig, or when:

  1. its magic is not WSI1 or its version not 1, or its carriage is not 1, 2 or 3;
  2. avail_id is not a text field, or avail_ms is 0;
  3. response_format is not 1, 2 or 3; or id_kind is not 1 or 2 with format 1 or 2, or not 3 with format 3;
  4. id_attr is, for carriage 1, not 1 to 255 bytes of A–Z, 0–9 and -; for carriage 3, not an XML name of 1 to 255 bytes (an ASCII letter or _, then ASCII letters, digits, _, - and .); for carriage 2, not empty;
  5. currency is not three of A–Z, or exponent is above 4;
  6. the frame breaks §4.3;
  7. the measurement terms break ad-presentation §8, or do not set its bit 0, claim: an ad of a record counts only through a claim;
  8. n is 0 or above 16; an ad's slot is not its index; ad_id or serving_id is not a text field; creative_id and registry are not both empty, or both text fields; duration_ms is 0; price_micros is above 10^12; price_source is not 1, 2 or 3, is 1 with a response format other than 1, is 2 with format 3, or is other than 3 with format 3; evidence is not zero exactly when price_source is not 2;
  9. two ads have one marker.

4.5 A form-1 receipt as the response

A stitcher that decides by wai.video.ssai's own deterministic decision (SPEC §5) seals a form-1 receipt, whose signed bytes are unchanged here. A record of format 3 names it by its identity, and a verifier that holds it checks it (§7.2). Its JSON carriage, for a verifier to be given it, is one JSON object with exactly these members, a byte field lowercase hexadecimal of its exact length:

member value
form 1
avail_id a string
manifest_hash 32 bytes: the attested manifest's hash
pod_hash 32 bytes
impressions an array, each {"slot", "ad_id", "creative_hash", "duration_ms", "price_micros", "quartiles_reached"}, as live-ads §8.2 writes a form-2 impression
merkle_root 32 bytes
budget_spent_micros, work_decide_ops, joules_micro integers
energy_provenance its class's label, or null unmetered; and the members a class with evidence carries, as live-ads §8.2 has them. The legacy label HwShunt does not read in this carriage
grant {"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}, funds_ceiling null for none
parent_receipt_hash 32 bytes, or null
signer_pubkey 32 bytes
signer_id a string
sig 64 bytes

Its signing payload is the concatenation of wai:video-ssai-receipt (ASCII) and 0x01; the length (u32) and bytes of avail_id; manifest_hash; pod_hash; merkle_root; budget_spent_micros, work_decide_ops and joules_micro (u64 each); the class in its signed encoding (energy-measurement §2.7), the tag byte 0 for none; the grant reference as live-ads §8.4 writes it; 1 and parent_receipt_hash, or 0; the impressions' count (u64) and each one's leaf (live-ads §8.3); and signer_pubkey, then the length (u32) and bytes of signer_id. Its integers are big-endian. sig is Ed25519 by signer_pubkey over the payload, and the receipt's identity is BLAKE3 over wai:video-ssai-receipt-id (ASCII) and 0x01, the payload and sig. A receipt verifies when its signature verifies; its merkle_root is the root of its impressions (live-ads §8.3); its budget_spent_micros is their billable spend and, when funds_ceiling is not null, at most it; work_decide_ops is at least the impressions' count; its slots are 0, 1, 2, … in order with each quartiles_reached at most 5; and its figure and class cohere as energy-measurement §2 has them.

4.6 The session and the shared break

A record of a session is a decision made for one playback session: a personalised stitched playlist, or the asset list one viewer was served. Its session is the session the player's session key names (§5.2). The signer MUST take it from the player's session key, which the player gives with its request for the session's playlist or asset list, by a means this revision does not fix (for one, a parameter of that request, which a stitcher forwards in the ad request it makes for the session). A signer MUST NOT derive a session from anything else, and a claim counts toward a record of a session only when its key names that session (§7.3).

A shared record is a decision every viewer of the stream is shown: a break of a linear channel, or a composited break all viewers see in the same picture. Each viewer's player that presents it signs a claim under its own session key, and each claim of a distinct session that is presented is one presentation (§7.4). A shared break is where a party that makes session keys of its own can sign claims for sessions nobody watched; a record that buys such breaks SHOULD set ad-presentation §8's attest bit, so that each presentation costs that party a vouched device (ad-presentation §13).

5. The stitched claim

5.1 Layout

size field meaning
4 magic WSC1 (ASCII)
1 version 1 in this revision
1 carriage the marker's carriage (§3.1)
32 manifest SHA-256 of the manifest the player read the marker from: the Media Playlist for carriage 1, the asset list for carriage 2, the MPD for carriage 3
16 session the session its key names (§5.2)
32 creative_hash the creative presented: its creative digest (ad-presentation §4)
32 player the session key, which signs the claim
1 geometry ad-presentation §5.1's: 1 the ad's own pixels (in a frame, its region, §6), 2 a full-screen television output, 3 the player's area
2 share_bp u16: the share of the picture the ad's region holds, in basis points (§6)
4 frames u32: as ad-presentation §5.1
long marker the marker's body (§3.1)
short ad_id the ad identity the marker carries: a text field
2 n u16: the presented objects' count
32 × n objects the content hashes of the creative's objects the spans touch, in order
2 m u16: the spans' count
11 × m spans ad-presentation §5.2's spans, over the ad's media time
long attestation a token (ad-presentation §7.1), or no bytes
64 sig §5.3

5.2 The session key and the session

A player MUST make a fresh Ed25519 key for each playback session of a stream, from its platform's random generator, and MUST NOT use it for another session or derive it from an identifier of the viewer, the device or an account (ad-presentation §3). The session a key names is the first 16 bytes of SHA-256 over wai:stitched-presentation-session (ASCII) and 0x01, and the key (32 bytes). It is a function of a key made for one session alone, so it links nothing across sessions.

5.3 Digest and signature

The digest of a claim is SHA-256 over its bytes from magic through attestation. The signature is Ed25519 by player over wai:stitched-presentation-claim (ASCII) and 0x01, followed by the bytes the digest covers.

5.4 Reading and rules

A reader reads exactly the bytes it is given as one claim, in the order of §5.1, and refuses it stitched_claim_malformed when the bytes end before a field the layout requires or go on after sig, or when:

  1. its magic is not WSC1 or its version not 1;
  2. carriage is not 1, 2 or 3, or marker is not a body of that carriage that reads (§3.1);
  3. geometry is not 1, 2 or 3, or share_bp is not 1 to 10 000;
  4. ad_id is not a text field;
  5. its objects and spans break ad-presentation §5.4 rules 4 to 6.

5.5 The attestation challenge

A token a claim carries answers ad-presentation §7.2's TokenChallenge with the record's token_type and issuer and this redemption context: SHA-256 over wai:stitched-presentation-attest (ASCII) and 0x01, carriage (one byte), the marker digest, session, creative_hash, player, and the length (one byte) and bytes of ad_id, the claim's own. It is a function of the ad's place in the stream and of the session key, each known before the ad plays, so a token answers one presentation of one session key; lifted onto another claim, its challenge_digest is not that claim's. The token is verified as ad-presentation §7.3 verifies one.

6. Composited frames

A break may show the ad and the program in one picture: a squeeze, the program shrunk into a window in one corner with the ad in the L around it; or a double box, the program and the ad in two boxes side by side. A record states such a frame (§4.3), and the ad's region is then live-compose §5.9's ad region of a canvas of the frame's width and height under its layout, corner, program window (window_w × window_h as pip_width × pip_height), gap and aspect: an integer function of the signed fields, so a player and a verifier compute the same rectangles. A frame of kind 0 is the full picture, and the region is the whole of it.

  • The share of the picture the region holds is live-compose §5.9's, in basis points; 10 000 for the full picture. A claim states it as share_bp, and a claim whose share_bp is not its record's frame's share is refused (§7.3 step 9).
  • A player that measures the ad's own pixels (geometry 1) MUST measure the fraction in view of the ad's region, not of the whole picture: the share of the region's area, as the picture is displayed, that lies within the visible part of its player (ad-presentation §10). One that cannot place the region states geometry 3, its own area, as ad-presentation §10 allows.
  • The viewability rules of ad-presentation §6.1 then apply to the region's pixels: half of the ad's region in view for the video rule, all of it for the cross-media rule. No text read for this revision defines a viewability rule of its own for a composited ad; a buyer that weighs a composited presentation by how much of the picture the ad held reads share_bp among the claim's facts.
  • One presentation is one claim per signing player. Every viewer of a composited break sees the same picture, and each player that presents it signs its own claim; a record of the break is shared (§4.6), and each presented claim of a distinct session is one presentation. A count of devices that requested the stream is not a count of presentations.

7. Verification

7.1 Inputs

A verifier is given:

  • records: pod-input records, a list of byte strings, in an order;
  • claims: claims, a list of byte strings, in an order, at most 2^20 of them at once, so that no spend of §7.4 passes 2^64 − 1 (a price is at most 10^12, and a claim is one presentation of one ad at most);
  • manifests: the manifests the players read (playlists, asset lists and MPDs), by their bytes;
  • responses: VAST and VMAP documents, by their bytes; and form-1 receipts in their JSON carriage (§4.5);
  • creatives: the creatives it holds (ad-presentation §4), each its objects;
  • issuers: the attestation issuers it trusts (ad-presentation §7.3);
  • signers: the keys whose records it accepts: the stitchers and proxies the buyer and the seller agreed on.

7.2 Each record

The records are judged first, in order. Each gets one verdict, accepted or the reason of the first step it fails:

  1. It reads (§4.4): pod_input_malformed.

  2. Its signature verifies under signer (§4.4): bad_key, bad_signature.

  3. signer is among the signers: pod_signer_unknown.

  4. No record before it that was accepted has its session and an ad of a marker it has: pod_duplicate. A decision is recorded once.

  5. The response. When the verifier holds it, the record agrees with it, as below: otherwise pod_response_mismatch.

    • Format 1, the response whose SHA-256 is response: it reads as XML (§3.5) with a root of local name VAST; and for each ad, exactly one Ad child of the root has the ad's serving_id as its identity (for id_kind 1, the text of the first AdServingId child of its first InLine child; for 2, its id attribute); that Ad has an InLine child; when creative_id is not empty, a UniversalAdId child of a Creative child of a Creatives child of that InLine has the text creative_id and the idRegistry registry; and when price_source is 1, that InLine's first Pricing child has model CPM and currency the record's currency, and its text is a decimal number (digits, then optionally . and digits) P with P × 10^(exponent + 3) an integer of at most 2^64 − 1 equal to price_micros.
    • Format 3, the receipt whose identity is response, among those that read (§4.5): it verifies (§4.5), and its impressions are the record's ads slot for slot: each one's slot the ad's, its ad_id the ad's serving_id, and its creative_hash, duration_ms and price_micros the ad's.
    • Format 2 is bound by its digest and not read.

    A record whose response the verifier does not hold is accepted with its response unchecked, and the verification states so (§7.5).

A record that is not accepted names no ad: a claim of one of its ads is judged as though it were not there.

7.3 Each claim

Then the claims, in order. Each gets one verdict, presented or the reason of the first step it fails:

  1. It reads (§5.4): stitched_claim_malformed.
  2. Its signature verifies under player (§5.3): bad_key, bad_signature.
  3. Its player names its session (§5.2): claim_key_unbound.
  4. Its record. Its record is the first accepted record of its carriage that has an ad whose marker is the claim's marker digest and whose ad_id is the claim's, and whose session is the claim's or which is shared. When an accepted record has such an ad but none is the claim's this way: claim_session_unbound. When none has such an ad: claim_no_decision. From here on the claim names that record and that ad.
  5. Its manifest. The verifier holds a manifest whose SHA-256 is manifest: claim_manifest_unknown.
  6. Its marker. The marker, with the claim's ad identity, is read from that manifest by §3 under the record's id_attr: for carriage 1, the manifest is a Media Playlist with a date range tag of the marker's ID and START-DATE whose id_attr attribute's value is the ad identity; for carriage 2, it is an asset list whose asset at the marker's index has the ad identity as its URI, and a manifest the verifier holds is a Media Playlist with an interstitial date range of the marker's ID and START-DATE; for carriage 3, it is an MPD with an Event of the marker's fields whose id_attr attribute's value is the ad identity: claim_marker_absent.
  7. One claim a session. No claim before it passed steps 1 to 6 for the same record, ad and session: claim_duplicate. The first that does is that session's own claim of the ad, whatever its later verdict; steps 8 to 11 are for own claims alone.
  8. The asset. Its creative_hash is the ad's; the verifier holds a creative of that creative digest whose durations sum to the ad's duration_ms; every span ends at most at that duration_ms; and objects is exactly the content hashes of the creative's objects the spans touch, in order (ad-presentation §9.2 step 6): claim_asset_mismatch.
  9. The frame. Its share_bp is the share of its record's frame (§6): claim_frame_mismatch.
  10. Viewability. It is viewable under the record's rule (ad-presentation §6.2, with geometry and the audible bit as there): claim_not_viewable.
  11. Attestation, when the record sets attest: its token is not empty (attestation_absent); it reads as a token of the record's token_type, its challenge_digest is SHA-256 of the TokenChallenge of §5.5, and its issuer's verifier accepts it under the record's issuer (attestation_invalid).

7.4 Presentations, quartiles and the spend that counts

For each ad of each accepted record:

  • its presentations are its own claims (one per session, step 7) that are presented. A record of a session has at most one; a shared record has one for each session whose claim is presented;
  • the quartile a presentation reaches is ad-presentation §6.3's, over the claim's spans with D the ad's duration_ms: 1 started, then a quarter, a half, three quarters and all of it, at VAST 4.3 §3.14.1's marks. The ad's quartiles are five counts: its presentations whose quartile is at least 1, 2, 3, 4 and 5;
  • its verdict is presented when it has a presentation; otherwise claim_absent when it has no own claim, or the verdict of its first own claim;
  • its presented spend is its price_micros times its presentations.

A record's presented spend is the sum over its ads. The spend of a verification is, for each currency, the sum of the presented spend of the accepted records of that currency. Spends of different currencies are never summed.

A quartile is recomputed from what a player signed, not compared with a beacon count: an ad whose segments were fetched ahead of its break (a prefetch) and never presented has no claim, so no presentation and no spend, whatever a request-based count says. For a record of format 3 whose receipt was checked, the verification states the receipt's quartiles_reached for each ad and its budget_spent_micros beside the presented ones, so a buyer and a seller see both figures from the same signed bytes.

7.5 What a verification states

  • For each record, in order: its digest when it reads, and its verdict; and for an accepted record, whether its response was checked, each ad's slot, own claims (their indices among the claims given), presentations, verdict, quartiles, presented spend and, for a checked format-3 response, its stated quartile; the record's presented spend, and its stated spend for a checked format-3 response; and its frame's share (§6) and ad region, the rectangles of live-compose §5.9, none for the full picture.
  • For each claim, in order: its digest when it reads; its verdict; from step 4 on, the record and the ad's slot it names; and for an own claim, from step 8 on, the quartile it reaches, its run and its viewable time (ad-presentation §6.2).
  • The spend, by currency.

A verification refuses nothing as a whole: each record and each claim gets its own verdict.

8. The producer, the tool and the service

The reference producer is web/ads/stitched.js of the browser client joule-moq-web, beside ad-presentation's web/ads/presentation.js, whose span recorder it uses. From the manifest a player fetched, it reads the markers and ad identities of §3 (hlsAds, interstitialAds, mpdAds); it measures the fraction in view of an ad's region in a frame (regionVisibleBp); and it writes and signs the claim with the platform's Ed25519. It needs no player of a particular kind: a player integration hands it the manifest bytes, the ad's media time and the observations ad-presentation §10 lists. Its tests require the corpus's claims from the corpus's manifests, byte for byte.

The tool, wai_stitched_presentation (feature stitched_presentation): verify reads a bundle (the inputs of §7.1 as JSON, in the corpus README's form) and prints what §7.5 states; sign-pod writes and signs a record from a draft with the signer's key; vectors writes and checks the corpus. The service, wai_stitched_presentation serve, answers POST /v1/verify with the same statement for a bundle and GET /v1/health, over HTTP/1.1 on a loopback address for a reverse proxy to publish; it reads one request a connection, bounds the head, the body and the time it waits, and holds no state between requests.

9. Reasons

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

reason raised at
pod_input_malformed §4.4, §7.2 step 1
pod_signer_unknown §7.2 step 3
pod_duplicate §7.2 step 4
pod_response_mismatch §7.2 step 5
stitched_claim_malformed §5.4, §7.3 step 1
claim_session_unbound §7.3 step 4
claim_manifest_unknown §7.3 step 5
claim_marker_absent §7.3 step 6
claim_frame_mismatch §7.3 step 9

A verification also gives live-events' bad_key and bad_signature (§7.2 step 2, §7.3 step 2), and ad-presentation's claim_key_unbound (§7.3 step 3), claim_no_decision (step 4), claim_duplicate (step 7), claim_asset_mismatch (step 8), claim_not_viewable (step 10), attestation_absent and attestation_invalid (step 11), and claim_absent (§7.4), each at the step named. None of them refuses a verification: each is one record's, one claim's or one ad's.

10. Conformance

live-conformance/stitched/ holds the corpus, written from fixed seeds by wai_stitched_presentation vectors gen: vectors.json, which holds the creatives, the attestation issuer (RFC 9578 Appendix A.2's first test key) and the signers the verifier holds, the players' session keys, and each vector's records, claims and facts; and each vector's documents as files of their own. Every document is a fixture written for its case. The vectors:

  • hls_stitched: two viewers' personalised playlists with ads stitched in, each ad under a date range that carries its ad identity; a record per viewer priced from its VAST response; viewer 2 left before its second ad, whose segments had been fetched ahead of the break, so that ad has no presentation; a control of each check of a record (an unknown signer, a price the response does not give, a decision recorded twice, terms that count without a claim, a flipped signature) and of a claim (a duplicate, a key that does not name the session, a session the record was not decided for, a marker no record names, a manifest not held, a manifest without the marker, a truncated claim, a flipped signature);
  • hls_interstitial: one shared playlist with an interstitial date range and an asset list for each viewer; records under the cross-media rule with an audible run and attestation, one ad priced by a winning bid whose document is bound by digest; attested presentations, a claim with no token and one carrying another presentation's token;
  • squeezeback: a squeeze on a DASH channel marked by an event; one shared record; three presentations, among them one that stopped at the midpoint, and claims whose share is not the frame's, whose region was too little in view, given twice, and of another object;
  • double_box: a double box on an HLS channel, its record in another currency at a price the signer states, its VMAP response bound and not read; a presentation measured over the ad's box and one over the player's area;
  • form1_prefetch: a stitcher's own decision sealed in a form-1 receipt whose beacons counted the second ad as started when its first segment was fetched ahead of the break; the viewer left after the first ad; and a record naming the same receipt at another price.

A conforming verifier gives every record and claim its verdict and facts, every ad its presentations, quartiles and spend, and every vector its spend by currency, as the corpus states. wai_stitched_presentation vectors verify and the test tests/stitched_presentation.rs check that every file regenerates byte for byte, that wai-rs gives every vector its facts, and that §9 lists exactly the codes the reference adds; the generator checks that the corpus gives every reason of §9 and every other code of a claim or a record but bad_key. live-conformance/stitched_presentation_verify.py is the second implementation, written from this text: it shares no code with wai-rs; it verifies Ed25519 with WAI's strict rule (claim-conformance/_ed25519.py), BLAKE3 with claim-conformance/_blake3.py, a form-1 receipt's leaves and Merkle root as the live-ads corpus's second implementation computes them, the spans, the run and the quartile and RSASSA-PSS with the ad-presentation corpus's second implementation, and an ad frame with the live-compose corpus's; it reads playlists, asset lists, MPDs and VAST documents itself; and it mutates copies of the corpus and requires every copy to fail. The reference producer's tests (§8) require the corpus's claims from its manifests.

11. Security and privacy considerations

  • What a claim proves. As ad-presentation §13 states for its claim: that the holder of the session key stated these facts of this ad, at this place of a manifest it holds the bytes of. Without attestation it removes the third party that strips, redirects or forges beacons, and the count of segment requests as a count of impressions; it does not remove a dishonest player or a party that makes session keys of its own, which a shared record (§4.6) is open to.
  • What a record proves. That its signer stated these inputs of one decision: the response's digest, the ads, their creatives and prices. A record of format 1 checked against the response shows the ads and the CPM prices are the response's; one of a winning bid binds the bid's document without reading it; one of format 3 checked against its receipt shows the stitcher's decision and its stated quartiles. The signer is the party a buyer trusts for the decision, and §7.1's signers are who those parties are.
  • The manifest. A claim names the manifest by its digest, and a verifier that holds the bytes checks that the stream carried the marker with that ad identity. A personalised playlist is a viewer's; its bytes reach whoever verifies the claim, and a collector SHOULD keep them no longer than the reconciliation they serve.
  • Linking. A session key is made for one playback session, and the session derived from it alone, so claims link nothing across sessions. A record of a session carries that session; a shared record carries none. A claim carries no identifier of the viewer, the device or an account.
  • Withholding. A party between the player and the verifier can withhold a claim; the ad then has no presentation, which is the seller's lost spend, never the buyer's overpayment.
  • A composited frame. The region is fixed by the record's frame; a player that states another share is refused, and one that measures the whole picture where it should measure the region overstates its fraction in view, which attestation does not bound: a buyer that needs the measurement itself bounded needs the player's integrity, as ad-presentation §13 says of every claim.