# WAI Extension: Live Ads > Status: Draft. Registers the record `wai.live.ads`: server-guided ad insertion for rooms. > The room authority signs an **ad policy**, `WAP1`, naming the grant its ad decider runs > under and how the room's breaks are presented. The breaks themselves are the room's signed > `cue` events ([live-events](live-events.md) §4.3), an out and its in, or a break of known > duration, with a post-roll marked by a **break annotation**, `WAB1`, in the cue author's > chain. Each break maps, deterministically, onto one interstitial date range of HLS and one > alternative-presentation event of DASH, so every edge writes the same playlist and MPD bytes > and the manifests stay shared and cacheable. Per-viewer decisioning happens behind the one > URI each break names: the **asset list** a player fetches when it reaches the break, filled > by `wai.video.ssai`'s deterministic decision. Each viewer's impressions are sealed in a > `wai.video.ssai` receipt of its **break form**, which names the break, the decision's > inputs and the asset list served, and a verifier recomputes the decision, the asset list and > the spend against the decider grant's funds ceiling. A policy of version 2 can require that > each impression count only when the player's signed presentation claim verifies > ([ad-presentation](ad-presentation.md)), and the spend is then held to the ceiling counting > only those impressions. No new transport. The reference > implementation is the `live_ads` module of `wai-rs` (feature `live_ads`) with the break form > in `wai_video_ssai`; the corpus is [`live-conformance/ads/`](../live-conformance/ads/), and > its second implementation is `live-conformance/live_ads_verify.py`. > Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. > > Over MoQ, a session's fills are also signalled as the records of > `draft-riedl-moq-ad-creative-signaling-01` on an MSF event timeline, each held to the break and > the receipt it describes (§13), and a stitched track is the composed track of > `draft-englishm-moq-content-stitching-00`, recomputed from the programme's track, the > creatives' tracks, the schedule and the receipts (§14). Both sections add no record and change > no byte of a cue, a policy or a receipt. Texts read for them: those two drafts, each in full, > at the latest revision in the IETF archive on 10 November 2026; the JSON Schema and the test > vectors of the first, from its source repository at commit `443f90b2`; RFC 7515, RFC 7517, > RFC 7797, RFC 8037 and RFC 8785 for the record's signature. SVTA 2053-1 was not read: §13 > takes its payload from what the draft states of it, and §13.8 says what that leaves unread. > > Texts read for this revision: `draft-pantos-hls-rfc8216bis-22` (1 May 2026), its §4.4.5.1 > (`EXT-X-DATERANGE`), §6.2.1, §6.2.4, Appendix D (Interstitials) and Appendix F (Preloading > HLS Date Range Resources). Its Appendix H is a glossary and defines no date-range schedule, > so nothing here maps onto it. ISO/IEC 23009-1:2026, the sixth edition, through the XML > schema that accompanies it (its types `EventType`, `AlternativeMPDEventType`, > `AlternativeMPDReplaceEventType`, `ImportedMpdType` and `PresentationType`) and the schema's > example MPDs G23, G29-1 and G29-2, at the schema's revision of 2026-06-13; the clause text of > its §5.16 was not available to this revision (§6.1). A section number of one of these texts > is that revision's. > > A receipt of version 2 also binds the **request terms** its decision was made under: the > consent signal the player gave, a Global Privacy Platform string with the sections that apply > and a child-directed flag (§8.6), and the supply chain each of its impressions was sold through, > each seller named by its advertising system's domain and its seller id, with references to each > system's sellers file and to the inventory owner's authorised-sellers file (§8.7). WAI carries > the signal and the chain as the texts below define them, binds them into what the decider > signs, and defines no scheme of its own beside them. A policy of version 3 requires every > receipt to state its consent signal, child-directed in a room the policy marks as directed to > children (§3.1, §9.3 step 13). > > Texts read for §8.6 and §8.7, each at its source: *Global Privacy Protocol String*, version > 1.0, its revision of 3 November 2023: its sections on what the string serves and holds, who > makes it, its format, header, delimiters and section encoding, and how a service reached by a > URL is given it. *OpenRTB API Specification* Version 2.6: §3.2.2 (`Source`), §3.2.3 (`Regs`), > §3.2.25 (`SupplyChain`) and §3.2.26 (`SupplyChainNode`). The *OpenRTB SupplyChain object*, in > full. *Sellers.json*, version 1.0, July 2019, in full. *ads.txt Specification* Version 1.1, > August 2022: §3 to §5. *Authorized Sellers for Apps (app-ads.txt)*, version 1.0, March 2019: > its abstract, its introduction, and its sections on publishing the file and on translating an > app's developer URL into the file's path. ## 1. Scope In scope: the ad policy, its signature and rules (§3); the room's breaks, derived from its `cue` events and break annotations, with their identity and refusals (§4); their HLS interstitial date ranges (§5) and their DASH alternative-presentation events (§6); the asset list, the decision that fills it, and a viewer joining a break already running (§7); the break form of the `wai.video.ssai` receipt (§8); and verification end to end, from the room's events to the spend (§9). The request terms a receipt of version 2 binds, and what a player, a decider and a verifier do with them, are in scope too (§8.6, §8.7). Also in scope: the ad creative signalling records of a session's fills on a MoQ event timeline (§13), and the composed track a stitcher publishes for a session (§14). Out of scope: a transport; how a decider assembles each viewer's candidate pod (targeting); how beacons reach it; the SCTE-35 messages a cue may carry, which WAI carries verbatim and never reads (live-events §4.3); the stitched form of `wai.video.ssai`, which is unchanged (§8.1); and a pre-roll, which a room does not have: a viewer who joins a running break is given what remains of it (§7.4), and one who joins between breaks starts with the program. Out of scope, too: what a GPP string's sections state, which the GPP string's texts define and WAI does not decode; whether a viewer gave a consent signal, or any party honoured one; and how sellers files and authorised-sellers files are fetched and kept fresh, which their own texts say. Relationship to the core spec: `wai.live.ads` is a **record** ([SPEC](../SPEC.md) §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable. Relationship to [live-events](live-events.md): this extension changes nothing in it. A break is opened and closed by `cue` events as they are; the break annotation rides in the `meta` kind (§4.2 there), bound to its cue by the chain, as [timed-text](timed-text.md)'s annotation is bound to its caption. Live-events' own date range of each cue (§8.3 there) is written beside the interstitial date range of this extension, under another `ID` and another `CLASS`. Relationship to [live-rooms](live-rooms.md): a cue's author is held to its grant: a principal whose grant in effect does not permit `cue` is refused by the room verifier, `kind_not_permitted`, before this extension reads its cue (live-rooms §9.2 step 4). The decider runs under a room grant, whose funds ceiling bounds the room's ad spend (§3.4), and names it in its receipts as live-rooms §4.5 has every receipt name a grant. Why a record of its own, beside `wai.video.ssai`: the policy, the breaks and their renderings are verified against a room and its events track, which the stitched form never reads; the impressions are `wai.video.ssai`'s, in a second form of its receipt (§8), so that one decision rule and one spend rule serve stitched and server-guided insertion alike, and every receipt of the stitched form keeps its bytes. ## 2. Terms - **Room**, **room clock**, **principal**, **event**, **chain**, **grant**, **room key**, **merged order**: as in live-events §2 and live-rooms §2 and §9.2. - **Cue**: a `cue` event (live-events §4.3). Its **type** is out (1), in (2), chapter (3) or break (4). - **Break**: a span of the room's program that ads replace, opened by a cue of type out or break (§4.3). A break is **open** from an out until its in, and **closed** otherwise. - **Post-roll**: a break played after the program ends (§4.2). - **Avail**: the duration, in milliseconds, a break's decision fills (§4.3). - **Decider**: the principal that decides each viewer's fill and signs the receipts, under the grant the policy names (§3.4). - **Pod**: the candidate ads a decision runs over for one viewer and one break, each an `ad_id`, a `creative_hash`, a `duration_ms` and a `price_micros` (§7.1). - **Fill**: the ads a decision selects, in slot order. - **Asset list**: the response to the request a player makes for a break: an HLS asset list (§7.2) or a DASH list MPD (§7.3). - **Session**: one viewer's playback session, named in a receipt by 16 bytes the decider assigns (§8.2); under a policy that requires presentation claims, the 16 bytes the player's session key names (ad-presentation §3), which the decider does not choose. - **Consent signal**, **GPP string**: §8.6. - **Supply chain**, **node**, **sellers file**, **authorised-sellers file**, **domain name**: §8.7. Integers in the records of §3 and §4 are little-endian, and a digest there is SHA-256 (FIPS 180-4), 32 bytes. Integers of the receipt's signed bytes (§8) are big-endian and its hashes BLAKE3, as the stitched form's are. Ed25519 is RFC 8032's, verified strictly ([SPEC](../SPEC.md) §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 *long* field is a u16 length and that many bytes. ## 3. The ad policy ### 3.1 Layout | offset | size | field | meaning | |---|---|---|---| | 0 | 4 | `magic` | `WAP1` (ASCII) | | 4 | 1 | `version` | 1; 2 for a policy with presentation fields; 3 for one with presentation fields and an audience | | 5 | 32 | `room` | the room id (live-rooms §3.2) | | 37 | 32 | `decider_grant` | the digest of the room grant the decider runs under (§3.4) | | 69 | 1 | `slots` | the most ads one break holds, 1 to 16 | | 70 | 4 | `open_avail_ms` | u32: the avail of a break opened with no planned duration (§4.3); 0: such a break is refused | | 74 | 4 | `preload_ms` | u32: how long before a break a player may fetch its asset list (§5.3, §6.2); 0: no preload | | 78 | 1 | `restrict` | bit 0 **skip**: no seeking forward in a break; bit 1 **jump**: no seeking past a break unplayed | | 79 | 2 | `skip_after_s` | u16: seconds of a break played before a viewer may skip the rest; 65535: no skip control | | 81 | 1 | `snap` | bit 0 **out**: leave the program at the segment boundary nearest the break's start; bit 1 **in**: return at the one nearest its end | | 82 | long | `asset_base` | the base of every asset-list URI (§7) | | … | long | `creative_base` | the base of every creative's URI (§7) | | … | 4 + long | presentation fields | version 2 and 3: `presentation`, `viewability`, `token_type` and `issuer`, as ad-presentation §8 defines them | | … | 1 | `audience` | version 3 only: bit 0 **child**: the room is directed to children (§8.6) | | … | 64 | `sig` | §3.2 | A **base** is `https://` followed by 1 to 492 bytes, each of `!` to `~` (0x21–0x7E) other than `"`, `#`, `&`, `'`, `<`, `>`, `?` and `\`, the last of them `/`. A base so holds no character that HLS's quoted strings, JSON strings or XML attribute values would have to escape, and a URI built on it (§7) is the base followed by a name of the break or of the creative. A policy of version 3 carries the presentation fields as a policy of version 2 does, all four zero when it requires no claim, and then its `audience`. It requires every receipt to be of version 2, stating the consent signal its decision was made under, and with **child** set a child-directed one (§9.3 step 13). ### 3.2 Digest and signature The **digest** of a policy is SHA-256 over its record from `magic` through `creative_base`, or through `issuer` for a policy of version 2, or through `audience` for one of version 3. The **signature** is Ed25519 by the room descriptor's `authority` over the 20 bytes `wai:live-ads-policy` (ASCII) and 0x01, followed by the bytes the digest covers. ### 3.3 Reading and rules A reader reads exactly the bytes it is given as one policy, in the order of §3.1, and refuses at the first of these that holds: 1. Fewer than 4 bytes: `truncated`. A magic other than `WAP1`: `malformed`. No byte after the magic: `truncated`. A version other than 1, 2 or 3: `unknown_version`. 2. The bytes end before a field the layout requires: `truncated`. 3. Bytes after `sig`: `malformed`. It then refuses as `ad_policy_malformed` a policy whose `slots` is not 1 to 16, whose `restrict` or `snap` sets a bit above bit 1, whose `asset_base` or `creative_base` is not a base (§3.1), of version 2 or 3, whose presentation fields break a rule of ad-presentation §8, or, of version 3, whose `audience` sets a bit above bit 0. ### 3.4 The decider The decider is the `principal` of the grant whose digest is `decider_grant`. That grant is one of the room's grants, which the room verifier checks (live-rooms §4.3); it need not be used in a `join`, since a decider sends no event. Its `funds_ceiling` and `currency` are the room's ad budget: every price and every spend of §7 and §8 is in millionths of a minor unit of `currency`, as [live-metering](live-metering.md)'s tariff prices are, so a spend of *s* is within a ceiling of *c* minor units when *s* ≤ *c* × 10^6. A `funds_ceiling` of 2^64 − 1 states no ceiling. The decider's receipts name the grant as live-rooms §4.5 has a receipt name one (§9.3). A grant is good for a break when it is good at the break's `t` (live-rooms §4.3), and it is not revoked through the authority's key log (live-rooms §5). ## 4. Breaks ### 4.1 Cues A cue (live-events §4.3) carries a `type`, a `cue_id`, a `duration_ms` and, optionally, an SCTE-35 message this extension never reads. Its `t` is the instant of the splice on the room clock: a publisher **MAY** send a cue before that instant, and a cue anchored to a media object (live-events §3.1) splices at that object. Every cue the room accepted, of any principal permitted `cue`, the room keys among them, is read here, in the merged order (live-rooms §9.2), each once. ### 4.2 The break annotation A break is mid-roll unless its author annotates it as a post-roll. The **annotation** is the payload of a `meta` event whose first four bytes are `WAB1`, by the same principal, at the cue's `seq` + 1 and at the cue's `t`: | offset | size | field | meaning | |---|---|---|---| | 0 | 4 | `magic` | `WAB1` (ASCII) | | 4 | 1 | `version` | 1 in this revision | | 5 | 32 | `cue` | the digest of the cue it annotates | | 37 | 1 | `placement` | 1 mid-roll, 2 post-roll | An annotation is refused `annotation_malformed` when it is not 38 bytes, its version is not 1, or its `placement` is not 1 or 2. It is refused `annotation_misplaced` unless the event before it in its author's chain, at `seq` − 1, is a cue of type break, at the same `t`, whose digest is `cue`. A **post-roll** is the break of an annotation of placement 2. It ends the program: after it, the room has no break and no cue (§4.3, `cue_after_close`), and a room that closes ends with it. A player plays it after the program has ended (§5.1, §6.2). ### 4.3 The schedule The **schedule** is the room's breaks and chapters, in the order of the cues that open them. Over the cues and annotations in the merged order, holding the open break (at most one) and the end of the last closed break, a verifier refuses, at the record: - any cue after a post-roll's annotation: `cue_after_close`; - **out** (1): when a break is open, or `t` is before the end of the last closed break, `break_overlap`. Otherwise it opens a break: its **planned** duration is `duration_ms`, and its avail is `duration_ms`, or the policy's `open_avail_ms` when `duration_ms` is 0; with both 0, `break_duration_unknown`; - **in** (2): unless the open break was opened by an out of this `cue_id` and of the in's author, `cue_in_unmatched`. The break's **duration** is ⌊(`t` − the out's `t`) / 1000⌋ milliseconds: when it is 0, `break_zero_duration`; when the in's `duration_ms` is not 0 and is not the duration, `cue_duration_mismatch`. The break is then closed, and its end is the in's `t`; - **chapter** (3): a chapter of the schedule, its `t` and its `duration_ms` (0 when not known). A chapter opens and closes nothing, and may fall inside a break; - **break** (4): when `duration_ms` is 0, `break_zero_duration`; when a break is open, or `t` is before the end of the last closed break, `break_overlap`. Otherwise it is a closed break whose duration and avail are `duration_ms`, ending at `t` + `duration_ms` × 1000; - an **annotation** (§4.2): read and placed, or refused; of placement 2, its break is the post-roll. A break open at the end is stated open: the room may still be live. A cue of the room that does not read is the room's refusal, and an event with a gap before it is a delivery gap the room states (§9.2 step 1). ### 4.4 A break's identity A break is named by the digest of the cue that opens it. From that digest: - its HLS **ID** is `wai-ad-` followed by the digest's 64 lowercase hexadecimal digits; - its DASH **event id** is the digest's first eight bytes read as a big-endian u64, of which the low 53 bits are kept, so that a player that holds an id as a double holds it exactly. A receiver identifies a break by its digest; the id lets a player tell one event from another in an EventStream, where a repeat of 53 bits among a room's breaks is possible but not expected. Every edge holding the room's events derives the same identity, and no other event of the room has it: live-events' own date range of the cue has the digest alone as its `ID`. ## 5. HLS: interstitial date ranges ### 5.1 The interstitial tag Each break is one Interstitial `EXT-X-DATERANGE` tag (`draft-pantos-hls-rfc8216bis-22` §4.4.5.1, Appendix D.2), written into every Media Playlist of the room when its cue is known. Its attributes are written in this order, separated by commas with no spaces, each only where its row says: | attribute | value | |---|---| | `ID` | quoted: the break's ID (§4.4) | | `CLASS` | quoted: `com.apple.hls.interstitial`, the class Appendix D.2 defines | | `START-DATE` | quoted: `t0` + `t` of the opening cue, as `YYYY-MM-DDTHH:MM:SS.mmmZ` in UTC, the milliseconds floored (live-events §8.3) | | `CUE` | quoted: `ONCE` for a mid-roll, `POST,ONCE` for a post-roll (§4.4.5.1) | | `DURATION` | a break of type 4: its duration as decimal seconds with three digits after the point (`30.000`) | | `PLANNED-DURATION` | an out with a planned duration: that duration, as `DURATION` | | `X-ASSET-LIST` | quoted: the break's HLS asset-list URI (§7.2) | | `X-RESUME-OFFSET` | a mid-roll of type 4: its duration, as `DURATION` | | `X-PLAYOUT-LIMIT` | a break of type 4: its duration, as `DURATION` | | `X-SNAP` | when `snap` is not 0: quoted, `OUT`, `IN` or `OUT,IN` by its bits | | `X-RESTRICT` | when `restrict` is not 0: quoted, `SKIP`, `JUMP` or `SKIP,JUMP` by its bits | | `X-SKIP-CONTROL-OFFSET` | when `skip_after_s` is not 65535: `skip_after_s` in decimal (Appendix D.3) | - `ONCE` keeps a viewer who seeks back over a break in a window of the room from being shown it again; `POST` plays the post-roll once the program has played to its end, which for a room is its playlist's `EXT-X-ENDLIST`. - `X-RESUME-OFFSET` equal to the break's duration returns the viewer to the program where the break ends on the room's timeline, whatever the fill's length, so the program a viewer returns to is the program after the in. A post-roll has nothing to resume, and no `X-RESUME-OFFSET`. - An out whose end is not yet known has no `DURATION`, `X-RESUME-OFFSET` or `X-PLAYOUT-LIMIT`: the closing tag adds them (§5.2). ### 5.2 The closing tag When an out's in closes it, the break gains a second tag with the same `ID`, as §6.2.1 has a server signal the actual end of a date range with a planned duration, and as §4.4.5.1 lets a later tag add attributes to a date range: | attribute | value | |---|---| | `ID` | quoted: the break's ID | | `CLASS` | quoted: as §5.1 | | `DURATION` | the break's duration (§4.3), as §5.1 writes durations | | `X-RESUME-OFFSET` | the same value | | `X-PLAYOUT-LIMIT` | the same value | No attribute of the first tag changes: the closing tag adds three it did not have, and repeats `CLASS` with its value. A player inside the break when the closing tag arrives ends the break at the playout limit, and returns to the program where the in placed its end. ### 5.3 The preload tag When the policy's `preload_ms` is not 0, each break also has a Preload `EXT-X-DATERANGE` tag (Appendix F, Appendix D.2), written just before its interstitial tag: | attribute | value | |---|---| | `ID` | quoted: the break's ID followed by `-preload` | | `CLASS` | quoted: `com.apple.hls.preload`, the class Appendix F defines | | `START-DATE` | quoted: the interstitial's `START-DATE` less `preload_ms`, in its form | | `DURATION` | `preload_ms`, as §5.1 writes durations | | `X-URI` | quoted: the break's HLS asset-list URI (§7.2) | | `X-TARGET-ID` | quoted: the break's ID | | `X-TARGET-CLASS` | quoted: the interstitial class | A player picks a random instant in the preload range and fetches the asset list there (Appendix D.2), so the requests of a room's audience for one break are spread over `preload_ms` rather than arriving together at its start. ### 5.4 What stays shared Every attribute of §5.1 to §5.3 is a function of the signed cue, its annotation, the signed policy and the room's `t0`, and of nothing a viewer supplies. Every Media Playlist of the room therefore carries the same date ranges with the same attributes, as §6.2.4 requires, and every edge that holds the room's events writes the same bytes, so playlists stay cacheable. The one URI a break names is where each viewer's decision is made (§7), and the request for it carries what makes it a viewer's: the `_HLS_primary_id` and `_HLS_start_offset` query parameters Appendix D.4 has a player add. A playlist that carries these tags carries `EXT-X-PROGRAM-DATE-TIME` on the same UTC timeline as `t0` (§4.4.5.1). The corpus states the tags of each room in the order a packager writes them: a break's preload and interstitial tags at its cue, its closing tag at its in. ## 6. DASH: alternative presentation events ### 6.1 What this revision read ISO/IEC 23009-1:2026 defines alternative presentation events in its §5.16, whose text this revision could not read. It read the sixth edition's MPD schema and its example MPDs. From them it takes: the event schemes `urn:mpeg:dash:event:alternativeMPD:replace:2025` and `urn:mpeg:dash:event:alternativeMPD:insert:2025` (example G23 and G29-1 use the first); an `Event`'s `presentationTime`, `duration` and `id`; its child `ReplacePresentation` or `InsertPresentation`, with the attributes `uri` (required), `earliestResolutionTimeOffset`, `maxDuration`, `executeOnce`, `skipAfter` and, for a replacement, `returnOffset`, with their types; and the **list MPD**, an MPD of `type` `list` whose Periods each import one MPD (`ImportedMPD`). The values below are written from those types and examples. Where the schema has an attribute whose meaning this revision could not state, it does not write it: `noJump`, `serviceDescriptionId`, `startWithOffset` and `clip`, whose defaults stand. A later revision checks every value here against §5.16. ### 6.2 The event A mid-roll is an `Event` of the replace scheme; a post-roll is one of the insert scheme, since after the program there is nothing to replace. With `timescale` 1000 on its EventStream: | attribute | value | |---|---| | `presentationTime` | `pt0` + ⌊`t` / 1000⌋, `t` the opening cue's, where `pt0` is the presentation time of `t0` in milliseconds on the Period's timeline (live-events §8.4) | | `duration` | when the break's duration is known (a break of type 4, or an out its in has closed): that duration in milliseconds | | `id` | the break's event id (§4.4) | and its one child, `ReplacePresentation` for a mid-roll and `InsertPresentation` for a post-roll: | attribute | value | |---|---| | `uri` | the break's DASH asset-list URI (§7.3) | | `earliestResolutionTimeOffset` | when `preload_ms` is not 0: `preload_ms` | | `maxDuration` | when the duration is known: the duration in milliseconds | | `executeOnce` | `true`, as HLS's `ONCE` | | `skipAfter` | `PT`, then `skip_after_s` in decimal, then `S`; with no skip control, the avail in whole seconds rounded up (`PT30S` for 30 000 ms), so that no viewer skips before the avail has played: the schema's examples read a missing `skipAfter` as a break a viewer may skip at once | | `returnOffset` | a `ReplacePresentation` whose duration is known: the duration in milliseconds, as HLS's `X-RESUME-OFFSET` | Attributes are written in the order of the two tables, each as `name="value"`, the element as ``, with no whitespace other than one space before each attribute. An out's event is written first without `duration`, `maxDuration` and `returnOffset`, and the MPD that follows its in writes the same event with them: an MPD update gives the event its end as HLS's closing tag does (§5.2). A planned duration has no DASH attribute. ### 6.3 The event streams The Period that holds the room's program carries, after every break of the schedule is written: 1. when the schedule has a mid-roll, ``, the mid-rolls' events in schedule order, and ``; 2. when it has a post-roll, the same with the insert scheme and the post-roll's event. Every value is a function of the signed records, the policy and `pt0`, so the MPD stays one for every viewer, as the playlists do (§5.4). ## 7. The asset list ### 7.1 The decision A player that reaches a break fetches its asset list. The decider fills it for that viewer with the decision of `wai.video.ssai` (SPEC §5), unchanged: - its inputs are the viewer's pod, the policy's `slots`, an **avail** and the **funds remaining**: the break's avail (§4.3) less the viewer's start offset (§7.4), or 0 when the offset reaches it; and the funds the decider states it had left of the decider grant's ceiling, at most that ceiling (§3.4); - the candidates are taken in order of `price_micros` descending, ties by `ad_id` ascending (as bytes); each is taken when fewer than `slots` are taken, its `duration_ms` is at most the avail not yet taken and its `price_micros` at most the funds not yet taken; the fill is the taken candidates in that order, slot 0 first. The fill is a pure function of those inputs, so anyone holding them recomputes it, and the receipt binds each of them (§8). ### 7.2 HLS asset list The break's **HLS asset-list URI** is `asset_base`, the break's ID, `.json?_HLS_interstitial_id=` and the ID again: the query parameter Appendix D.4 has a packager add. The response is the JSON object Appendix D.2 defines, written exactly: ``` {"ASSETS":[{"URI":".m3u8","DURATION":},…]} ``` one Asset-Description for each ad of the fill in slot order, `` its 64 lowercase hexadecimal digits and `` its `duration_ms` as decimal seconds with three digits after the point, with no whitespace and no byte after the closing brace. An empty fill is `{"ASSETS":[]}`, which has a player resume the program without playing an ad (Appendix D.5). The asset list carries no `SKIP-CONTROL`: the tag's attributes stand. A creative's URI names it by its hash, so the asset list a viewer is served binds the creative bytes the receipt names. ### 7.3 DASH list MPD The break's **DASH asset-list URI** is `asset_base`, the break's ID and `.mpd`. The response is a list MPD, written exactly as the XML declaration ``, a line feed, and: ``` .mpd… ``` one Period for each ad of the fill, its `id` the slot in decimal and `` as §7.2, with no whitespace other than that shown and no byte after ``. The profile and `minBufferTime` are those of the schema's list-MPD example (G29-2). An MPD has at least one Period, so an empty fill has no list MPD: the decider answers the request with no body, and the receipt binds the empty asset list, zero bytes (§8.2). ### 7.4 Joining a break A viewer who reaches a break after its start, by joining the room inside it, has its player add the offset into the break at which it starts (`_HLS_start_offset`, Appendix D.4). The decider fills the remaining avail only (§7.1), and the receipt states the offset (`start_offset_ms`). A viewer who joins between breaks is given no break: a room has no pre-roll. The asset list a joining viewer is served is a fill like any other, and every check of §9.3 applies to it. ## 8. The break form of the `wai.video.ssai` receipt ### 8.1 Forms A `wai.video.ssai` receipt has two forms: - **form 1, stitched**: an avail of an attested manifest, named by `avail_id` and `manifest_hash`, as SPEC §5 registers it. This revision changes nothing in it: its signed bytes, its identity and every receipt of it stay as they were; - **form 2, break**: a break of a room's schedule and the asset list one viewer was served for it, defined here. The two forms sign under different domains (§8.4), so no signature of one is a signature of the other, and they share the impression, the pod hash, the Merkle root, the billable spend and the identity domain, so a figure profile of energy-measurement §6.1, `wai:video-ssai-receipt-id`, names either. ### 8.2 Fields and their JSON carriage A form-2 receipt is carried as one JSON object (RFC 8259) with exactly these members; a byte field is lowercase hexadecimal of its exact length, and an integer a JSON integer from 0 to 2^64 − 1: | member | value | |---|---| | `form` | 2 | | `room` | 32 bytes: the room id | | `cue` | 32 bytes: the break's digest (§4.4) | | `session` | 16 bytes: the session (§2), one value for every break of one playback session, and no other's | | `start_offset_ms` | the viewer's offset into the break (§7.4); 0 for a viewer present at its start | | `format` | from 0 to 255: 1, the HLS asset list (§7.2); 2, the DASH list MPD (§7.3) | | `asset_list_sha256` | 32 bytes: SHA-256 of the asset list served, its exact bytes | | `avail_ms` | the break's avail (§4.3) | | `slots` | the policy's `slots` | | `funds_remaining_micros` | the funds the decision ran with (§7.1) | | `pod_hash` | 32 bytes: §8.3 | | `impressions` | an array: each filled slot, `{"slot", "ad_id", "creative_hash", "duration_ms", "price_micros", "quartiles_reached"}`, in slot order; `ad_id` a string, `creative_hash` 32 bytes, `quartiles_reached` 0 to 255 | | `merkle_root` | 32 bytes: §8.3 | | `budget_spent_micros` | the billable spend (§8.3) | | `work_decide_ops` | the candidates the decision considered | | `joules_micro` | the decision's energy, or 0 unmetered | | `energy_provenance` | its class's label (energy-measurement §2.7: `ModelBased`, `Estimator`, `OnChipCounter`, `CalibratedInstrument`), or `null` unmetered | | `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | only for a class that carries them, as energy-measurement §2.7 names and writes them: `{"relative_ppm", "window_us"}`, `relative_ppm` from 0 to 2^32 − 1; `Unknown`, `Off` or `On`; a string | | `grant` | `{"grant_hash", "capability", "joule_ceiling_micro", "funds_ceiling"}`: the grant reference (live-rooms §4.5), `funds_ceiling` `null` for none | | `signer_pubkey` | 32 bytes: the decider's Ed25519 key | | `signer_id` | a string: the decider's `did:key` or origin-bound id | | `sig` | 64 bytes: §8.4 | | `version` | a receipt of version 2 alone: 2. A receipt with no `version` member is of version 1 | | `consent` | version 2 only: the consent signal (§8.6) | | `supply_chains` | version 2 only: the supply chains (§8.7), one for each impression, in the impressions' order, each `null` when the decider states none for it | `quartiles_reached` is the stitched form's: 0 never rendered, 1 started (the billable impression), 2 to 5 a quarter, a half, three quarters and all of it. A receipt that does not read so, or that carries the legacy class label `HwShunt` (energy-measurement §2.6), is refused `receipt_malformed`. A receipt of version 1 has none of `version`, `consent` and `supply_chains`, and one of version 2 has all three; their members are §8.6's and §8.7's, each of its type, or the receipt is refused `receipt_malformed`. ### 8.3 Hashes BLAKE3, with integers big-endian, as the stitched form computes them: - the **pod hash**: over `wai:video-ssai-pod` 0x01, the candidates' count (u64), and for each candidate in the pod's order, `ad_id`'s length (u32) and bytes, `creative_hash`, `duration_ms` (u64) and `price_micros` (u64); - an impression's **leaf**: over `wai:video-ssai-imp` 0x01, `slot` (u64), `ad_id`'s length (u32) and bytes, `creative_hash`, `duration_ms` (u64), `price_micros` (u64) and `quartiles_reached` (u8); - the **Merkle root** over the leaves in slot order: [jwp-receipts](jwp-receipts.md) §2.2's tree, each leaf taken as a content hash (its first leaf form) and the final one duplicated on an odd level; all zero for no impression; - the **billable spend**: the sum of `price_micros` over the impressions whose `quartiles_reached` is at least 1. ### 8.4 Signing payload and identity The **signing payload** is the concatenation of: 1. `wai:video-ssai-break-receipt` (ASCII) and 0x01; 2. `room`, `cue`, `session`, `start_offset_ms` (u64), `format` (u8), `asset_list_sha256`, `avail_ms` (u64), `slots` (u64), `funds_remaining_micros` (u64); 3. `pod_hash`, `merkle_root`, `budget_spent_micros` (u64), `work_decide_ops` (u64), `joules_micro` (u64), then the class in its signed encoding (energy-measurement §2.7), the tag byte 0 for none; 4. the grant reference: `grant_hash`, the length (u32) and bytes of `capability`, `joule_ceiling_micro` (u64), then 1 and `funds_ceiling` (u64), or 0 for none; 5. the impressions' count (u64), then each one's leaf; 6. `signer_pubkey`, then the length (u32) and bytes of `signer_id`. `sig` is Ed25519 by `signer_pubkey` over the signing payload. The receipt's **identity**, its receipt hash, is BLAKE3 over `wai:video-ssai-receipt-id` 0x01, the signing payload and `sig`: the stitched form's identity domain, over a payload whose first bytes no stitched payload has. A receipt of **version 2** signs, in place of item 1, `wai:video-ssai-break-receipt` (ASCII) and 0x02, and, between items 5 and 6, its consent signal and then its supply chains, each in its signed encoding (§8.6, §8.7). No receipt of version 1 is one of version 2, and every receipt of version 1 keeps its bytes and its identity. ### 8.5 Its own rules A form-2 receipt holds, on its own: 1. `format` is 1 or 2; 2. its signature verifies (`bad_key`, `bad_signature`); 3. `merkle_root` is the root of its impressions, `budget_spent_micros` their billable spend, and when `grant`'s `funds_ceiling` is not `null`, the spend is within it (§3.4); 4. the impressions' slots are 0, 1, 2, … in order, each `quartiles_reached` is at most 5, and `work_decide_ops` is at least their count; 5. the figure and its class cohere as energy-measurement §2 has them: `joules_micro` 0 with no class, or above 0 with a class, a hardware class's declared uncertainty with `relative_ppm` and `window_us` above 0 and at the resolution floor (`joules_micro` × `relative_ppm` at least 288 676), and a calibrated instrument's reference holding a character that is not Unicode White_Space; 6. of version 2, its consent signal holds the rules of §8.6, and its supply chains the rules of §8.7. A rule of 1, 3, 4 or 5 that fails is `receipt_invalid`. These rules are what a buyer holding one receipt checks alone; §9.3 adds what the room, the policy and the pod let a verifier check. Rule 6 that fails is `consent_invalid` for the consent signal and `supply_chain_invalid` for the chains. ### 8.6 The consent signal A receipt of version 2 states, in its member `consent`, the **consent signal** its decision was made under: | member | value | |---|---| | `gpp` | the Global Privacy Platform string (the **GPP string**) the player gave with its request for the asset list, byte for byte; `""` when it gave none | | `gpp_sid` | the sections of that string the player gave as applying to the request, each a section id from 1 to 65 535, in the order given; `[]` for none | | `child_directed` | 1 when the request is directed to children: the policy sets **child** (§3.1), or the player's request states so; 0 otherwise | They are three of the members OpenRTB 2.6 gives an ad request's `Regs` object, `gpp`, `gpp_sid` and `coppa` (its §3.2.3), and the GPP string is the one the *Global Privacy Protocol String* defines. The signal holds these rules, or the receipt is refused `consent_invalid` (§8.5 rule 6): 1. `gpp` is at most 65 535 bytes, each an ASCII letter or digit, `-`, `_`, `.` or `~`, the characters the GPP string's text lists as URL-safe; and when it is not empty, no part of it is empty between two of its `~` delimiters, before the first or after the last: the header and each section are there; 2. `gpp_sid` holds at most 16 section ids, no two the same, each at least 1, and none when `gpp` is empty; 3. `child_directed` is 0 or 1. Its **signed encoding** (§8.4), big-endian as every field of the receipt: the length (u32) and bytes of `gpp`; the count (u64) of `gpp_sid`, then each section id (u16); `child_directed` (u8). What each party does with it: - **The player.** A player that holds a GPP string for the viewer from its platform's consent tool **MUST** give it, with the sections that apply, with its request for the asset list, as it holds it, by a means this revision does not fix (for one, the request's parameters `gpp` and `gpp_sid`, as the GPP string's text names them for a service reached by a URL); a player whose request is directed to children states so the same way. A player that holds no string gives none. It **MUST NOT** write, alter or complete a GPP string: the GPP string's text has a property's owner or its consent tool make one, and every other party pass it on as it is. - **The decider.** A decider **MUST** state in the receipt the string and the sections it was given, byte for byte, and **MUST** pass them unchanged in every request it makes to fill the break (an OpenRTB 2.6 request's `Regs.gpp` and `Regs.gpp_sid`, or a URL's `gpp` and `gpp_sid`). It **MUST** set `child_directed` when the policy sets **child** or the request states it, and **MUST** then mark every request it makes to fill the break as directed to children (an OpenRTB 2.6 request's `Regs.coppa` 1). It **MUST NOT** clear a flag the policy or the request set, and **MUST NOT** state a string or a section it was not given. - **What no party infers.** An empty `gpp` states that no string was given: not that the viewer consented, and not that no law applies to the request. A `child_directed` of 0 states that neither the policy nor the request marked the request as directed to children: not that the viewer is not a child. Neither the string nor the flag names the viewer, and a decider or a verifier **MUST NOT** use either to identify a viewer, or to link one viewer's receipts to one another. That rule binds those parties and does not keep receipts from linking: a GPP string that is not empty is made for the viewer by a consent tool, not for one request, and can stay the same, byte for byte, across the viewer's sessions and rooms, so whoever holds two receipts that carry it can link them through it (§12). - **The verifier.** A verifier reads the string's characters and its parts, and does not decode its sections: what a section states, and whether it permits what was done, is for the GPP string's texts and the parties to judge. A receipt that verifies shows what the decider signed that it was given and passed on; not that the viewer gave it, and not that any party honoured it. ### 8.7 The supply chain A receipt of version 2 states, in its member `supply_chains`, the chain of sellers each of its impressions was sold through: the `SupplyChain` object (OpenRTB 2.6 §3.2.25 and §3.2.26, and the *OpenRTB SupplyChain object*) of the request whose answer gave the decider that impression's candidate, with references to the files that let a buyer check each seller. A pod's candidates can come from the answers to several requests, each request with its own chain, so the member is an array with one entry for each impression, in the impressions' order, and two impressions whose candidates answered one request state the same chain. WAI carries the object and the references as those texts define them, and defines no supply-chain scheme of its own. An entry is `null` when the decider states no chain for its impression, or an object: | member | value | |---|---| | `ver` | the object's `ver`: its version, `major.minor` (`1.0` in the revision read) | | `complete` | its `complete`: 1 when the chain holds every node back to the owner of the inventory, 0 otherwise | | `nodes` | its `nodes`, in its order: the first the initial seller (in a complete chain, the owner's own account), the last the one that sent the request; each an object of the members below | | `authorised` | the authorised-sellers file the chain is checked against, `{"file", "domain", "sha256"}`, or `null` | A node: | member | value | |---|---| | `asi` | the canonical domain of the advertising system (the SSP, exchange or other system that bidders connect to) | | `sid` | the seller's or reseller's account id in that system | | `hp` | 1 when the node is in the flow of payment for the inventory, 0 otherwise | | `rid`, `name`, `domain` | the node's request id, the name and the business domain of the entity paid under `sid`, each `null` when the node has none | | `sellers` | SHA-256 of the advertising system's **sellers file**, the file at `https:///sellers.json` (*Sellers.json* 1.0), its bytes as the decider held them when it made the decision; `null` when it held none | In `authorised`: `file` is `ads.txt` for a site's **authorised-sellers file** (*ads.txt Specification* 1.1 §3.1) or `app-ads.txt` for an app's (*Authorized Sellers for Apps (app-ads.txt)* 1.0), at `https:///` followed by `file`; `domain` is the host the file is at, the site's or the one derived from the app's developer URL; and `sha256` is SHA-256 of the file's bytes as the decider held them. The object's `ext` members are not carried. A **domain name** is 1 to 253 bytes: two labels or more separated by `.`, each 1 to 63 ASCII letters, digits and `-`, neither beginning nor ending with `-`. The texts read have `asi` and a node's `domain` be a bare domain, never a URL. Each chain holds rules 1 to 3, and the member rule 4, or the receipt is refused `supply_chain_invalid` (§8.5 rule 6): 1. `ver` is one or more digits, `.` and one or more digits, at most 15 bytes, and `complete` is 0 or 1; 2. `nodes` holds 1 to 32 nodes; each node's `asi` is a domain name, its `sid` 1 to 255 bytes, its `hp` 0 or 1, its `rid` and `name` `null` or 1 to 255 bytes, and its `domain` `null` or a domain name; 3. `authorised`, when it is not `null`, has a `file` of `ads.txt` or `app-ads.txt` and a `domain` that is a domain name; 4. `supply_chains` holds as many entries as the receipt holds impressions. Its **signed encoding** (§8.4), big-endian: the entries' count (u64), then each entry: 0 for `null`; or 1, then `ver` (its length, u32, and its bytes), `complete` (u8), the nodes' count (u64), and for each node `asi` and `sid` (each its length, u32, and its bytes), `hp` (u8), each of `rid`, `name` and `domain` as 0, or 1 and its length (u32) and bytes, and `sellers` as 0, or 1 and its 32 bytes; then `authorised` as 0, or 1, `file` and `domain` (each its length, u32, and its bytes) and `sha256`. **The files.** A verifier finds a file a chain names among the supply files it holds by its SHA-256 (§9.1), and reads it so: - a **sellers file** reads when it is UTF-8 with no byte order mark at its start, and a JSON text (RFC 8259) within the limits that RFC 8259 leaves a reader to set (its §4, §8.1, §8.2 and §9), which are these: its arrays and objects nest at most 64 deep, the file's own object counted as the first; its value is an object with a member `sellers` that is an array of objects, the **sellers**; each seller has a member `seller_id` that is a string, and when it has a `seller_type`, that is a string too; neither the file's object nor a seller has two members of one of these three names; and no member name of the file's object or of a seller, nor the string of a `seller_id` or a `seller_type`, holds a `\u` escape of a surrogate (U+D800 to U+DFFF) unless it is a high one followed at once by a low one, the pair that escapes one character (RFC 8259 §7). The rest of the file is read only by RFC 8259's grammar: a number is not converted to a value, so one of any size reads, and a string's escapes are not decoded, so one that escapes a lone surrogate reads; - an **authorised-sellers file** reads when it is UTF-8, and is read as *ads.txt Specification* 1.1 §3.2 to §3.5 describe it, by these rules: a line ends at LF, CR LF or CR; a `#` and the rest of its line are a comment, and dropped; a line is trimmed of spaces and tabs, and passed over when it is then empty; a line whose part before its first `=` is not empty and holds no `,`, space or tab is a variable record, and passed over; any other line, with the part from its first `;` (its extension fields) dropped, is a **data record** when it is three or four fields separated by `,`, each not empty once trimmed of spaces and tabs: the advertising system's domain, the account id, the relationship and, optionally, a certification authority's id. Every other line is passed over, as the text has a consumer read liberally. A node is **listed** in a sellers file that has a seller whose `seller_id` is its `sid`, byte for byte; the first such seller is the node's **seller** there. A node is **authorised** by an authorised-sellers file that has a data record whose first field is its `asi`, ASCII letters compared without regard to case, whose second field is its `sid`, byte for byte, and whose relationship, compared without regard to case, is `DIRECT`, or, for any node but the first of a complete chain, `RESELLER`. The first node of a complete chain is the owner's own account, which *ads.txt Specification* 1.1 §3.3 has the owner list as `DIRECT`; an account another party controls to resell is `RESELLER`. What each party does with it: - **The decider.** For each impression, a decider **MUST** state the chain of the request whose answer gave it the impression's candidate, node for node as that request carried it, and `null` only when that request carried no chain or no request gave the candidate; and it **MUST** state as a node's `sellers`, or as `authorised`, only the digest of a file it held when it made the decision. It **MUST NOT** state a node the request did not carry, leave out one it did, or state for one impression the chain of another impression's request. - **The verifier.** A verifier that holds a file a chain names checks the chain against it (§9.3 step 14); a part whose file it does not hold stays unchecked, and the verification says so (§9.4). What it checks is the chain against the bytes the decider bound: whether those are the files the advertising systems and the owner publish now is for the parties, who fetch them, to judge. ## 9. Verification ### 9.1 Inputs An ads verifier is given what a room verifier is given (live-rooms §9.1: a descriptor, grants, a key-log head or none, and the chains), and: - **policy**: the room's ad policy; - **pt0**: the presentation time of `t0`, in milliseconds on the Period's timeline (§6.2); - **receipts**: form-2 receipts in JSON (§8.2), in a list, each with the **pod** its decision ran over, the candidates in the order the decider gave them; - under a policy that requires presentation claims, the **claims**, **creatives** and **issuers** of ad-presentation §9.1; - **supply files**: the sellers files and authorised-sellers files it holds, by their bytes, in any order; a receipt's chains name each by its SHA-256 (§8.7). ### 9.2 Steps It runs these steps and stops at the first refusal, which it reports with the reason and where: 1. **The room** (live-rooms §9): a refusal is the ads verifier's, at `room` with the room verifier's own place. A gap is the outcome **gap**: a schedule read from part of the cues is not the room's, so the verifier states the gaps and nothing more. 2. **The policy**, at `policy`: it reads (§3.3: `truncated`, `malformed`, `unknown_version`, `ad_policy_malformed`); its `room` is the room id (`wrong_room`); its signature verifies under the descriptor's `authority` (`bad_key`, `bad_signature`); `decider_grant` is the digest of a grant the room verifier holds (`decider_grant_unknown`); and that grant is not revoked through the key log the room verifier read (live-rooms §5, `grant_revoked`). 3. **The schedule** (§4.2, §4.3), over the events the room accepted in the merged order, each once: the refusal, at the record's chain and its index in that chain. 4. **The renderings** (§5, §6): the tags, the events and the asset-list URIs of every break. No refusal. 5. **The receipts**, in the list's order, each by §9.3: the refusal, at `receipt` and its index. Under a policy that requires presentation claims, the claims are read before the first receipt, and each receipt's claims are judged after its step 11 (ad-presentation §9.2); a claim's verdict never refuses the room. 6. **The outcome** (§9.4). ### 9.3 Each receipt For receipt *k*, with its pod, in this order: 1. It reads as §8.2 has it: `receipt_malformed`. 2. Its `room` is the room id: `wrong_room`. 3. Its own rules (§8.5): `bad_key`, `bad_signature` or `receipt_invalid`. 4. `signer_pubkey` is the decider (§3.4): `receipt_not_decider`. 5. `grant` is the decider grant's reference: `grant_hash` its digest, `capability` `wai.live.room`, `joule_ceiling_micro` its `joules_ceiling`, and `funds_ceiling` its `funds_ceiling`, or `null` when that is 2^64 − 1 (live-rooms §4.5): `receipt_grant_mismatch`. 6. `cue` names a break of the schedule: `receipt_break_unknown`. 7. The decider grant is good at the break's `t` (§3.4): `outside_grant_window`. 8. `pod_hash` is the pod hash (§8.3) of the pod given: `pod_mismatch`. 9. `avail_ms` is the break's avail, `slots` the policy's, and the impressions, less `quartiles_reached`, are the fill §7.1 gives for the pod, the slots, the avail less `start_offset_ms` and `funds_remaining_micros`: `decision_mismatch`. 10. `asset_list_sha256` is SHA-256 of the asset list of the fill in the receipt's `format` (§7.2, §7.3): `asset_list_mismatch`. 11. No receipt before it in the list names this `cue` and this `session`: `receipt_duplicate`. A viewer is filled once for a break. 12. **Spend-equivalence.** `funds_remaining_micros` is within the decider grant's ceiling, and so is the spend that counts of this receipt and every receipt before it, together (§3.4): otherwise `funds_exceeded`. The spend that counts is a receipt's billable spend, or, under a policy that requires presentation claims, its presented spend: the prices of the impressions whose claim verifies (ad-presentation §9.3). Step 12 holds the room's ad spend to one ceiling across every viewer and break, in the order the receipts are given. A decision's stated funds are the decider's statement; that the spend of every receipt together stays within the ceiling is checked, whatever was stated. 13. **The consent signal**, under a policy of version 3: the receipt is of version 2 (`request_terms_absent`); and when the policy sets **child**, its `child_directed` is 1 (`child_directed_unset`). 14. **The supply chains**, of a receipt of version 2: for each chain it states, in the impressions' order, and in it each node in order whose sellers file the verifier holds, that file reads (`supply_file_malformed`) and lists the node (`supply_seller_unlisted`); then, when the verifier holds the authorised-sellers file the chain names, that file reads (`supply_file_malformed`) and authorises every node, in order (`supply_seller_unauthorised`) (§8.7). A chain two impressions state is checked for each. Steps 13 and 14 read what the receipt signed and the bytes of the files held; neither changes the decision, the asset list or the spend. ### 9.4 Outcomes and what a verification states - **refuse**: the reason and where (§9.2). - **gap**: the room has a gap: its gaps, under live-events' code `delivery_gap`. - **accept**: no refusal and no gap. With accept, the verifier states: - each break, in schedule order: its digest, ID and event id, its author, `cue_id`, `t` and type (out or break), its placement, its planned duration and avail, and, once closed by an in, the in's digest, its end and its duration; or that it is open; - each chapter: its digest, `t` and `duration_ms`; - the HLS tags, in the order of §5.4; each break's HLS and DASH asset-list URIs; the DASH event streams (§6.3), and each out's event as it was written before its in; - each receipt: its identity (§8.4), the asset list it binds, written by the verifier, and its billable spend; and the spend of every receipt together, the spend that counts (§9.3 step 12); - under a policy that requires presentation claims, what ad-presentation §9.4 states; - each receipt of version 2: its consent signal, as §8.6 writes it; and its supply chains, one for each impression in its order, each `null` when it states none, or: its `complete`, and for each node its `asi` and `sid`, whether the verifier held its sellers file (`sellers_checked`), the `seller_type` of its seller there (§8.7), as written, or `null`, and, when the authorised-sellers file was held, the relationship of the first record that authorises it, in capitals, or `null`; and whether that file was held (`authorised_checked`). ## 10. Reasons The codes this extension adds, each with where it is raised: | reason | raised at | |---|---| | `ad_policy_malformed` | §3.3, §9.2 step 2 | | `decider_grant_unknown` | §9.2 step 2 | | `annotation_malformed` | §4.2 | | `annotation_misplaced` | §4.2 | | `cue_after_close` | §4.2, §4.3 | | `break_overlap` | §4.3 | | `break_duration_unknown` | §4.3 | | `break_zero_duration` | §4.3 | | `cue_in_unmatched` | §4.3 | | `cue_duration_mismatch` | §4.3 | | `receipt_malformed` | §8.2, §9.3 step 1 | | `receipt_invalid` | §8.5, §9.3 step 3 | | `receipt_not_decider` | §9.3 step 4 | | `receipt_grant_mismatch` | §9.3 step 5 | | `receipt_break_unknown` | §9.3 step 6 | | `pod_mismatch` | §9.3 step 8 | | `decision_mismatch` | §9.3 step 9 | | `asset_list_mismatch` | §9.3 step 10 | | `receipt_duplicate` | §9.3 step 11 | | `funds_exceeded` | §9.3 step 12 | | `consent_invalid` | §8.5 rule 6, §8.6, §9.3 step 3 | | `supply_chain_invalid` | §8.5 rule 6, §8.7, §9.3 step 3 | | `request_terms_absent` | §9.3 step 13 | | `child_directed_unset` | §9.3 step 13 | | `supply_file_malformed` | §8.7, §9.3 step 14 | | `supply_seller_unlisted` | §8.7, §9.3 step 14 | | `supply_seller_unauthorised` | §8.7, §9.3 step 14 | An ads verifier also raises every code a room verifier raises (live-rooms §11 and the live-events codes it lists), at the room: `kind_not_permitted` among them for a cue or an annotation by a principal whose grant does not permit its kind; `truncated`, `malformed` and `unknown_version` for a policy that does not read; `wrong_room`, `bad_key` and `bad_signature` for a policy or a receipt of another room or whose signature fails; `grant_revoked` and `outside_grant_window` for the decider grant (§9.2 step 2, §9.3 step 7); and `delivery_gap` for a gap, which is not a refusal. ## 11. Conformance [`live-conformance/ads/`](../live-conformance/ads/) holds the corpus, written from fixed seeds by `wai_live_ads_vectors gen`: `vectors.json`, and each accepted room's HLS tags as a file of its own, `NAME.m3u8`, one tag a line in the order of §5.4. It has: - **Rooms**, each a room (descriptor, grants, key-log head or none, chains), its policy, `pt0` and its receipts with their pods, and the outcome §9.4 gives them with everything it states: - a live break, an out with no planned duration and its in, a viewer present at its start filled from an HLS asset list, and a viewer who joins it running, filled from a DASH list MPD with what remains of it; - a mid-roll of known duration under a policy that restricts skipping and jumping, snaps both ways and offers a skip control, with a viewer whose pod nothing fits; - a program with chapters, a mid-roll, and a post-roll at its close; - chapters, and an out with a planned duration closed by its in; - one refused room for each reason of §10, each a control of one defect; one for each other rule of §8.5, each refused `receipt_invalid`; a cue by a principal whose grant does not permit `cue`, which the room refuses `kind_not_permitted`; and a decider grant revoked through the key log, and one whose window ends before the break; - a room with a gap; - the request terms (§8.6, §8.7): the live break under a policy of version 3, its first viewer's receipt stating a GPP string with its applicable section and, for its two ads, the chains of the two requests they answered, one from the channel's own account at an SSP through an exchange and one from the channel's own account at the exchange, each checked against the sellers files and the channel's app-ads.txt, and its second viewer's stating no string, a child-directed request and an incomplete chain whose sellers file the verifier does not hold; a room the policy marks as directed to children; one chain two impressions state, of four nodes that the channel's ads.txt authorises only when it is read by each rule of §8.7 (a record of four fields, other letter cases, CR LF and a CR alone, an extension field); and a sellers file that lists its node only when what §8.7 does not read is read by RFC 8259's grammar alone (numbers past binary64's range and of 400 digits, escapes of lone surrogates in members skipped, a member 64 deep); - refused there: four receipts whose GPP string, child-directed flag, a node's seller id or the order of its chains was changed after the decider signed them, each `bad_signature`; a receipt stating version 3, one of version 1 carrying a consent signal, and one of version 2 without its chains (`receipt_malformed`); a receipt for each rule of §8.6 (`consent_invalid`) and for each rule of §8.7 (`supply_chain_invalid`); a receipt of version 1 under a policy of version 3 (`request_terms_absent`) and one with no child-directed flag in a room directed to children (`child_directed_unset`); a sellers file with no sellers, one nested 65 deep, one whose second seller's id escapes a lone surrogate, one with `sellers` twice and one holding `NaN`, and an app-ads.txt that is not UTF-8 (each `supply_file_malformed`); a node its sellers file does not list (`supply_seller_unlisted`); a first node of a complete chain authorised only as a reseller, a node the app-ads.txt does not name, and one it names only in lines a reader passes over, a comment and a line of five fields (each `supply_seller_unauthorised`); and a policy whose `audience` sets a bit above bit 0 (`ad_policy_malformed`). The sellers files and the authorised-sellers files the vectors hold are files of their own, `supply.NAME`, each named in the `supply_files` of the vectors that hold it, read as bytes; every one is a fixture written for its case under `.example` names. A conforming verifier gives every room the outcome, the reason, the place and everything else the corpus states, and every tag file byte for byte. `wai_live_ads_vectors verify` and the test `tests/live_ads.rs` check that every file regenerates byte for byte, that `wai-rs` gives every case its outcome, that every reason of §10 appears, and that §10 lists exactly the reference's codes. `live-conformance/live_ads_verify.py` is the second implementation, written from this text: it shares no code with `wai-rs`; it verifies each room with the live-rooms corpus's second implementation (`live_rooms_verify.py`), every signature with WAI's strict rule (`claim-conformance/_ed25519.py`) and every BLAKE3 hash with `claim-conformance/_blake3.py`; it writes every tag, event and asset list from §5 to §7 and requires their bytes; it reads every receipt's request terms and every sellers file and authorised-sellers file by §8.6 and §8.7 itself; and it mutates copies of the corpus and requires every copy to fail. ## 12. Security and privacy considerations - Every break is a cue its author signed, under a grant that permits `cue`; a relay or an edge can withhold a cue, which is a gap in its author's chain, but cannot add or move a break. The date ranges and events an edge writes are recomputable from the signed records, and a playlist whose interstitials differ from them is one the edge altered. - The decision is a pure function of its inputs, and every input is bound: the pod by its hash, the avail by the break, the slots by the policy, the funds by the receipt, and the asset list served by its digest, whose URIs name each creative by its hash. A decider that serves an ad the decision did not select, or a creative other than the one it bills, signs a receipt that fails §9.3. - The funds a decision states are the decider's statement. What is checked is the bound: no receipt spends past the ceiling, and no receipts together do, so an overspent room fails verification at the receipt that crosses the ceiling. - `quartiles_reached` is the decider's statement of the beacons it received; a receipt binds it and its billing, not the beacons. The impressions a buyer and a seller reconcile are the same signed bytes. A policy of version 2 can require more: that an impression count only when the player's signed presentation claim verifies, reaching the quartile the receipt states under a viewability rule, optionally with a device attestation ([ad-presentation](ad-presentation.md) §9, §13 for what that does and does not prove). - A receipt names a session by 16 bytes the decider assigns. A decider **SHOULD** assign a value with no meaning outside its own records, and **MUST NOT** derive it from a viewer's key or another identifier a third party holds, so that receipts that carry no GPP string (§8.6) do not link a viewer across rooms; within a room, one session's receipts are linkable to one another by design, so that a viewer is filled once for a break (§9.3 step 11). Under a policy that requires presentation claims the session is derived from a key the player makes for that session of that room alone (ad-presentation §3), which is no viewer's key and links nothing across rooms. - The asset-list request carries the viewer's playback-session parameter (Appendix D.4) to the decider, and nowhere else: the manifests every viewer shares carry nothing of any viewer. - The policy is signed by the room authority and the receipts by the decider. A decider whose grant is revoked or out of its window has its receipts refused (§9.2, §9.3). - **The consent signal** is bound into what the decider signs: a receipt whose string, sections or child-directed flag changed after signing fails its signature (§8.5 rule 2), as one whose chains changed does. A verified receipt shows what §8.6 says it shows, what the decider stated it was given and passed on, and no more. The string travels with the asset-list request to the decider, from the decider to those it requests ads from, and in the receipt to whoever holds it. - **A GPP string links what carries it.** A consent tool makes the string for a viewer, not for one request, and it can stay the same, byte for byte, across the viewer's sessions and rooms. A receipt whose `gpp` is not empty can therefore be linked, through the string, to every other receipt that carries the same string, in any room, and to a stitched pod-input record that carries it (stitched-presentation §4.7), by whoever holds them: the same viewer's among them, for as long as the string stays the same. §8.6's rule that a decider and a verifier not use the string to link receipts binds those parties; it does not remove the link. The string is in the signed bytes, so a holder cannot strip it from a receipt and keep the receipt verifiable. What the session item above says of rooms holds of a receipt whose `gpp` is empty. - **A room for children.** A policy that sets **child** binds every decision of the room to a child-directed signal: a receipt that does not carry one is refused (§9.3 step 13). That the decider marked each of its requests so is its statement, as the funds a decision states are. - **The supply chain** and the digests of the files it names are the decider's signed statement. A verifier that holds the files checks every node against them; each file is fetched from the domain it belongs to, by its own text's access method, and a verifier that needs them to be the files published now fetches them itself and compares their digests with the receipt's. ## 13. Ad creative signalling on a MoQ event timeline `draft-riedl-moq-ad-creative-signaling-01` (1 November 2026) carries, on an MSF event timeline, **records** that say what plays inside a placement opportunity: where the splice signalling of a cue says where a break is (live-events §8.1), a record says which creatives fill it. On MoQ every subscriber of a track receives the same Objects, so a record is the same for everyone who holds it, and what differs between viewers is which track they hold (the draft's §8). This section maps the draft onto a room: a record is the image of one break and one fill, signed by the decider, and a verifier holds each record to the break of the schedule and the receipt it describes. A record for a break the room does not have is refused. Nothing here changes §3 to §9. A record adds no claim of its own: the fill it states is the one a receipt already signs, and a record is checked against that receipt. ### 13.1 The track The records ride an MSF event timeline (MSF §8). Its catalog entry declares, as the draft's §3 requires, `packaging` `"eventtimeline"`, `mimeType` `"application/json"`, `eventType` `"urn:svta:advertising-wg:ad-creative-signaling"`, and `depends`, the media track or tracks of its namespace whose breaks its records describe; and `wai_creative_signing` ([moq-streaming-format](moq-streaming-format.md) §4.7), which states the track's scope and the keys its records are signed with. A track's **scope** is one of the three the draft's §8 names. A room's fills are decided viewer by viewer (§7), so its records are published at scope `session`, one track for one session, or at scope `cohort`, one track for the sessions that are given the same fills. A track of scope `shared` carries records every viewer of the programme is given, which a room has only where every session's fill is the same. The records are the same at every scope: scope is which track carries them. Each Object of the track is a JSON array of records (MSF §8.3), the first Object of a Group the independent array of every record still accessible. A reader reads an Object as an array of at most 65,536 bytes whose every element is a record of §13.2. ### 13.2 A record A record is a JSON object (RFC 8259) of one index, `data` and `sig`. A reader treats it as untrusted input (the draft's §12.3) and refuses it, at the first of these that holds: 1. `creative_record_malformed`: it is more than 65,536 bytes; it is not a JSON text whose value is an object, or a member is named twice in one object; it nests more than 16 deep, the record's own object counted as 1; or it holds a number outside §13.3's spellings. 2. `creative_index_invalid`: it has not exactly one of `t` (wallclock, milliseconds since 1970), `m` (media time, milliseconds) and `l` (a MOQT Location, an array of a Group ID and an Object ID); or the one it has is not a whole number from 0 that §13.3 spells (so below 10^15), or for `l` two of them. The index keys are MSF-01's lowercase ones. A member the record holds beside its index, `data` and `sig` is read as part of what is signed, and otherwise passed over. 3. `creative_envelope_invalid`: `data` is not an object; its `version` is not 2; its `type` is not `pod`, `slot`, `trackingEvent` or `verification`; its `payload` is not an array of one or more objects; or its `features`, when present, is not an object whose every flag is `false` but `remoteFields`, the one flag the draft's §4.3 lets a publisher set. 4. `creative_url_not_https`: a string of an array named `urls`, a value of an object named `$remote`, or a string named `resource`, anywhere in the payload, is not an `https` URL. 5. `creative_signature_malformed`: `sig` is present and is not a string of two base64url parts joined by `..`. A record that reads has its index, its envelope's type, whether it **names a resource** (it holds a `urls`, a `$remote` or a `resource`), and, for a `pod` envelope, its **pods**. A pod is carried when it has a `duration` and an array `slots`, its `duration` and each of its `slots`' `start` and `duration` are a whole number of milliseconds (a MediaTime is in seconds, the draft's §4.3), each duration is above zero, its slots are at most 64, in order, none beginning before the last one ends, and the last ends within the pod; a slot's `identifiers`, when it has them, are an array of objects with a string `scheme` and a string `value`. A `pod` envelope with a pod that is not so, and an envelope of another type, read and carry no pod: this section maps nothing onto them. A WAI sink requests no resource a record names. What it presented it states in a signed claim ([ad-presentation](ad-presentation.md) §5), and the records §13.5 writes name no resource. ### 13.3 Numbers and the canonical form The signature covers the record in the form of RFC 8785, whose numbers are written as ECMAScript writes a double. A reader of this section reads only numbers with one spelling it can state without floating point: an optional `-`, an integer part with no leading zero, and at most six digits of a fraction, with no exponent and at most 15 significant digits, counted as the number is written from its first digit that is not `0`, the trailing zeros of a fraction among them. The canonical spelling of such a number is the number without the trailing zeros of its fraction, without the point when no fraction is left, and without the sign when it is zero: `30.030` is `30.03`, `0.0` is `0`. Within these bounds that is the spelling RFC 8785 gives. Any other spelling is refused (`creative_record_malformed`), so that no record is canonicalised two ways. The **canonical form** of a record is RFC 8785's over the record without `sig`: members in the order of their names' UTF-16 code units, strings escaped as its §3.2.2.2 has them, numbers as above, and no white space. ### 13.4 The signature `sig` is a JWS in the Compact Serialization with a detached, unencoded payload (RFC 7515, RFC 7797), as the draft's §4.5 defines: the base64url of the protected header, `..`, and the base64url of the signature. The signing input is the first part as written, a `.`, and the record's canonical form. A verifier is given a JWK Set (RFC 7517), of which it reads the keys whose `kty` is `OKP` and `crv` `Ed25519`, each with its `kid` and its `x` of 32 bytes, and refuses the record, at the first of these that holds: 1. `creative_signature_missing`: the record has no `sig`; 2. `creative_signature_malformed`: the first part is not the base64url of a JSON object, or the header has no string `alg`; 3. `creative_alg_refused`: `alg` is not `EdDSA`. `none` and the MAC algorithms are refused here, before anything else of the header is read: every subscriber holds the verification key, and a key that verifies a MAC also forges one; 4. `creative_signature_malformed`: `b64` is not `false`, `crit` is not the array of `"b64"` alone, `kid` is not a string, or the second part is not the base64url of 64 bytes; 5. `creative_key_unknown`: the set holds no key under `kid`. A key the header carries or points to (`jwk`, `jku`, `x5c`, `x5u`) is never read; 6. `creative_signature_invalid`: the signature does not verify, strictly (SPEC §7 *Signatures*), over the signing input. A record re-serialised on its way, with its members in another order, white space or numbers respelled within §13.3, verifies as it did; one value changed does not. ### 13.5 The record of a fill The record of one session's fill of one break is written by the decider, and is the same bytes whoever writes it: - its index is `t`, the wallclock millisecond the break opens at, ⌊(`t0` + the opening cue's `t`) / 1000⌋ as live-events §8.1 writes a cue's `t`, plus the receipt's `start_offset_ms`: for a session that joined a break running (§7.4), the instant it joined at, inside the break's window (the draft's §5.2); - its `data` is a `pod` envelope of one pod: `{"payload":[{"duration":D,"slots":[…]}],"type":"pod","version":2}`, `D` the sum of the fill's durations in seconds; - each slot is one impression of the receipt, in slot order: `{"duration":d,"identifiers":[{"scheme":"urn:wai:ad:id","value":AD},{"scheme":"urn:wai:ad:creative","value":HASH}],"start":s,"type":"linear"}`, `AD` the impression's `ad_id`, `HASH` its `creative_hash` in 64 lowercase hexadecimal digits, `d` its duration and `s` the sum of the durations before it, in seconds; - its `sig` is by the decider's key, under the protected header `{"alg":"EdDSA","b64":false,"crit":["b64"],"kid":KID}`, `KID` the key's 64 lowercase hexadecimal digits. The record is written in its canonical form with `sig` in its place among the members. A fill of no ad has no record. A record of this form carries no tracking URL, no verification resource and no `$remote`: an impression counts by its receipt and, under a policy that requires it, by the player's claim, and neither is a request. ### 13.6 A record against a room A verifier that has verified a room (§9) and holds records given for one session, with a JWK Set, binds each record in order, and refuses at the first record that fails, with its index and the first of: 1. the record does not read (§13.2), or its signature does not verify (§13.4), with that reason; 2. `creative_index_unbound`: its index is `m` or `l`. A room's breaks are on its wallclock; 3. `creative_pod_not_carried`: its envelope is not one pod, carried (§13.2); 4. `creative_break_unknown`: no break of the schedule has the index in its window, from the millisecond it opens at for its avail; 5. `creative_fill_unknown`: no receipt of the room is for that break and this session; 6. `creative_not_decider`: the key that signed the record is not the receipt's `signer_pubkey`, which §9.3 step 4 held to the decider; 7. `creative_pod_mismatch`: the record's index is not the break's opening millisecond plus the receipt's `start_offset_ms`, or its pod is not the one §13.5 writes for the receipt's impressions: the same duration, and slot for slot the same start, duration and identifiers; 8. `creative_record_duplicate`: an earlier record of the list bound to the same break. A room that §9 did not accept has no records to bind. With no refusal the verifier states, for each record, its break and its receipt. In the other direction, a record indexed by wallclock whose envelope is one pod, carried, describes a **placement opportunity**: the millisecond it opens at and its pod's duration. A producer that takes its breaks from such records signs, for each, a cue of type break (4) at that instant with that `duration_ms` (§4.3), and the record then binds to the break the cue opens. A record states no placement: a post-roll is the producer's to annotate (§4.2). ### 13.7 Reasons | reason | raised at | |---|---| | `creative_record_malformed` | §13.2 rule 1, §13.3 | | `creative_index_invalid` | §13.2 rule 2 | | `creative_envelope_invalid` | §13.2 rule 3 | | `creative_url_not_https` | §13.2 rule 4 | | `creative_signature_missing` | §13.4 step 1 | | `creative_signature_malformed` | §13.2 rule 5, §13.4 steps 2 and 4 | | `creative_alg_refused` | §13.4 step 3 | | `creative_key_unknown` | §13.4 step 5 | | `creative_signature_invalid` | §13.4 step 6 | | `creative_index_unbound` | §13.6 step 2 | | `creative_pod_not_carried` | §13.6 step 3 | | `creative_break_unknown` | §13.6 step 4 | | `creative_fill_unknown` | §13.6 step 5 | | `creative_not_decider` | §13.6 step 6 | | `creative_pod_mismatch` | §13.6 step 7 | | `creative_record_duplicate` | §13.6 step 8 | ### 13.8 Conformance, and what is not done [`live-conformance/ad-signalling/`](../live-conformance/ad-signalling/) holds the corpus of this section and the next, written by `tests/ad_signalling.rs`: `vectors.json`, `catalog.json`, and under `draft-vectors/` the draft's own test vectors, copied from its repository. It has records read on their own, among them every one of the draft's accepting and rejecting vectors and the draft's signed record, verified with the draft's test key, an outside vector for §13.4; records written here for each rule of §13.2 to §13.4; and a room with its receipts, with the records §13.5 writes for three of its sessions and one planted fault for each step of §13.6, among them a record for a break the room does not have. The reference is the `live_ads::creative` module of `wai-rs`; `live-conformance/ad_signalling_verify.py` is the second implementation, written from this text and sharing no code with it. - **SVTA 2053-1 was not read.** This section reads of a payload only what the draft states: a pod's `duration` and `slots`, a slot's `start`, `duration` and `identifiers`, and that timing is in seconds. It does not check a payload against SVTA 2053-1's object model: which members a pod or a slot requires, the tracking event types, or the members it does not name. - **Not done.** The room service of §14.8 publishes these records on each session it stitches; no relay or player of the reference reads them but its tests. The `slot`, `trackingEvent` and `verification` envelopes are read and mapped onto nothing. Stitched-presentation's pod-input records are not mapped. A splice signalling track is live-events §8.1's, and the draft's alignment with it (its §5.2) is met for a session present at a break's start, whose record and whose cue carry one `t`. - **What a record proves.** That the decider signed this pod at this index. It does not prove that anything played: that is the receipt's, and under a policy that requires it the claim's. A record is checked against the receipt, never in its place. - **Scope and linkability.** A track of scope `session` is one viewer's; its name and its records are linkable to that session, as its receipts are (§12). A record carries no viewer's data, and the identifiers it carries are the receipt's. ## 14. Content stitching over MoQ `draft-englishm-moq-content-stitching-00` (2 November 2026) names the pieces of **content stitching**: a stitcher builds the track an audience receives out of Objects of other tracks, so that what audiences share is cached once. It defines no format. This section says what each of its pieces is in a room, and fixes the composed track as a function of signed records, so that a verifier recomputes it. ### 14.1 The draft's terms, in a room | the draft's term | in a room | |---|---| | ingredient track | the programme's track, and one track for each creative, named by the creative hash a receipt states | | signal | a cue (live-events §4.3), signed by a principal whose grant permits it; it is an event of another track, never a property of a media Object | | recipe | the schedule (§4.3) and, for each break, the fill the decider's receipt states for the audience (§7.1) | | stitcher | the party that publishes composed tracks; it terminates each audience's connection, and is an edge (§14.5) | | composed track | the track §14.3 gives | | audience | the sessions given one recipe: one session, or the sessions of a cohort | The two carriages of ads are the draft's two placements of the stitcher. In the **stitched** carriage a stitcher publishes a composed track and the viewer subscribes to it alone. In the **guided** carriage of §5 to §7 there is no stitcher: the player is the End Subscriber of the programme's track and of each creative's track its asset list names, and changes between them itself. The draft's §6 notes that a subscriber of ingredient tracks can receive what its recipe would not have given it; the guided carriage accepts that by design, and its impressions count by receipt and claim, not by what a viewer could have fetched. ### 14.2 The recipe of a session The recipe of one session in a room §9 accepted is one **placement** for each closed break of the schedule, in order: the millisecond of the room clock it begins at, ⌊`t` / 1000⌋ of the opening cue; its duration (§4.3); and its fill, the creative hashes of the impressions of the session's receipt for the break, in slot order. A break the session has no receipt for has an empty fill. A session that joined a break running (§7.4) is placed at the instant it joined, for what remained. ### 14.3 The composed track A track is stated by its **listing**: for each Group, its Group ID and, in Object ID order, the SHA-256 of each Object's payload. The programme's listing also states, for each Group, the millisecond of the programme's timeline it begins at; Group IDs and those instants increase together, and every Group has an Object (`stitch_listing_malformed` otherwise). A creative's listing is its Groups from Group 0, and every one of them has an Object too: a fill that names a creative whose listing has a Group with none is refused `stitch_listing_malformed` at its placement, after `stitch_creative_unknown` and before `stitch_fill_exceeds_span`. A listing no fill names is not read. The composed track of a programme, placements and creatives has the programme's Group IDs, and each of its Groups carries one ingredient Group, whole: Object for Object, the same payloads under the same Object IDs. Placements are taken in order; each has a duration above zero and begins at or after the last one's end (`stitch_placements_unordered`). For each: 1. its **span** is the programme's Groups that begin from its instant until its end. A Group begins exactly at its instant, or it is refused `stitch_break_off_boundary`: a composed track changes ingredient only at a Group boundary (the draft's §4.3). A placement whose fill is empty changes no ingredient, so it is held to no boundary: every Group of its span carries the programme's; 2. when the stitcher held the placement's signal only after it had published the span's first Group (a recipe states, for a placement, the last Group ID the stitcher had published when it held the signal, if any; that ID is then the span's first or later), the placement is **not stitched**: every Group of its span carries the programme's, and nothing already published is revised (the draft's §4.5); 3. when the programme's break candidates are given, the marks of its package ([packaging](packaging.md) §8, its item *Marks*), its instant is one of them, or it is refused `stitch_break_not_offered`. A room's own programme has no package and no marks: its breaks are where its cues place them, and rule 1 alone holds them to a boundary; 4. the Groups of its span carry, from the first, the Groups of each creative of its fill in slot order, each creative's from Group 0. A creative with no listing is `stitch_creative_unknown`, and a fill with more Groups than the span `stitch_fill_exceeds_span`; 5. the Groups of the span after the fill's last carry the programme's own. Every other Group carries the programme's. Two audiences with the same fills have the same composed track under two names, and an audience filled nothing has the programme's Objects under the programme's Group IDs. A cue whose instant falls inside a programme Group (an injector signs its cue at its automation system's splice instant, [cue-injection](cue-injection.md) §7.2, which no rule ties to the programme's Groups) is stitched from the **next boundary**, the first Group that begins at or after the instant. The decider fills the break for each session as for a session that joins it running (§7.4): its start offset is the distance from the instant to that Group, ⌊`t` / 1000⌋ to the Group's millisecond, the fill is decided over the avail less it (§7.1), and the receipt states it (`start_offset_ms`). The recipe then places the session at that Group (§14.2), and rule 1 holds. A session filled nothing keeps its placement at the instant, which rule 1 does not hold to a boundary. The splice instant itself, the cue's `t`, is unchanged: it is what the injector signed and what its SCTE 35 section states (cue-injection §5.2). A composed Object is published by the stitcher (MOQT §2.1), which sets its properties. It carries no property of the ingredient Object it was taken from: an ingredient's `WAI_RECEIPT` ([moq-streaming-format](moq-streaming-format.md) §5) names a group receipt of the ingredient's own chain, which does not cover a Group of the composed track. Its payload is the ingredient's envelope, whole, so everything the envelope states is unchanged: a creative's Objects keep their own decode times, from 0, and their content hashes, which its digest (ad-presentation §4), its records and the stitcher's receipts name. **Its timing.** A composed track has a timeline of its own, the **composed timeline**: it begins at its first Object's decode time, and each Object ends its own duration after the one before it ended, whatever decode time its bytes state. So a Group of a creative is placed where the programme before it ended, and the programme after it where the creative ended. The stitcher stamps every composed Object with the timing a sink presents it by ([co-watch](co-watch.md) §15.2): a CaptureTimestamp, the programme's capture clock (a programme Object's CaptureTimestamp less its end on its own timeline, one constant for the programme's tracks, co-watch §15.7) plus the Object's end on the composed timeline, and the programme's PlayoutDelay, both inside its Immutable Properties, with a HopTime of its own clock beside them. While no creative lasts otherwise than the Groups it fills, every programme Object carries the stamps its source gave it. The stamps are in no receipt: a receipt covers an Object's payload by its content hash ([jwp-receipts](jwp-receipts.md) §2.1), its leaf binds the figure, the class and the origin (jwp-receipts §2.2), and its group receipt the track alias, the Group ID, the root over the leaves in order, the parent and the signer (jwp-receipts §2.3); no Object property is among them. They are the stitcher's word, as a source's are its own, and a sink that presents a composed track by them holds them to what is signed, each composed track's Objects in the order published, refusing an Object at the first of: 1. it carries no CaptureTimestamp or no PlayoutDelay (`stitch_untimed`); 2. its PlayoutDelay is not the one its catalog states (`stitch_delay_other`); 3. its CaptureTimestamp less its end on the composed timeline, in milliseconds, is not the constant the session's first Object states, across its composed video and sound (`stitch_timing_off`). A refused Object is not presented. A sink that missed a Group begins the timeline again at the next Group it holds from its first Object, whose own stamp states the constant again. An HTTP egress places a composed track's Groups by the same rule from the bytes alone and marks each change of ingredient ([live-http-egress](live-http-egress.md) §4.6); it reads no stamp. ### 14.4 A composed track, verified A verifier that holds the programme's listing, the creatives' listings, the recipe and a composed track's listing computes §14.3's composed track, with its refusal, and compares the listing given with it, Group by Group, refusing at the first that differs: - `stitch_group_order`: its Group ID is not the programme's at that place, or it is past the programme's last; - `stitch_wrong_ingredient`: its Objects are those of another ingredient Group than the one §14.3 gives: the programme's where an ad belongs, an ad the fill does not hold, or the fill's ads in another order; - `stitch_transition_inside_group`: each of its Objects is an Object of some ingredient Group, and together they are no one ingredient Group; - `stitch_object_differs`: one of its Objects is no ingredient's. A listing may end before the programme does: a composed track is published as its programme is. With no refusal the verifier states each Group's source and, for each placement, whether it was stitched. ### 14.5 The stitcher is an edge A stitcher that serves one session a composed track of its own names that track by the session key's 64 lowercase hexadecimal digits (ad-presentation §3), in a namespace of its own. The name presents the session key with the subscription, and says nothing of the viewer: the key is made for one session of one room and derived from nothing. The stitcher terminates the connection that subscription arrives on, so it is the **edge** of ad-presentation §14.3, and writes the session's origin (`WSO1`) under the room's traffic policy: `player` the key the Track Name states; the remote address of the connection, its own transport peer's; and the `User-Agent` field value of the request that opened the session (a WebTransport session's extended CONNECT), with no bytes when there was none. An IPv4 peer it sees as an IPv4-mapped IPv6 address it states with family 4, so that one address has one form. It admits no subscription to a Track Name that is not a key's. A player that holds a device key writes the session's device binding (`WDB1`, ad-presentation §14.4), signed by the device key and by the session key, with the device's voucher when it has one. From these two records the invalid-traffic rules decide (ad-presentation §14.6); this section adds no rule to them. ### 14.6 Reasons | reason | raised at | |---|---| | `stitch_listing_malformed` | §14.3 | | `stitch_placements_unordered` | §14.3 | | `stitch_break_off_boundary` | §14.3 rule 1 | | `stitch_break_not_offered` | §14.3 rule 3 | | `stitch_creative_unknown` | §14.3 rule 4 | | `stitch_fill_exceeds_span` | §14.3 rule 4 | | `stitch_group_order` | §14.4 | | `stitch_wrong_ingredient` | §14.4 | | `stitch_transition_inside_group` | §14.4 | | `stitch_object_differs` | §14.4 | | `stitch_untimed` | §14.3, *Its timing*, step 1 | | `stitch_delay_other` | §14.3, *Its timing*, step 2 | | `stitch_timing_off` | §14.3, *Its timing*, step 3 | ### 14.7 Conformance, and what is not done The corpus is §13.8's. It has a programme's listing and four creatives' listings; the recipes of three sessions of its room, among them one that joined a break running and one filled nothing; a signal held in time and one held too late; a break whose cue falls inside a Group, filled from the next boundary and filled nothing; one planted fault for each rule of §14.3 and §14.4; in `timing`, a session's composed video and sound with each Object's decode time, duration and stamps, a creative of the Groups it fills and one that lasts longer, and a planted fault for each step of *Its timing*; and, in `written`, the session origins and device bindings a stitcher's edge and its players write for the twelve sessions of the ad-traffic corpus's room, with what the invalid-traffic rules decide from them. The reference is the `live_ads::stitch` module of `wai-rs`, whose tests also stitch a break at a break candidate of a package's marks and refuse one at a piece start the marks do not offer. - **Not done.** One stitcher is wired: the room service of §14.8, over its room's own source. No relay stitches, and none needs to: the draft states that nothing in it is expected of a relay, and this section asks nothing of one. Composition by individual Objects (the draft's §5.3) has no form here: a composed track changes ingredient at Group boundaries alone. The ingredients' Groups are taken to be prepared alike, as the draft's §4.3 has them: this section does not check a creative's codec configuration against the programme's. A creative whose Groups last otherwise than the Groups it fills moves what follows on the composed timeline by the difference, which the stamps state and a live sink's delay then carries. - **Access to ingredients.** A deployment of the stitched carriage that needs a viewer to receive only its composed track controls access to the ingredient tracks as well (the draft's §6). That control is a relay's and a deployment's, and none is defined here. - **What a verification proves.** That the listing given is the composed track of the recipe: that every Object of it is the programme's or a creative's the receipt names, in the place the schedule and the fill give. It does not prove that the listing is what a relay delivered to the session, or that the session played it. - **Names.** A composed track's name is a session key, which no other session and no other room shares (the draft's §7). A track for one session is linkable to that session, as its receipts are. ### 14.8 A room service as stitcher The reference room service (`joule-moq-room`, its `[ads]`) is a stitcher of its room's own source, the programme. In each room it publishes into, the room's authority signs an ad policy of version 2 that requires a claim for each impression (ad-presentation §8), the decider's grant, and the room's traffic policy (ad-presentation §14.2); the source cues each break as the programme's publisher, a `cue` of type break at the instant a video Group begins, signed before that Group's first Object is published, so the stitcher holds the signal in time (§14.3 rule 2). - **What it reads.** The cues of the source's own events track, and of each injector's that the room's configuration names (`[ads] injectors`, cue-injection §8 step 5), read back over the relay as any reader reads them: each record held to its signature, its principal's chain and its kind, and the schedule (§4.3) taken over those that verify. A record that does not verify, or that another principal than the track's states, places no break, and the room service states why. An injector takes its grant from the room service (the role `injector`): the participant's role with no media to publish and the kinds `meta` and `cue` alone. It publishes events alone, so the clock records its `join` when its events track appears, as it records an audience member's ([live-rooms](live-rooms.md) §7.2 asks a `join` for a grant, not for media), and the room verifier holds its cues as a present principal's. Its splice instants are its automation system's: a break whose cue falls inside a programme Group is stitched from the next boundary, the offset stated (§14.3). - **Where a session finds its tracks.** The namespace `[room, stitcher, video, stitched, D, K]`, `D` the traffic policy's digest and `K` the session key, both in 64 lowercase hexadecimal digits: the composed video named `K` (§14.5), its group receipts on `receipts`, signed by the stitcher over the composed Objects as published, each Group's as the Group ends, and a running receipt for each composed Object as it is sent, a `meta` event of the stitcher's own chain on its events track ([live-http-egress](live-http-egress.md) §3.5), so that an HTTP egress serves each part as its running receipt covers it, its creative records (§13.5) on `ads`, and a catalog of the three on `catalog` (moq-streaming-format §4.7, scope `session`), each composed track with the source's `wai_tempo` (moq-streaming-format §4.4), its Objects stamped as §14.3 has them. `D` lets the edge that terminates the subscription state the session's origin under the policy it is for. Where the fixture creatives carry sound, the session's composed sound is named `K` too, in `[room, stitcher, audio, stitched, D, K]` with its group receipts on `receipts` there, and the catalog names both. - **The sound.** The source cuts its sound where its video's Groups end, one sound Group for each video Group under the same Group ID, and each creative's sound is cut from the sound rung of a wai-pack package where that creative's video Groups end. So the programme's sound listing states each Group at the instant of the video Group of its ID, each creative has a sound listing under its creative hash, and the composed sound is §14.3's composition over them with the same recipe: where the composed video's Group carries a creative's Group, the sound's Group of that ID carries that creative's sound Group, whole; elsewhere the programme's sound. §14.4 verifies the two composed tracks against the one recipe, each with its own listings. A creative's digest names its video objects (ad-presentation §4), what a claim measures; its sound is named by the room's records, not by the digest. - **How a break is decided for a session.** At the break's first Group: where an SCTE 224 document the room service holds imports to blackouts of the room's programme over the break's opening instant (audience-decision §10), the decider signs an audience decision for the region the session stated (audience-decision §4.3, aggregated by the session; unresolved when it stated none), and a break whose verdict is not `show` is filled by nothing; otherwise the fill is §7.1's. The record is written at once; the receipt is sealed once the break has ended and its claims have had their time, each `quartiles_reached` the quartile its own claim reaches (ad-presentation §9.2 step 7). - **The edges.** The relay that terminates a session's subscription to its composed track writes the session's origin (ad-presentation §14.3) under the policy whose digest the namespace states, signed by its edge key (`joule-moq-relay`'s `[ads_edge]`), and keeps it for its operator; the HTTP egress gateway does the same for a viewer's asset-list request (§7), which it passes on to the decider. The gateway also serves one stitched session over HTTP (`joule-moq-http`'s `Gateway::start_stitched`): the session's composed video and sound as `video/0` and `audio/0`, each part beside the stitcher's receipts, each change of ingredient marked (live-http-egress §4.6). A player writes its session's device binding (ad-presentation §14.4) and gives it to the room service (`joule-moq-web`'s `web/ads/traffic.js` writes one in a browser). A player integration in a web page holds that binding, and each presentation claim the page makes, to the session and to what it handed its player before the page sends them ([player-integration](player-integration.md) §3, *A stitched session*). Neither record holds anything the record's table does not. - **What a verifier is given.** The room's records as the room service holds them (its descriptor, grants and chains, each injector's chain as the stitcher read it among them, both policies and the lists, the receipts with their pods, the creatives, each session's creative records and composed listing, the claims and the bindings, and each audience decision with its document) and the session origins each edge kept. Over them it runs §9 with ad-presentation §9, §13.6 for each session's records, §14.4 for each composed track, audience-decision §10.7 for each decision, and ad-presentation §14.7, which states each impression valid, invalid or undecided and the presented spend as the valid, the excluded and the withheld together (`joule_moq_room::ads::audit`). Where the room composes sound, the records carry the programme's sound listing, each creative's and each session's composed sound, and the verifier holds the composed sound to the recipe as it holds the video. - **What is not bound, and not read.** A creative's sound is not bound by its creative digest: the receipts name its picture, and its sound's listing is the room's records' alone, so a room that gave a creative another sound throughout, its listing too, is not found by the receipts. Cues of a principal of the room that the configuration does not name as an injector are not read.