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 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]>
619 lines
39 KiB
Markdown
619 lines
39 KiB
Markdown
# 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).
|