WAI Extension: Stitched Presentation
Mirrored from the canonical text at commit 260cbd09 ().
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 (anEXT-X-DATERANGEtag, an asset of an interstitial’s asset list, or anEventof a DASHEventStream) 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 thestitched_presentationmodule ofwai-rs(featurestitched_presentation); the corpus islive-conformance/stitched/, and its second implementation islive-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 (itsEventStreamandEvent). 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.
- For the consent signal a record of version 2 carries (§4.7), the texts live-ads read for its §8.6, at the revisions it names.
A section number of one of these texts is that revision’s.
A pod-input record of version 2 also carries the consent signal its decision was made under, a Global Privacy Platform string with the sections that apply and a child-directed flag, bound into what the signer signs (§4.7); a verifier states it beside the record’s verdict.
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).
The consent signal a record of version 2 carries, and what a player and a signer do with it, are
in scope too (§4.7).
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.ssaireceipt (§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
<,>,&,",',&#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; or 2 for a record that carries the consent signal (§4.7) |
| 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 |
long + 2 + 2 × k | consent signal | version 2 only: gpp, k, gpp_sid and child_directed (§4.7) |
| 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:
- its magic is not
WSI1or its version not 1 or 2, or itscarriageis not 1, 2 or 3; avail_idis not a text field, oravail_msis 0;response_formatis not 1, 2 or 3; orid_kindis not 1 or 2 with format 1 or 2, or not 3 with format 3;id_attris, for carriage 1, not 1 to 255 bytes ofA–Z,0–9and-; 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;currencyis not three ofA–Z, orexponentis above 4;- the frame breaks §4.3;
- 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;
nis 0 or above 16; an ad’sslotis not its index;ad_idorserving_idis not a text field;creative_idandregistryare not both empty, or both text fields;duration_msis 0;price_microsis above 10^12;price_sourceis 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;evidenceis not zero exactly whenprice_sourceis not 2;- two ads have one
marker; - of version 2, its consent signal breaks a rule of live-ads §8.6, or, in a shared record (§4.6), carries a GPP string.
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).
4.7 The consent signal
A record of version 2 carries, after its ads and before signer, the consent signal its
decision was made under: live-ads §8.6’s, in this record’s layout.
| size | field | meaning |
|---|---|---|
| long | gpp | the GPP string the player gave, byte for byte; no bytes when it gave none |
| 1 | k | the count of gpp_sid, 0 to 16 |
2 × k | gpp_sid | u16 each: the string’s sections the player gave as applying, in the order given |
| 1 | child_directed | 1 when the request is directed to children, 0 otherwise |
The signal holds live-ads §8.6’s rules (§4.4 rule 10), and the parties do with it what that section says, the signer in the decider’s place:
- The player gives the string, its sections and a child-directed request with its request for the session’s playlist or asset list, as it gives its session key (§4.6), by a means this revision does not fix, and MUST NOT write, alter or complete a GPP string.
- The signer states in the record what it was given, byte for byte, MUST pass it
unchanged in every ad request it makes for the decision (an ad request URL’s
gppandgpp_sidparameters, as the GPP string’s text names them, or an OpenRTB 2.6 request’sRegs.gpp,Regs.gpp_sidandRegs.coppa), and MUST NOT state a string, a section or a flag it was not given, or clear a flag it was given. - A shared record (§4.6) is a decision every viewer is shown, and carries no viewer’s
string: its
gppandgpp_sidare empty, and itschild_directedis 1 when the stream is directed to children. A signer MUST NOT apply one viewer’s signal to a break every viewer sees. - What no party infers is live-ads §8.6’s: an empty string is no string given, a flag of 0 is no mark given, and neither names the viewer. A string that is not empty can still link every record and receipt that carries it (§11).
The signal is in the bytes the digest and the signature cover (§4.4): a record whose signal changed after signing fails its signature.
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:
- its magic is not
WSC1or its version not 1; carriageis not 1, 2 or 3, ormarkeris not a body of that carriage that reads (§3.1);geometryis not 1, 2 or 3, orshare_bpis not 1 to 10 000;ad_idis not a text field;- 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 whoseshare_bpis 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_bpamong 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:
-
It reads (§4.4):
pod_input_malformed. -
Its signature verifies under
signer(§4.4):bad_key,bad_signature. -
signeris among the signers:pod_signer_unknown. -
No record before it that was accepted has its
sessionand an ad of amarkerit has:pod_duplicate. A decision is recorded once. -
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 nameVAST; and for each ad, exactly oneAdchild of the root has the ad’sserving_idas its identity (forid_kind1, the text of the firstAdServingIdchild of its firstInLinechild; for 2, itsidattribute); thatAdhas anInLinechild; whencreative_idis not empty, aUniversalAdIdchild of aCreativechild of aCreativeschild of thatInLinehas the textcreative_idand theidRegistryregistry; and whenprice_sourceis 1, thatInLine’s firstPricingchild hasmodelCPMandcurrencythe record’scurrency, 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 toprice_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’sslotthe ad’s, itsad_idthe ad’sserving_id, and itscreative_hash,duration_msandprice_microsthe 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).
- Format 1, the response whose SHA-256 is
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:
- It reads (§5.4):
stitched_claim_malformed. - Its signature verifies under
player(§5.3):bad_key,bad_signature. - Its
playernames itssession(§5.2):claim_key_unbound. - Its record. Its record is the first accepted record of its
carriagethat has an ad whosemarkeris the claim’s marker digest and whosead_idis the claim’s, and whosesessionis 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. - Its manifest. The verifier holds a manifest whose SHA-256 is
manifest:claim_manifest_unknown. - 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’sIDandSTART-DATEwhoseid_attrattribute’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 itsURI, and a manifest the verifier holds is a Media Playlist with an interstitial date range of the marker’sIDandSTART-DATE; for carriage 3, it is an MPD with an Event of the marker’s fields whoseid_attrattribute’s value is the ad identity:claim_marker_absent. - 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. - The asset. Its
creative_hashis the ad’s; the verifier holds a creative of that creative digest whose durations sum to the ad’sduration_ms; every span ends at most at thatduration_ms; andobjectsis exactly the content hashes of the creative’s objects the spans touch, in order (ad-presentation §9.2 step 6):claim_asset_mismatch. - The frame. Its
share_bpis the share of its record’s frame (§6):claim_frame_mismatch. - Viewability. It is viewable under the record’s rule (ad-presentation §6.2, with
geometryand the audible bit as there):claim_not_viewable. - Attestation, when the record sets attest: its token is not empty
(
attestation_absent); it reads as a token of the record’stoken_type, itschallenge_digestis SHA-256 of the TokenChallenge of §5.5, and its issuer’s verifier accepts it under the record’sissuer(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_absentwhen it has no own claim, or the verdict of its first own claim; - its presented spend is its
price_microstimes 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; and, for a record of version 2, its consent signal (§4.7), as live-ads §8.6 writes it.
- 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;
- consent: two viewers’ personalised playlists, each with one stitched ad, and a record of
version 2 per viewer: one carrying the GPP string its player gave with its applicable
section, one a child-directed request with no string, each presented; a record whose GPP
string was changed after signing (
bad_signature), whose ad’s claim then names no decision; a shared record carrying a viewer’s string, and a record stating a section with no string (eachpod_input_malformed).
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.
- The consent signal. A record of version 2 binds the signal into what the signer signs, so
one whose string, sections or flag changed after signing is refused at its signature, and its
ads count nothing. A verified record shows what the signer stated it was given and passed on
(live-ads §8.6), not that the viewer gave it or that the ad server honoured it. A shared
record carries no viewer’s string, so a break every viewer sees links to no viewer’s signal.
A record of one session whose
gppis not empty can be linked through it, by whoever holds them, to every other record and every live-ads receipt that carries the same string, in any session or room, as live-ads §12 says of a receipt: the string is made for the viewer, not for one session, and is in the bytes the signature covers. What Linking above says of claims does not extend to such a record.