open-standards/wai/extensions/audience-decision.md
Transaction Science 11161d7132
Some checks failed
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 26s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 25s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 17s
wai-audit collector and player integration / does this change touch it (push) Successful in 22s
wai-ledger service / does this change touch it (push) Successful in 13s
wai-quantum for JavaScript / does this change touch it (push) Successful in 28s
wai-quantum studio and desktop / does this change touch it (push) Successful in 26s
wai-app and Create / wai-app · does this change touch it (push) Successful in 4m16s
wai-fleet service / does this change touch it (push) Successful in 4m4s
WAI operation catalogue / does this change touch it (push) Successful in 4m10s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 4m30s
wai-sink-swift native sink package / does this change touch it (push) Successful in 4m21s
WAI Studio / does this change touch it (push) Successful in 4m36s
wai-sink-android native sink library / does this change touch it (push) Successful in 4m40s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 6m36s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 1m47s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m35s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 9s
joule-moq-room room service / joule-moq-room, every target (push) Failing after 5m51s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m33s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 39s
wai-quantum for JavaScript / javascript · linux (push) Successful in 2m40s
wai-audit collector and player integration / collector · linux (push) Successful in 3m38s
wai-ledger service / service · linux (push) Successful in 3m37s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m10s
wai-quantum studio and desktop / studio · browser (push) Successful in 4m34s
wai-fleet service / service · linux (push) Successful in 2m19s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 8m21s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m41s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 14m48s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 6m7s
WAI Studio / Studio in WebKit · linux (push) Successful in 18m41s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m39s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 56s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m7s
wai-ledger service / service · macos (push) Successful in 1m9s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m49s
wai-fleet service / service · macos (push) Successful in 38s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m2s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m45s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 7m1s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m29s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 16m4s
The SCTE 224 and ad-signalling corpora hold a case for every check of their second implementations
The two corpora landed with a list each of the checks no case exercised: 39 of the 71 of
audience_scte224_verify.py, shown there by documents the script made itself, and 14 of the
61 of ad_signalling_verify.py. Both lists are empty now, and are gone from the scripts: a
second implementation with any one check switched off no longer passes its corpus, and a
check with no case fails the run.

- live-conformance/audience-scte224/: 59 more documents and one more predicate, written by
  tests/audience_scte224.rs. Each limit has a document at it, which imports, and one past
  it (1,048,576 bytes, 64 elements deep, audiences 8 deep, 8,192 regions, 4,096 media
  points, 256 blackouts, six digits of a fraction, the first instant of 1970, the last
  instant a decision carries); a document near a limit is stated by its pieces
  (document_parts), so the file holds no megabyte of spaces. The rest are the single rules
  the first cases stated only together with another.
- live-conformance/ad-signalling/: 17 more records and two more stitching cases, written by
  live_ads::signalling_corpus, and a key of another curve in the key set.

Writing the cases found five places where the reference and the second implementation
disagreed, each settled by the text: an attribute on a Region, an action:Content or an
Alternate is scte224_not_carried (the reference said malformed and action_unmappable); a
Remove that holds a policy's definition is scte224_malformed (the reference imported it;
audience-decision §10.5 now states the reason); audiences nested past 8 are
scte224_not_carried (the second implementation said malformed); a number's 15 significant
digits are counted as written (live-ads §13.3 now says so; the reference read
123456789012345.0); a pod with no slots member is not carried (§13.2 now says so; the
second implementation carried it).

Left as found, not in either list: a creative's listing with a Group of no Object is
refused by the second implementation (stitch_listing_malformed) and composed by the
reference; the corpus states its creatives once for every case, so no case reaches it.

Regenerated on the Framework (x86_64) and checked byte for byte on Thor (aarch64).

Co-Authored-By: Claude <[email protected]>
2026-11-11 00:17:27 -05:00

619 lines
39 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Audience Decisions and Territories
> Status: Draft. Registers two records, each a route-record document signed by one Ed25519 key.
> `wai.video.audience`: a decider's signed **audience decision** (`wai-audience-decision`, §4):
> whether the sessions in one region may be shown a programme over a span, under the blackouts in
> force for it. Each blackout is named by the id a session class carries (`blackouts`,
> [http-delivery](http-delivery.md) Appendix H), and states the regions it covers in
> [rung-price](rung-price.md) §2's region codes and the programme shown in its place, if any. A
> session's location is aggregated in the session, before it leaves the session, to the one listed
> region that holds it, to *outside*, or to *unresolved* (§4.3). A decision carries that and
> nothing else of where a session is, and is refused when it carries more
> (`audience_location_not_aggregated`) or a region its blackouts do not list
> (`audience_region_not_in_set`). Which blackouts apply, the verdict and the programme shown in
> place follow from the blackouts and the region, and a verifier recomputes them; a session class
> of the programme carries the blackouts that apply as its own (§4.7). `wai.delivery.territory`: a
> publisher's signed **territory statement** (`wai-territory`, §5): the regions its content may be
> shown in, everywhere, only in some, or everywhere but some. A publisher's latest statement stands
> alone, and one that lapses revives none. A verifier reports, beside the placement of a server key
> ([cache-authority](cache-authority.md) §4.4), whether the receivers' region is within the
> publisher's territory, and the placement is the same with the statement as without it: a
> statement names no server, origin or role, and one that does is refused
> (`territory_scopes_serving`). No new transport and no new cryptography. The reference
> implementation is the `audience_decision` module of `wai-rs` (feature `audience_decision`); the
> corpus is [`live-conformance/audience/`](../live-conformance/audience/), and its second
> implementation is `live-conformance/audience_decision_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> A `Media` document of SCTE 224 whose policies are applied by time, over audiences stated in
> region codes and with the blackout action, **imports** to the blackouts in force over each span
> of its programme, and spans are written back as such a document (§10). What a document states
> that those members do not carry exactly is refused with its own reason, never passed over. A
> decision or a sink predicate is then held to the document it was made from.
>
> Texts read for this revision: WAI's own; and, for §10, the XML schema of ANSI/SCTE 224 2021
> (`scte_224_20211011.xsd`, target namespace `http://www.scte.org/schemas/224`) and the practice
> *Blackouts and Airing Substitution* (BP-EPG-BLK v1.0 draft, 1 October 2026), each as §10.1
> credits it. The text of ANSI/SCTE 224 2021 itself could not be read: its publisher's site
> refused the request. §10 therefore binds only what the schema and that practice fix, and §10.1
> lists what it leaves unread. ISO 3166 was not read: this text binds only the structure of a code
> (rung-price §2).
## 1. Scope
In scope: the audience decision, its form, digest and signature, how a session's location is
aggregated before it leaves the session, what follows from a decision's blackouts and region, and a
decision beside a session class (§4); the territory statement, its form, digest and signature, the
statement that stands at an instant, a region against a territory, and the report beside a
placement (§5); and a bearer with no return path (§6).
Also in scope: the `Media` documents of SCTE 224 that import to a programme's blackouts, the
document written from them, and a decision or a predicate held to a document (§10).
Out of scope: a transport; how a session learns where it is; the parts of SCTE 224 §10.1 lists as
unread, and how a decider obtains a document; what a rights holder and a decider agree, which their own contract states; the
session class and its receipt ([http-delivery](http-delivery.md) §8.6 and Appendix H), which this
extension reads and does not change; who may serve what a node holds, which
[cache-authority](cache-authority.md) §4.3 gives and this extension leaves as it is; the form in
which a predicate travels on a bearer with no return path, which [sink-predicate](sink-predicate.md)
registers; and payment.
Relationship to the core spec: `wai.video.audience` and `wai.delivery.territory` are **records**
([SPEC](../SPEC.md) §5): signed records a WAI tool emits and verifies. They name no payload format
and are not dispatchable. They travel beside the envelopes and playlists they are about and are
checked where those are used: a decision where a session is served, a territory statement where a
placement is reported.
## 2. Terms
- **Document**, **canonical form**, **time**, **token**, **key** and **signature**: as
[route-record](route-record.md) §2. A time is in microseconds.
- **Region code**, its **country**, and **holds**: rung-price §2, WAI's one definition of them.
- **Decider**: an Ed25519 key that signs audience decisions: the service that decides, for a
programme, what the sessions in a region are shown.
- **Programme**: what a decision is about, named by the token a session class names it by, its
`content_id` (http-delivery Appendix H).
- **Blackout**: a restriction on a programme over a span: the regions in which it is not shown,
and the programme shown in its place there, if any. Its **id** is a token: the id a session
class lists among its `blackouts`.
- **Listed codes**: of a set of blackouts, every region code any of them lists.
- **Session**: one playback of a programme by a client. A session's **location** is its own
state: the region code it holds for where it is, at the finest it holds (a subdivision code or
a country code), or none. How a session comes to hold it is out of scope.
- **Publisher**: the key that signs a group (cache-authority §2), and signs its territory
statements.
- **Placement**: cache-authority §4.4's placing of a server key, written as that section writes it.
## 3. Why these two records
1. **A blackout is contracted per region, and decided per region.** A rights holder restricts a
programme in regions, and a session is shown it or not by where it is. A decision states the
region it decided for, the blackouts in force and what follows, signed, so a party that holds it
checks what follows without asking anyone.
2. **A decision needs a region, not a location.** Whether a blackout applies is fixed by which of
its listed codes holds the session. So a session aggregates its own location to that code
before anything leaves it, and a decision carries the code and no more (§4.3). Every session
that aggregates to one region gets one decision, as every viewer of a session class gets one
playlist (http-delivery §8.6).
3. **One region scheme.** Blackouts, a decision's region and a territory are written in rung-price
§2's codes and held by its *holds*, which a lineup's countries, a price list's footprint and a
cache's population already use.
4. **Where content may be shown is not who may serve it.** A node's operator places a node
(cache-authority §4.3), and nothing a publisher signs changes that. A publisher's territory is
reported beside a placement, as a key log's confirmation of a claim is reported beside its
verdict (cache-authority §4.4), and never in it.
## 4. The audience decision
### 4.1 Members
A decision is a document of exactly these members:
| member | form |
|---|---|
| `kind` | `"wai-audience-decision"` |
| `version` | `1` |
| `decider` | a key: 64 lowercase hexadecimal digits |
| `programme` | a token: the programme |
| `start` | a time: from when the decision holds |
| `end` | a time: from when it holds nothing |
| `blackouts` | 0 to 256 blackouts (§4.2), in strictly ascending order of their ids' bytes: every blackout in force for the programme over the span |
| `evidence` | an object of `region` and `resolved` (§4.3) |
| `applied` | 0 to 256 tokens, strictly ascending by their bytes: the ids of the blackouts that apply (§4.6) |
| `verdict` | `"show"`, `"withhold"` or `"alternate"` (§4.6) |
| `alternate` | a token, the programme shown in place, when `verdict` is `"alternate"`; `null` otherwise |
| `sig` | a signature (§4.4) |
A decision holds for one span. A decider given blackouts that begin or end inside a programme
signs one decision for each span between those instants.
### 4.2 A blackout
| member | form |
|---|---|
| `id` | a token: the blackout's id |
| `regions` | 1 to 8,192 region codes, strictly ascending by their bytes, with no subdivision code whose country it also lists: where the programme is not shown |
| `alternate` | a token, the programme shown in its place there, or `null` for none |
The regions of one blackout have one encoding, as a footprint does (rung-price §4.5 rule 3). Two
blackouts may list the same codes, or one a country and another a subdivision of it.
### 4.3 The evidence, aggregated in the session
A session's location never leaves the session. What leaves it is the location's **aggregation**
against the listed codes of the blackouts in force, written as `evidence`:
| member | form |
|---|---|
| `region` | a region code, or `null` |
| `resolved` | `true` or `false`; `false` only with a `region` of `null` |
The aggregation of a location *s* against listed codes *L* is the first of these that holds:
1. *s* is none: **unresolved**, `{"region": null, "resolved": false}`;
2. *L* lists *s*: `{"region": s, "resolved": true}`;
3. *s* is a subdivision code and *L* lists its country: `{"region": the country, "resolved": true}`;
4. *s* is a country code and *L* lists a subdivision of it: **unresolved**, since *s* does not say
which of its subdivisions holds the session;
5. otherwise: **outside**, `{"region": null, "resolved": true}`.
So the region, when there is one, is the most specific listed code that holds the location, and a
blackout's regions hold that region exactly when they hold the location (rung-price §2 *Holds*):
the aggregation decides what the location would decide, and says no more of it. A session that
holds a finer location keeps it. One that holds none, or too coarse a one, is unresolved, and every
blackout in force applies to it (§4.6).
The evidence holds nothing else: no coordinates, postal code, address, network address or
session's identifier, and no region finer than the one the aggregation gives. A decision whose
evidence holds any other member, or a region its listed codes hold without listing it (one finer
than the aggregation gives), is refused `audience_location_not_aggregated`; one whose region its
listed codes neither list nor hold is refused `audience_region_not_in_set` (§4.5).
### 4.4 Digest and signature
The **digest** of a decision is SHA-256 over the canonical form of the decision without `sig`,
written as 64 lowercase hexadecimal digits. The **signature** is Ed25519 by `decider` over
`wai:audience-decision` (ASCII) and 0x01, followed by that same canonical form: route-record §5.3's
construction under its own domain, as rung-price §4.4 is. It is verified strictly, as SPEC §7
*Signatures* verifies every signature: a key that is not the canonical encoding of a point of large
order fails, as does a signature whose scalar is not below the group order.
A signature over any other bytes fails: over the canonical form without the domain, under another
record's domain, or over the text as it was written when that is not its canonical form. The
canonical form is what is signed, so a decision reads the same whatever the order of its members.
### 4.5 Reading and rules
A reader is given a text. It refuses it `audience_decision_malformed` when the text holds more
than 1,048,576 bytes, is not a document, or breaks one of rules 1 to 4. A decision that reads is
then refused for rule 5, 6 or 7, with that rule's own reason, and then for its signature
(`audience_decision_signature_invalid`, §4.4). The rules, in order:
1. Its members are exactly §4.1's, and each blackout's exactly §4.2's, each of its form;
`evidence` is an object that holds `region` and `resolved`, each of its form. `version` is 1.
2. `start` is below `end`.
3. The blackouts are in strictly ascending order of their ids, each blackout's `regions` are as
§4.2 states, and `applied` is strictly ascending.
4. `resolved` is `false` only with a `region` of `null`, and `alternate` is a token exactly when
`verdict` is `"alternate"`.
5. `evidence` holds no member but `region` and `resolved`, and the listed codes do not hold a
`region` they do not list (`audience_location_not_aggregated`).
6. A `region` that is not `null` is a listed code (`audience_region_not_in_set`).
7. `applied`, `verdict` and `alternate` are what §4.6 gives (`audience_decision_inconsistent`).
Rule 5 comes before rule 6: a region the listed codes hold without listing it is a finer location
passed on, and a region they neither list nor hold is not a region of the blackouts at all.
### 4.6 What follows from the blackouts and the region
- **Applied**: with `resolved` and a `region`, the ids of the blackouts whose `regions` hold it;
outside (`resolved` and no `region`), none; unresolved, the id of every blackout.
- **Verdict**: `"show"`, with no `alternate`, when none applies; `"alternate"`, with that
programme, when every blackout that applies names one and the same `alternate`; `"withhold"`,
with no `alternate`, otherwise.
A decision is the same for every session whose location aggregates to its region: a decider
**MAY** sign it once and give it to each of them.
### 4.7 A decision and a session class
A session class (http-delivery Appendix H, `class`) of a programme carries the blackouts that apply
to its sessions as its `blackouts`. Given a decision's text and a class, a verifier, in order:
1. reads the decision (§4.5), and refuses with its reason when it does not read;
2. reads the class: an object of exactly `content_id`, `entitlement`, `geo`, `ad_pod_id` (each a
string), `blackouts` (an array of strings) and `window` (an integer from 0 to 2⁵³ − 1), and
refuses it `audience_class_malformed` otherwise;
3. requires the class's `content_id` to be the decision's `programme`;
4. requires the class's `blackouts`, distinct and in ascending order of their bytes as its hash
orders them (http-delivery Appendix H), to be `applied`;
5. requires the class's `geo` to be the region's country when the decision has a `region`, and not
a listed code when the decision is outside, since a listed country would hold every session in
it. An unresolved decision requires nothing of `geo`.
A class that fails step 3, 4 or 5 is refused `audience_class_mismatch`, naming the first member that
differs: `content_id`, `blackouts` or `geo`. One that passes is **consistent** with the decision,
and the verifier states its `session_class_hash`, as Appendix H computes it, by which a playlist
receipt of that class names it.
## 5. The territory statement
### 5.1 Members
A territory statement is a document of exactly these members:
| member | form |
|---|---|
| `kind` | `"wai-territory"` |
| `version` | `1` |
| `publisher` | a key: the publisher |
| `issued` | a time: from when it states the territory |
| `expires` | a time: from when it states nothing |
| `show` | `"everywhere"`, `"only"` or `"except"`: shown everywhere, only in `regions`, or everywhere but in `regions` |
| `regions` | 0 to 8,192 region codes, strictly ascending by their bytes, with no subdivision code whose country it also lists; none exactly when `show` is `"everywhere"` |
| `sig` | a signature (§5.2) |
`show` and `regions` are a lineup's `geo` and `countries` ([channel-lineup](channel-lineup.md)
§6.2), for all of a publisher's content and in region codes.
### 5.2 Digest and signature
The digest is SHA-256 over the canonical form of the statement without `sig`. The signature is
Ed25519 by `publisher` over `wai:territory` (ASCII) and 0x01, followed by that same canonical form,
verified strictly as a decision's is (§4.4); a signature over any other bytes fails.
### 5.3 Reading and rules
**A statement names no server.** Who may serve what a node holds is given by the node's operator's
origin (cache-authority §4.3), and a territory statement changes none of it. A statement that holds
a member by which it would name who serves — `servers` or `nodes` (keys), `origins` (key logs),
`roles` or `may_sign` (authorities, as a key log writes them) — is refused
`territory_scopes_serving`, before any other rule of form: one reader would take such a member as
scoping the servers it places, and another would pass over it.
A reader is given a text. It refuses it `territory_malformed` when the text holds more than
1,048,576 bytes or is not a document; `territory_scopes_serving` as above; `territory_malformed`
when it breaks one of rules 1 to 3; and then `territory_signature_invalid` for its signature
(§5.2). The rules, in order:
1. Its members are exactly §5.1's, each of its form. `version` is 1.
2. `issued` is below `expires`.
3. `regions` are as §5.1 states, and empty exactly when `show` is `"everywhere"`.
### 5.4 The standing statement
Given texts, a publisher and an instant *t* (a time), the publisher's statements are the texts that
read (§5.3) and whose `publisher` is that key; a text that does not read is passed over. Of the
publisher's statements whose `issued` is at or before *t*, the first of these that holds:
1. there are none: no statement stands (`territory_not_stated`);
2. two that differ (by their digests) share the latest `issued`: none stands
(`territory_contradicted`), and no earlier one revives;
3. the latest's `expires` is at or before *t*: none stands (`territory_lapsed`), and no earlier one
revives;
4. otherwise the latest **stands**.
Two texts of one body under two signatures are one statement, with one digest. A publisher's
latest statement replaces every earlier one, as a seller's latest price list does (rung-price §5).
### 5.5 A region against a territory
Of a standing statement and the region a relying party states for the receivers (a region code, or
none when it states none), the **territory** is:
- `within` when `show` is `"everywhere"`;
- otherwise `undetermined` when no region is stated;
- for `"only"`: `within` when `regions` hold the region; `undetermined` when the region is a
country of which `regions` list a subdivision; `outside` otherwise;
- for `"except"`: `outside` when `regions` hold the region; `undetermined` when the region is a
country of which `regions` list a subdivision; `within` otherwise.
It is written `within statement=<digest>`, `outside statement=<digest>` or
`undetermined statement=<digest>`, and with no standing statement `unstated reason=<code>`, the
code of §5.4. An unstated territory is reported as unstated, never as within.
### 5.6 Beside a placement
Given a server key, an instant `at_ms` and the heads its relying party names, as cache-authority
§4.4 is, and a publisher, the receivers' region and territory statements, a verifier reports two
lines: the **placement**, computed by cache-authority §4.4 from the key, the instant and the heads
alone, and the **territory** (§5.5) of the publisher's statement standing at `at_ms` × 1000.
The placement is the same whatever statements are given and whatever territory is reported beside
it: an `outside` territory refuses no key, and a `within` territory confirms none. cache-authority
§4.3 and §4.4 are unchanged. What a relying party does with a placement beside an `outside`
territory is its own: a payer that pays only for service within a publisher's territory says so in
its own terms.
## 6. No return path
On a bearer with no return path ([unidirectional-delivery](unidirectional-delivery.md) §2), every
sink receives every byte and no decision is made away from it. The blackouts in force are then a
**sink-evaluated predicate** of the programme and the sink's location: the sink aggregates its
location against their listed codes (§4.3) and presents what §4.6 gives, with the programme shown
in place when it holds it. It causes no request in doing so, and its location and its verdict leave
the device nowhere (unidirectional-delivery §2). Two sinks holding the same blackouts and the same
location reach the same verdict. There is then no decision to sign: a decision of §4 records one
made away from the sink. The blackouts travel to the sink in a predicate of
[sink-predicate](sink-predicate.md), whose blackouts are §4.2's and whose verdict is §4.6's at the
sink's aggregated location (sink-predicate §4.2).
## 7. Reasons
| reason | raised at |
|---|---|
| `audience_decision_malformed` | §4.5 rules 1 to 4, §4.7 step 1 |
| `audience_location_not_aggregated` | §4.3, §4.5 rule 5 |
| `audience_region_not_in_set` | §4.3, §4.5 rule 6 |
| `audience_decision_inconsistent` | §4.5 rule 7 |
| `audience_decision_signature_invalid` | §4.4, §4.5 |
| `audience_class_malformed` | §4.7 step 2 |
| `audience_class_mismatch` | §4.7 steps 3 to 5 |
| `territory_malformed` | §5.3 |
| `territory_scopes_serving` | §5.3 |
| `territory_signature_invalid` | §5.2, §5.3 |
| `territory_not_stated` | §5.4 step 1 |
| `territory_contradicted` | §5.4 step 2 |
| `territory_lapsed` | §5.4 step 3 |
## 8. Conformance
[`live-conformance/audience/`](../live-conformance/audience/) holds the corpus, written from fixed
seeds by `wai_audience_decision_vectors gen`: `vectors.json`, which holds every key and each case's
inputs and outcome. Every key is Ed25519 from a seed of one byte repeated 32 times and attests
nothing, and every region is an ISO 3166-1 user-assigned code or a subdivision of one, which names
no place. The cases:
- **aggregations** (§4.3): locations aggregated against the listed codes of blackouts: a listed
subdivision, a subdivision of a listed country, a listed country, a country of which only
subdivisions are listed, a subdivision beside a listed one, one outside, and none, with and
without blackouts;
- **decisions** (§4.4 to §4.6): one for each verdict, outside, unresolved, under no blackouts, under
two alternates that differ, and one written in another order than its canonical form; and a
control of each rule of §4.5: a text that is not a document, each member out of form, each order,
a coordinate, a postal code and a session's identifier in the evidence, a region finer than the
aggregation gives, a region the blackouts neither list nor hold, each of `applied`, `verdict` and
`alternate` other than §4.6 gives, and signatures over the wrong bytes (without the domain, under
the territory domain, over the text as written, over a body edited after signing), by another
key, flipped and under a key of small order;
- **classes** (§4.7): classes consistent with a decision of each verdict, with blackouts written out
of order and twice, and a control of each step;
- **statements** (§5.2, §5.3): one of each `show`, one written out of canonical order, a control of
each rule, each of the five members that would name who serves, and signatures over the wrong
bytes, by another key, flipped and under a key of small order;
- **territories** (§5.4, §5.5): each territory of each `show`, a later statement replacing an
earlier one in either order, one body under two signatures, a text that does not read passed
over, statements at their bounds, one lapsed, two contradicting, and none;
- **placements** (§5.6): each of cache-authority §4.4's placements beside a territory, a confirmed
key beside an `outside` territory, and a statement that names a server passed over.
A conforming implementation gives every case its outcome. `wai_audience_decision_vectors verify`
and the test `tests/audience_decision.rs` check that the file regenerates byte for byte, that
`wai-rs` gives every case its outcome, and that §7 lists exactly the codes the reference reports.
`live-conformance/audience_decision_verify.py` is the second implementation, written from this
text: it shares no code with `wai-rs`; it reads documents and their canonical form with the route
record corpus's second implementation, region codes with the rung price corpus's, placements with
the cache authority corpus's, Ed25519 with WAI's strict rule (`claim-conformance/_ed25519.py`) and
BLAKE3 with `claim-conformance/_blake3.py`. It requires every case's outcome and every reason of
§7; that a decision aggregated from a location decides what the location decides; that no
placement changes when its statements are removed; and that no outcome depends on the order of a
document's members or of the statements given. It then mutates copies of the corpus and requires
every copy to fail, and drops each of its own checks in turn and requires the corpus to catch the
weakened verifier.
## 9. Security and privacy considerations
- **Location stays in the session.** A session's location is read in the session and aggregated
there (§4.3). A decision holds a region the blackouts list, *outside* or *unresolved*, and a
reader refuses one that holds more. Every session that aggregates to one region is given one
decision, so a decision names no session.
- **What a decision proves.** That the decider stated these blackouts for the programme over the
span, and what follows from them for the region. It does not prove that a rights holder issued
those blackouts: a rights holder checks the set against the restrictions it issued. Nor does it
prove where a session was: the region is the session's word, aggregated from its own state.
- **Unresolved is not outside.** A session whose location does not decide the blackouts is
unresolved, and every blackout in force applies to it; it is never read as outside.
- **What a territory statement proves.** That the publisher stated where its content may be shown
from `issued` to `expires`. It confirms no server and refuses none, and an unstated territory is
reported as unstated, never as within.
- **Bounded reading.** Texts hold at most 1,048,576 bytes, documents nest at most 64 deep, and every
integer is checked against its bound before any arithmetic.
## 10. SCTE 224 documents
A rights holder that states its blackouts in SCTE 224 (Event Scheduling and Notification
Interface) sends `Media` documents: a service, the points on it at which policies are applied and
removed, and for each policy the audiences it applies to and what is done for them. This section
says which of those documents state a programme's blackouts exactly, what each imports to, and how
the same blackouts are written back. An import is a pure function of the document's bytes.
### 10.1 What was read, and what was not
- **The schema.** `scte_224_20211011.xsd`, the XML schema of ANSI/SCTE 224 2021, as the schema
documentation of the EPG Linear Schedule Data specification reproduces it (Motion Picture
Laboratories, `EPG-v1.0-DRAFT-20240418`, at movielabs.com/md/epg/). It fixes the elements
`Media`, `MediaPoint`, `Apply`, `Remove`, `Policy`, `ViewingPolicy`, `Audience` and
`MatchSignal`, their attributes and their order, the values of `match` (`ALL`, the default,
`ANY` and `NONE`), and that an audience's properties and a viewing policy's actions are
elements of another namespace.
- **The practice.** *Blackouts and Airing Substitution, Encoding Best Practice* (BP-EPG-BLK,
version 1.0 draft, 1 October 2026), © Motion Picture Laboratories, Inc., licensed under CC BY
4.0. It fixes: the namespaces `urn:scte:224:audience` and `urn:scte:224:action`; the property
`audience:Zip`; the action `action:Content` and its value `urn:scte:224:action:blackout`; a
media point matched by time through `matchTime`; `Apply`'s `duration` as how long an applied
policy stays in force unless it is removed earlier; `Remove` naming, by reference, a policy
applied before; and `expectedDuration` as describing the event and not the policy's lifetime.
- **Not read.** The clauses of ANSI/SCTE 224 2021. So this section does not bind: the standard's
list of audience properties beyond `audience:Zip`, and whether it defines one for a country or
a subdivision; what `ALL` and `NONE` mean for an audience of several members; the actions other
than the blackout; `MatchSignal` and how a signal matches; `matchOffset`, `order`, `reusable`,
`source`, `priority`, and the `effective` and `expires` window; a policy applied while it is in
force; `Audit` and `Results`; and the interface the documents are sent over. Each is refused
where a document uses it (§10.3 to §10.5), and none is given a meaning here.
### 10.2 The document
A document is at most 1,048,576 bytes of UTF-8 with no byte order mark, read over the XML subset
of [stitched-presentation](stitched-presentation.md) §3.5 with namespaces added: an attribute
`xmlns` or `xmlns:`*prefix* declares a namespace for its element and that element's content; an
element's name is its namespace and its local name, an unprefixed element taking the default
namespace; an attribute's is its prefix's namespace and its local name, an unprefixed attribute
having no namespace; the prefix `xml` is bound, and any other prefix must be declared. A document
outside the subset, one with a prefix that is not declared, or an element with two attributes of
one name is refused `scte224_malformed`.
In this section an element named without a prefix is in the namespace
`http://www.scte.org/schemas/224`, `xlink:href` is the attribute `href` of
`http://www.w3.org/1999/xlink`, and `audience:` and `action:` are the practice's two namespaces.
WAI defines two elements of its own, in the place the schema gives to properties of another
namespace:
| element | namespace | content |
|---|---|---|
| `Region` | `tag:transaction.science,2026:wai/audience` | an audience property: one region code (rung-price §2) |
| `Alternate` | `tag:transaction.science,2026:wai/action` | an action property: a token, the programme shown in place |
The root is `Media`, and its `id` is the **programme**: a token (`scte224_id_not_token`
otherwise; `scte224_malformed` when the root is another element or has no `id`).
Wherever an element may stand, these state no policy and are passed over with their content: the
elements `AltID`, `Metadata` and `Ext`, the attributes `description` and `lastUpdated`, namespace
declarations, a media point's `id` and its `expectedDuration`. Any other attribute of an element
this section reads, beside the `id`, `xlink:href`, `match`, `matchTime` and `duration` it reads
where it says so, is refused `scte224_not_carried`: its meaning is one §10.1 lists as unread.
A `Policy`, a `ViewingPolicy` or an `Audience` with an `id` and no `xlink:href` **defines** that
id, wherever it stands in the document; an id defined twice by elements of one name is
`scte224_malformed`. One with an `xlink:href` stands for the element of its name that defines
that value, compared byte for byte: it has no `id`, no content and no other attribute
(`scte224_malformed`), and a value the document does not define is refused
`scte224_reference_unresolved`. Nothing is fetched.
### 10.3 Times
A media point's `matchTime` is an `xs:dateTime` that names an instant: a date from 1970, `T`, a
time of day with at most six digits of a fraction, and a zone, `Z` or an offset. A `duration` is
an `xs:duration` of days, hours, minutes and seconds alone, the seconds with at most six digits
of a fraction, above zero. Each is read to the microsecond, exactly. A time with no zone, a
fraction finer than a microsecond, a duration with years or months, and a media point with no
`matchTime` are refused `scte224_not_carried`: a document that states them states no instant a
decision's `start` and `end` carry.
### 10.4 Audiences, viewing policies and policies
An **audience** names a set of region codes:
- its `match` is one of the schema's three values (`scte224_malformed` otherwise), and is `ANY`,
or it has exactly one member (under `ANY` or under the default `ALL`,
which for one member are one audience). An audience of `NONE`, one of `ALL` with several
members and one with no member are refused `scte224_audience_unmappable`: a list of regions
states the places a programme is not shown, and cannot state every place but some, or a
conjunction §10.1 leaves unread;
- each member is a `Region`, whose code is in the set (`scte224_malformed` when its text is not
a region code), or an `Audience`, whose set is added, nested at most 8 deep, the viewing policy's own audience counted as 1
(`scte224_not_carried` deeper);
- any other property is refused `scte224_audience_unmappable`: `audience:Zip` among them, since a
postal code is not a region code and no region code holds it. An audience is carried whole or
not at all;
- an element of the schema's own namespace other than `Audience` is `scte224_malformed`.
A **viewing policy** states one blackout (§4.2). Its `id` is the blackout's id, a token
(`scte224_id_not_token` when it is not, or when there is none). It holds exactly one `Audience`
(`scte224_malformed` otherwise), whose set is the blackout's `regions`, written in the one
encoding §4.2 requires: ascending, and without a subdivision whose country the set also holds,
which changes no place the set names. It holds exactly one `action:Content` whose text is
`urn:scte:224:action:blackout`, and at most one `Alternate`, the blackout's `alternate`
(`scte224_id_not_token` when it is not a token). Any other action, another value of
`action:Content`, a second `action:Content` or `Alternate`, or no blackout action is refused
`scte224_action_unmappable`; an element of the schema's own namespace other than the `Audience` is
`scte224_malformed`.
A **policy** states the blackouts of its viewing policies, and may hold none; any other child of
it is `scte224_malformed`, as is an `Apply` or a `Remove` that does not hold exactly one `Policy`.
### 10.5 What a document imports to
Each child of `Media` that is not passed over is a `MediaPoint` (`scte224_malformed` otherwise).
The media points take effect in the order of their instants, those of one instant in document
order, and in each its `Remove` and `Apply` children in document order:
- `Apply` holds one `Policy` and puts it **in force** from the media point's instant, for its
`duration` when it has one. A policy applied while it is in force is refused
`scte224_not_carried`.
- `Remove` holds one `Policy`, by reference (`scte224_malformed` when it holds a policy's
definition), and ends it at the media point's instant when it is in force. A policy that no media point at or before that instant applied is
`scte224_malformed`; one whose duration has already ended is left as it is.
- Any other child, `MatchSignal` among them, is refused `scte224_not_carried`.
A policy still in force with no end once every media point has taken effect is refused
`scte224_span_open`: a decision holds for a span that ends (§4.1).
The **instants** of a document are the instants at which a policy comes into force or ends. Each
two consecutive instants bound a **span**, and the span's blackouts are those of every policy in
force throughout it, one for each id, in ascending order of their ids; a span between two
policies is a span under no blackout. A span of more than 256 blackouts, a document of more than
4,096 media points and a set of more than 8,192 regions are refused `scte224_not_carried`.
A document **imports to** its programme and its spans in order. Each span is the `programme`,
`start`, `end` and `blackouts` of a decision (§4.1), and of a sink predicate
([sink-predicate](sink-predicate.md) §3.1), over that span.
### 10.6 Writing a document
Spans of one programme, in order, none beginning before the last one ends, each of §4.2's form
and each id stating one blackout throughout, are written as one document: the XML declaration, a
`Media` whose `id` is the programme, and for each span a `MediaPoint` whose `matchTime` is the
span's start in UTC and whose one `Apply` has the span's length as its `duration` and a `Policy`
with no id, which holds no viewing policy for a span under no blackout. A blackout is written in full, a `ViewingPolicy` of an `Audience` of `ANY` with one
`Region` for each code, the blackout action and its `Alternate` if any, in the first span that
holds it, and by reference after that. The document imports to the spans it was written from
when each begins where the last ended; a gap between two of them imports as a span under no
blackout. The reference writes one element a line, indented one space a level, times as
`YYYY-MM-DDThh:mm:ss` with a fraction only when there is one and `Z`, and durations in seconds as
`PT`*n*`S`; the corpus's `exported` case holds those bytes. An importer reads any spelling of
§10.2's subset.
### 10.7 A decision and a predicate against a document
A verifier that holds a document and a decision (§4), or a sink predicate, first imports the
document, with its refusal; then reads the decision (§4.5) or the predicate (sink-predicate
§3.5), with its own refusal; and then requires the document's programme to be the record's, and
one of its spans to have the record's `start` and `end` and exactly its blackouts, member for
member. A record that fails this is refused `scte224_decision_differs`.
What a decision states beside its span and its blackouts, the evidence, the blackouts applied,
the verdict and the programme shown in place, follows from them (§4.6) and is checked by §4.5. So
a verifier that holds the document and the region a session aggregated to recomputes the whole
decision, and a sink that holds a predicate of the document reaches the verdict a decision made
away from it would state (§6).
| reason | raised at |
|---|---|
| `scte224_malformed` | §10.2, §10.4, §10.5 |
| `scte224_not_carried` | §10.2, §10.3, §10.5 |
| `scte224_reference_unresolved` | §10.2 |
| `scte224_id_not_token` | §10.2, §10.4 |
| `scte224_audience_unmappable` | §10.4 |
| `scte224_action_unmappable` | §10.4 |
| `scte224_span_open` | §10.5 |
| `scte224_decision_differs` | §10.7 |
### 10.8 Conformance and considerations
[`live-conformance/audience-scte224/`](../live-conformance/audience-scte224/) holds
`vectors.json`, written by `tests/audience_scte224.rs`: documents with the spans each imports to
or the one reason it is refused, among them a document for each rule above, one edit from a
document that imports; the document §10.6 writes; and decisions and predicates held to a document,
each with the verdict a session or a sink at its location reaches. The reference is the
`audience_decision::scte224` module of `wai-rs`, and
`live-conformance/audience_scte224_verify.py` is the second implementation, written from this
section and sharing no code with it.
- **What an import proves.** That the document states these blackouts over these spans. It does
not prove who wrote the document: a document is unsigned, and a party that needs to know its
source holds it from that source. The signed record is the decision made from it.
- **Nothing is approximated.** A postal code is not mapped to the region that holds it, an
excluded audience is not turned into a list, and a policy with one audience that maps and one
that does not is not imported in part: each would state a blackout the rights holder did not.
- **Location.** A document names regions and no session. An import reads no location, and a
decision made from it carries the session's aggregation and nothing more (§4.3).