Some checks failed
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <[email protected]>
663 lines
38 KiB
Markdown
663 lines
38 KiB
Markdown
# WAI Extension: Co-watch
|
||
|
||
> Status: Draft. Registers the record `wai.live.cowatch`: people in different places watching
|
||
> one programme together in a room, with shared control and with their sync measured. The room
|
||
> authority signs a **co-watch policy**, `WXP1`, naming the **source** the room watches (a WAI
|
||
> package, a sequence of on-demand objects, or another room's recording) and the bounds every
|
||
> rule below holds to. A principal whose grant allows it sends **controls** (`play`, `pause`,
|
||
> `seek` and `rate`), each effective at a stated instant of the room clock a little ahead of
|
||
> when it is sent, so that every viewer applies it at that same instant whatever its own
|
||
> latency; controls for one instant resolve by a pinned order, and the controls together
|
||
> define the **controlled position** of the source at every instant. Each viewer's player
|
||
> signs, periodically, a **presentation statement**, `WXS1`: the source position it was
|
||
> presenting at a room-clock instant, under the player's key. From those statements, the room
|
||
> clock signs a **skew statement**, `WXK1`, per window of the room clock: how far each viewer
|
||
> was from the controlled position, and each pair of viewers from one another, as percentiles,
|
||
> with a verdict against the policy's bound, which anyone holding the room recomputes; a viewer
|
||
> whose statements are missing is `unknown`, never in sync. A viewer that drifts past the bound
|
||
> corrects by a pinned **catch-up rule**: a rate nudge within limits, or a seek past a
|
||
> threshold. Controls and statements ride in the room's events, as `meta` payloads that their
|
||
> authors sign. No new transport. The reference implementation is the `co_watch` module of
|
||
> `wai-rs` (feature `co_watch`); the corpus is
|
||
> [`live-conformance/cowatch/`](../live-conformance/cowatch/), and its second implementation
|
||
> is `live-conformance/co_watch_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: the live family this extension builds on, as merged
|
||
> ([live-events](live-events.md), [live-rooms](live-rooms.md),
|
||
> [live-recording](live-recording.md), [live-metering](live-metering.md) and
|
||
> [ad-presentation](ad-presentation.md)), [synthesis](synthesis.md) §10.3 and
|
||
> [delivery-telemetry](delivery-telemetry.md); RFC 8032 and FIPS 180-4. No outside text is
|
||
> pinned: the rules of this extension are WAI's own.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the co-watch policy, its signature, the policy in effect at each instant and the
|
||
source it names (§3); how this extension's statements ride in a room's events, who may sign
|
||
each, and the grant attribute it reads (§4); controls, who may send them, their rules and the
|
||
order in which they apply (§5); the controlled position (§6); presentation statements (§7);
|
||
windows, samples, errors, percentiles, verdicts and the skew statement (§8); the catch-up rule
|
||
(§9); where each rule is enforced (§10); and verification (§11).
|
||
|
||
Out of scope: a transport; how a player fetches and decodes the source; how a player estimates
|
||
the room clock (§7.3 states what it **SHOULD** do, and §14 what a wrong estimate does); how a
|
||
sink renders a control, an indicator or a verdict; and the room's media and chat, which
|
||
[live-rooms](live-rooms.md) and [live-events](live-events.md) already carry, so that a co-watch
|
||
room is also a room whose participants may see and hear one another.
|
||
|
||
Relationship to the core spec: `wai.live.cowatch` is a **record** ([SPEC](../SPEC.md) §5):
|
||
signed records a WAI tool emits and verifies. It names no payload format and is not
|
||
dispatchable.
|
||
|
||
Relationship to [live-events](live-events.md): this extension changes nothing in it. Its
|
||
statements are payloads of the `meta` kind (§4.2 there), which live-events leaves to
|
||
applications, so every statement is signed by its author, chained, stamped with the room clock
|
||
and rate-bounded by its author's policy (live-events §5, §6). A control is not a `control`
|
||
event (live-events §4.9): that kind's commands are fixed, and a record of another command is
|
||
refused by every verifier of the current revision.
|
||
|
||
Relationship to [live-rooms](live-rooms.md): the descriptor's and the grant's layouts are
|
||
unchanged. A room states its co-watch policy in a record of its own, `WXP1` (§3), chained like
|
||
live-moderation's review policy. Who may control is the direct power (live-rooms §3.3, bit 1)
|
||
or a grant attribute (§4.4), which live-rooms §4.4 signs with the grant and leaves to the
|
||
extensions that read it: a new power bit would be refused by every room verifier of the current
|
||
revision (live-rooms §3.4: no power above bit 4).
|
||
|
||
Relationship to [live-recording](live-recording.md): a room's recording is a source (§3.5). A
|
||
viewer that watches it through a relay fetches its tracks below their live edge, from the
|
||
relay's DVR window or its store, which live-rooms §10.6 grants by the seek power: a co-watch
|
||
room watching a recording gives its viewers that power.
|
||
|
||
Relationship to [ad-presentation](ad-presentation.md) and [synthesis](synthesis.md) §10.3: a
|
||
presentation statement is a player's signed claim about what it presented, as a presentation
|
||
claim and a player's log are. It is signed under the player's key as a presentation claim is
|
||
(ad-presentation §5.3), and chained, as a player's log is, by the event chain it rides in.
|
||
|
||
Relationship to [delivery-telemetry](delivery-telemetry.md): a player **MAY** also report its
|
||
`present` and `stall-start` observations there. Those are unsigned and advisory (§1.1 there); a
|
||
presentation statement is signed, and skew is computed from statements alone.
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**, **room clock**, **principal**, **event**, **chain**, **grant**, **grant in effect**,
|
||
**room key**, **merged order**: as in live-events §2 and live-rooms §2 and §9.2. A room key
|
||
is the room authority's key or the clock's.
|
||
- **Statement**: the payload of a `meta` event whose first four bytes are one of the magics of
|
||
§4.1. It is named by the digest of its event (live-events §3.2).
|
||
- **Source**: what the room watches (§3.5). A **source position** is an instant of the source's
|
||
own media time, in microseconds from its start.
|
||
- **Controlled position**: the source position the controls define for an instant of the room
|
||
clock (§6).
|
||
- **Player**: the software that presents the source to a viewer and states what it presented
|
||
(§7). Its **key** is the key of the principal it presents for.
|
||
- **Viewer**: a principal, not a room key, present in the room under a grant that may
|
||
subscribe (live-rooms §3.3).
|
||
- **Error** of a presentation statement: its position less the controlled position at its
|
||
instant (§8.3). **Skew** of two viewers: the difference of their errors.
|
||
- *µs* is microseconds of room clock; a *rate* is in millionths, so 1 000 000 is normal speed.
|
||
|
||
Integers are little-endian; a signed integer is two's complement. A digest is SHA-256 (FIPS
|
||
180-4), 32 bytes. Ed25519 is RFC 8032's, verified strictly ([SPEC](../SPEC.md) §7
|
||
*Signatures*): a key that fails step 1 or 2 is `bad_key`, and a signature that fails a later
|
||
step is `bad_signature`.
|
||
|
||
## 3. The co-watch policy
|
||
|
||
### 3.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WXP1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id (live-rooms §3.2) |
|
||
| `prev` | 32 | the digest of the policy this one replaces (§3.4); 32 zero bytes for the first |
|
||
| `effective` | 8 | u64: the room-clock instant from which this policy applies; 0 for the first |
|
||
| `source_type` | 1 | 1 a WAI package, 2 an object sequence, 3 a recording of a room (§3.5) |
|
||
| `source` | 32 | the source's digest (§3.5) |
|
||
| `duration_us` | 8 | u64: the source's duration, above 0 |
|
||
| `lead_ms` | 4 | u32: the least time from a control's sending to its instant (§5.3), above 0 |
|
||
| `horizon_ms` | 4 | u32: the most time from a control's sending to its instant, at least `lead_ms` |
|
||
| `min_rate`, `max_rate` | 4 each | u32: the rates a control and the catch-up rule may set, `min_rate` above 0 and at most 1 000 000, `max_rate` at least 1 000 000 |
|
||
| `period_ms` | 4 | u32: the slot length: a player states its presentation at least once in each (§7.3, §8.1), above 0 |
|
||
| `window_ms` | 4 | u32: the length of a skew window (§8.1), a multiple of `period_ms` of 1 to 1000 slots |
|
||
| `settle_ms` | 4 | u32: how long after a viewer's join, a control's instant or a source's start a sample is settling (§8.3) |
|
||
| `bound_us` | 4 | u32: the sync bound (§8.5), above 0 |
|
||
| `quantile` | 1 | the percentile a verdict is taken at (§8.5), 1 to 100 |
|
||
| `tolerance_us` | 4 | u32: the error the catch-up rule leaves alone (§9), at most `bound_us` |
|
||
| `nudge` | 4 | u32: the rate the catch-up rule adds or takes away (§9), above 0 |
|
||
| `seek_us` | 4 | u32: the error past which the catch-up rule seeks (§9), above `bound_us` |
|
||
| `sig` | 64 | §3.2 |
|
||
|
||
### 3.2 Digest and signature
|
||
|
||
The **digest** of a policy is SHA-256 over its record from `magic` through `seek_us`. The
|
||
**signature** is Ed25519 by the room descriptor's `authority` over the 20 bytes
|
||
`wai:co-watch-policy` (ASCII) and 0x01, followed by the bytes the digest covers.
|
||
|
||
### 3.3 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given as one policy, in the order of §3.1, and refuses
|
||
at the first of these that holds:
|
||
|
||
1. Fewer than 4 bytes: `truncated`. A magic other than `WXP1`: `malformed`. No byte after the
|
||
magic: `truncated`. A version other than 1: `unknown_version`.
|
||
2. The bytes end before a field the layout requires: `truncated`.
|
||
3. Bytes after `sig`: `malformed`.
|
||
|
||
It then holds the policy to the rules of §3.1's table, and refuses one that breaks any of them
|
||
as `cw_policy_malformed`: `source_type` is 1, 2 or 3; `duration_us`, `lead_ms`, `period_ms`,
|
||
`bound_us` and `nudge` are above 0; `horizon_ms` is at least `lead_ms`; `min_rate` is above 0
|
||
and at most 1 000 000, and `max_rate` at least 1 000 000; `window_ms` is `period_ms` times a
|
||
whole number from 1 to 1000; `quantile` is 1 to 100; `tolerance_us` is at most `bound_us`; and
|
||
`seek_us` is above `bound_us`. Then its `room` is the room id (`wrong_room`), its signature
|
||
verifies (`bad_key`, `bad_signature`), and its link holds (§3.4, `cw_policy_unlinked`).
|
||
|
||
### 3.4 The policy in effect
|
||
|
||
A verifier is given a room's policies in a list. The first has `prev` zero and `effective` 0.
|
||
Each later one has as `prev` the digest of the one before it in the list, an `effective` above
|
||
that one's, and, when it names the source of the one before it (the same `source_type` and
|
||
`source`), that one's `duration_us`. A policy that breaks this is refused `cw_policy_unlinked`.
|
||
The **policy in effect** at room-clock instant *t* is the last of the list whose `effective` is
|
||
at or below *t*.
|
||
|
||
A policy is replaced, never amended. A policy that names another source than the one before it
|
||
**starts** that source at its `effective`: the controlled position begins again (§6.4). One
|
||
that names the same source changes the bounds alone, and the controlled position runs on.
|
||
|
||
### 3.5 The source
|
||
|
||
- **A WAI package** (1): `source` is the `receipt_hash` of the package's receipt
|
||
([packaging](packaging.md) §9). A source position is the package's presentation time.
|
||
- **An object sequence** (2): on-demand media objects presented in order, each its content hash
|
||
(SHA-256 of the object's bytes as served) and its presentation duration in milliseconds (a
|
||
u32, at least 1). `source` is SHA-256 over the 20 bytes `wai:co-watch-source` (ASCII) and
|
||
0x01, the objects' count (u32), and for each object in order its content hash and its
|
||
duration; `duration_us` is 1000 times the sum of the durations. The object at index *j*
|
||
holds the source positions from 1000 times the sum of the durations before it, for 1000
|
||
times its own. This is the form of ad-presentation §4's creative digest, under a label of
|
||
its own.
|
||
- **A recording of a room** (3): `source` is the digest of the recording's manifest
|
||
([live-recording](live-recording.md) §4.1). A source position is the recorded room's clock
|
||
less the recording's first instant.
|
||
|
||
A verifier given an object sequence's objects for a policy checks them against it: the policy
|
||
names an object sequence, its `source` is their digest, and its `duration_us` is 1000 times the
|
||
sum of their durations. Otherwise `cw_source_mismatch`. A player checks the objects it fetches
|
||
by their content hashes, so it presents what the policy names.
|
||
|
||
## 4. Statements
|
||
|
||
### 4.1 Carriage
|
||
|
||
| magic | statement | section | who signs it |
|
||
|---|---|---|---|
|
||
| `WXC1` | control | §5.1 | a room key, or a principal whose grant in effect allows the action (§5.2) |
|
||
| `WXS1` | presentation statement | §7.1 | a viewer's player, with the viewer's key: any principal that is not a room key |
|
||
| `WXK1` | skew statement | §8.4 | a room key |
|
||
|
||
Each statement is the payload of a `meta` event: its magic, a `version` byte of 1, and the
|
||
fields its section lists. A `meta` payload whose first four bytes are none of these magics is
|
||
not this extension's, and a sink that does not implement it shows none of them: they are timed
|
||
metadata for its application (live-events §4.2). Every statement is bounded by its author's
|
||
policy (live-events §5, `max_payload`), 1024 bytes for a room key, which bounds the counts §8.4
|
||
allows.
|
||
|
||
### 4.2 Reading
|
||
|
||
A statement that ends before a field, that holds bytes after its last field, whose `version` is
|
||
not 1, or whose field holds a value its section does not allow, is `cw_statement_malformed`.
|
||
|
||
### 4.3 Who signs
|
||
|
||
A presentation statement by a room key, and a skew statement by any other principal, are
|
||
`cw_not_permitted`. A control's author is checked by §5.2. A grant's powers and attributes are
|
||
those of the author's grant in effect at its event's `t` (live-rooms §9.2 step 4); a room key
|
||
holds every power. Since a statement is a `meta` event, the room verifier has already held it
|
||
to its author's grant: a principal whose policy does not permit `meta` cannot send one
|
||
(live-rooms §9.2 step 4, `kind_not_permitted`).
|
||
|
||
### 4.4 The control attribute
|
||
|
||
This extension reads one grant attribute (live-rooms §4.4):
|
||
|
||
| key | value | meaning |
|
||
|---|---|---|
|
||
| `watch_control` | a decimal integer from 1 to 15 | the actions the principal may control: bit 0 `play`, bit 1 `pause`, bit 2 `seek`, bit 3 `rate` |
|
||
|
||
A **decimal integer** here is 1 or 2 of the digits `0`–`9`, the first not `0`. A grant whose
|
||
`watch_control` is not of that form, or is above 15, is refused `cw_attr_malformed`. A grant
|
||
without it allows no action, unless its powers hold the direct power (§5.2). The authority
|
||
gives a room's hosts every action and its guests some, or every viewer every action, as the
|
||
room calls for.
|
||
|
||
### 4.5 The policy a statement is read under
|
||
|
||
A statement is read under the policy in effect at its event's `t`. A statement at an instant
|
||
with no policy in effect is refused `cw_no_policy`.
|
||
|
||
## 5. Controls
|
||
|
||
### 5.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic`, `version` | 5 | `WXC1`, 1 |
|
||
| `action` | 1 | 1 `play`, 2 `pause`, 3 `seek`, 4 `rate` |
|
||
| `at` | 8 | u64: the room-clock instant it takes effect |
|
||
| `arg` | 8 | u64: for `seek`, the source position to go to; for `rate`, the rate, 1 to 2^32 − 1; for `play` and `pause`, 0 |
|
||
|
||
A control whose `action` is outside 1 to 4, or whose `arg` is not as the table allows for its
|
||
action, is `cw_statement_malformed`.
|
||
|
||
### 5.2 Who may control
|
||
|
||
A control's author is a room key, or a principal whose grant in effect at the control's `t`
|
||
holds the direct power (live-rooms §3.3, bit 1), or carries `watch_control` with the action's
|
||
bit set (§4.4). Otherwise the control is refused `control_not_permitted`.
|
||
|
||
### 5.3 Rules
|
||
|
||
A control is sent ahead of its instant, so that it reaches every viewer before the instant
|
||
comes, and not far ahead. With *t* its event's `t`, under the policy in effect at *t*:
|
||
|
||
1. `at` is at least *t* + `lead_ms` × 1000: otherwise `control_not_ahead`. A control whose
|
||
`at` is at or before its own `t`, in the past when it is sent, is so refused.
|
||
2. `at` is at most *t* + `horizon_ms` × 1000: otherwise `control_beyond_horizon`.
|
||
|
||
Then, under the policy in effect at `at`, the instant the control is for:
|
||
|
||
3. A `seek`'s `arg` is at most `duration_us`, and a `rate`'s is from `min_rate` to `max_rate`:
|
||
otherwise `control_out_of_range`.
|
||
|
||
A relay or a room service that carries the room's events **MUST NOT** carry a control these
|
||
rules or §5.2 refuse, and a player **MUST NOT** apply one.
|
||
|
||
### 5.4 The order of application
|
||
|
||
The accepted controls apply in ascending order of `at`; of two with one `at`, the one whose
|
||
event has the lower `t` first; of two with one `at` and one `t`, the one whose event digest is
|
||
the lower first (the digests compared as bytes). So of two controls for one instant, the one
|
||
sent later prevails, and between two sent at one instant the order is still fixed: every party
|
||
holding the controls applies them in one order, whatever order it received them in.
|
||
|
||
Of the controls at one instant, a later one **overrides** an earlier one that sets the same
|
||
thing: `play` and `pause` set whether the source plays, `seek` sets the position, and `rate`
|
||
sets the rate. A control that a later one at its instant overrides is stated **overridden**,
|
||
and every other control **applied**. Controls at one instant that set different things all
|
||
apply: a `seek` and a `play` at one instant go to the position and play from it.
|
||
|
||
## 6. The controlled position
|
||
|
||
### 6.1 The state
|
||
|
||
The controlled position is a function of the room clock, kept as a **state**: whether the
|
||
source plays, its rate, and an **anchor**: an instant and the source position at it.
|
||
|
||
### 6.2 The position at an instant
|
||
|
||
At instant *x* at or after the anchor's instant *a*, with anchor position *p*, rate *r* and
|
||
duration *D* (the `duration_us` of the policy in effect at *x*), the controlled position is:
|
||
|
||
- while the source plays: *p* + ⌊(*x* − *a*) × *r* / 1 000 000⌋, or *D* if that is above *D*;
|
||
- while it does not: *p*.
|
||
|
||
The product is computed without overflow (at least 128 bits).
|
||
|
||
### 6.3 Applying a control
|
||
|
||
A control at instant `at` first sets the anchor to (`at`, the controlled position at `at`),
|
||
then:
|
||
|
||
- `play`: the source plays; `pause`: it does not;
|
||
- `seek`: the anchor's position becomes `arg`;
|
||
- `rate`: the rate becomes `arg`.
|
||
|
||
A source that reaches its duration while it plays stays at *D*: it has ended, and a `seek`
|
||
before *D* sets it going again from there.
|
||
|
||
### 6.4 Where a source starts
|
||
|
||
A source starts at the first policy's `effective`, 0, and at the `effective` of each later
|
||
policy that names another source (§3.4). At a start *s*, the state is: the source does not
|
||
play, its rate is 1 000 000, and its anchor is (*s*, 0). A control applies to the source in
|
||
effect at its `at`; a control whose `at` is a start applies after it.
|
||
|
||
The controlled position at *x* is then the state reached from the last start at or before *x*
|
||
by applying, in the order of §5.4, every accepted control whose `at` is from that start to
|
||
*x*, and §6.2 at *x*.
|
||
|
||
## 7. Presentation statements
|
||
|
||
### 7.1 Layout
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic`, `version` | 5 | `WXS1`, 1 |
|
||
| `room` | 32 | the room id |
|
||
| `player` | 32 | the key that signs it: the viewer's |
|
||
| `source` | 32 | the source it presents (§3.5) |
|
||
| `at` | 8 | u64: the room-clock instant it describes |
|
||
| `position` | 8 | u64: the source position it was presenting at `at` |
|
||
| `state` | 1 | 1 playing, 2 paused, 3 stalled (waiting for data), 4 ended |
|
||
| `rate` | 4 | u32: the rate it was presenting at, above 0: the controlled rate, or a catch-up rate (§9) |
|
||
| `sig` | 64 | Ed25519 by `player` over the 26 bytes `wai:co-watch-presentation` (ASCII) and 0x01, followed by the record from `magic` through `rate` |
|
||
|
||
A statement whose `state` is outside 1 to 4 or whose `rate` is 0 is `cw_statement_malformed`.
|
||
Its **digest**, where a party names it apart from its event, is SHA-256 over the record from
|
||
`magic` through `rate`.
|
||
|
||
The statement carries its own signature, beside its event's, so that it stands on its own: a
|
||
viewer that shows a third party how closely it watched presents its statements and its grant,
|
||
with no other event of the room.
|
||
|
||
### 7.2 Rules
|
||
|
||
A presentation statement is checked, in this order:
|
||
|
||
1. its `room` is the room id: otherwise `wrong_room`;
|
||
2. its `player` is its event's principal: otherwise `presentation_wrong_key`. A statement
|
||
lifted from another viewer's chain, or made by a key the viewer does not hold, is so
|
||
refused;
|
||
3. its signature verifies under `player` (§7.1): `bad_key`, `bad_signature`;
|
||
4. its `at` is at most its event's `t`: a statement describes what was presented, never what
|
||
will be. Otherwise `presentation_ahead`;
|
||
5. its `source` is the `source` of the policy in effect at `at`: otherwise
|
||
`presentation_wrong_source`.
|
||
|
||
### 7.3 The player
|
||
|
||
A player in a co-watch room **MUST**:
|
||
|
||
- apply each control at its `at` on its estimate of the room clock, and one that reaches it
|
||
after its `at` at once, and present the controlled position by the catch-up rule (§9);
|
||
- state its presentation at least once in every slot (§8.1) while it is present, and **SHOULD**
|
||
state it twice in each, so that timer jitter leaves no slot empty;
|
||
- state as `position` the position of the frame or sample it presented at `at`, as its media
|
||
pipeline reports it, and as `at` the instant on its estimate of the room clock;
|
||
- state `stalled` while it waits for data, and `ended` once it has presented the source's end.
|
||
|
||
A player **SHOULD** estimate the room clock against a room key's clock, by a request whose
|
||
answer states the instant it was made at, taking the answer of least round trip of several
|
||
and the midpoint of that round trip, and **SHOULD** measure again at least each minute. Its
|
||
statements state its estimate: a player whose clock is off states errors that are off by as
|
||
much (§14).
|
||
|
||
## 8. Skew
|
||
|
||
### 8.1 Windows and slots
|
||
|
||
A **window** is a stretch of the room clock, from `from` up to, but not including, `to`, over
|
||
which one policy is in effect and whose length is a whole number of that policy's slots, from 1
|
||
to 1000. Its **slots** are `from` + *j* × *P* to `from` + (*j* + 1) × *P*, *j* from 0, with *P*
|
||
the policy's `period_ms` × 1000. A room clock that states windows regularly states them
|
||
`window_ms` long, starting at multiples of `window_ms` × 1000; §8.4 checks any window a
|
||
statement names.
|
||
|
||
### 8.2 Viewers and due slots
|
||
|
||
A principal is **present** from the `t` of a `join` of it to the `t` of the following `leave`
|
||
of it, if any (live-rooms §7.2). A slot is **due** for a principal that is not a room key when
|
||
it lies wholly within a stretch of its presence (the `join` at or before the slot's start, and
|
||
any `leave` at or after its end), and the grant in effect for it at the slot's start may
|
||
subscribe (live-rooms §3.3). The **viewers** of a window are the principals with a slot of the
|
||
window due.
|
||
|
||
### 8.3 Samples, errors and settling
|
||
|
||
A skew statement names, for each viewer it lists, `held`: how many of the viewer's events the
|
||
room clock held when it made the statement (its next `seq`). A viewer's **sample** in a slot is,
|
||
of its accepted presentation statements whose event's `seq` is below `held` and whose `at` is
|
||
within the slot, the first in the merged order. A statement the clock had not received when it
|
||
made its statement, a later `seq`, is not a sample of that statement, whenever it arrived.
|
||
|
||
A sample's **error** is its `position` less the controlled position at its `at` (§6), a signed
|
||
number of microseconds. A sample is **settling** when its `at` is within `settle_ms` × 1000 µs
|
||
(of the policy in effect at its `at`) after a `join` of the viewer, the `at` of an accepted
|
||
control, or a source's start: at or after that instant and before it plus `settle_ms` × 1000. A
|
||
settling sample fills its slot, and is not **counted**: a viewer seeking to the room on its
|
||
join, or every viewer applying a control, takes a moment that the bound is not about.
|
||
|
||
### 8.4 The skew statement
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic`, `version` | 5 | `WXK1`, 1 |
|
||
| `from`, `to` | 8 each | u64: the window (§8.1) |
|
||
| `bound_us` | 4 | u32: the bound it judges by, the policy's |
|
||
| `quantile` | 1 | the percentile it judges at, the policy's |
|
||
| `viewers` | 1 + 59 × *v* | a u8 count (1 to 8), then each viewer: `principal` (32), `held` (u64), `due`, `samples`, `counted` (u16 each), `p50`, `pq`, `max` (u32 each), `verdict` (u8) |
|
||
| `pairs` | 1 + 17 × *v*(*v* − 1)/2 | a u8 count, then each pair: `a`, `b` (u8 each, indexes into `viewers`), `n` (u16), `p50`, `pq`, `max` (u32 each), `verdict` (u8) |
|
||
|
||
For a viewer: `due` is its due slots, `samples` the due slots it has a sample in, and `counted`
|
||
those whose sample is counted; `p50`, `pq` and `max` are the 50th and the `quantile`-th
|
||
percentile and the greatest of the absolute errors of its counted samples, and all three 0 when
|
||
it has none; `verdict` is §8.5's. For a pair: `n` is the due slots in which both have a counted
|
||
sample, and `p50`, `pq` and `max` are over the absolute values of the skew in those slots, the
|
||
first viewer's error less the second's; `verdict` is §8.5's. A value above 2^32 − 1 is
|
||
written 2^32 − 1.
|
||
|
||
A skew statement is `cw_statement_malformed` when: its viewers are not in strictly ascending
|
||
order of `principal`; a `verdict` is outside 1 to 3; its pairs are not every pair of its
|
||
viewers exactly once, in ascending order of (`a`, `b`) with `a` below `b`; `from` is not below
|
||
`to`; `to` is above its event's `t`; the policy in effect at `from` is not in effect up to `to`,
|
||
or `to` − `from` is not a whole number of its slots from 1 to 1000. Then, under the policy in effect at
|
||
`from`:
|
||
|
||
1. `bound_us` and `quantile` are the policy's: otherwise `skew_mismatch`;
|
||
2. each viewer it lists is a viewer of the window, its `held` is at most the number of that
|
||
viewer's events the room holds, and its entry is the one recomputed from the room: otherwise
|
||
`skew_mismatch`;
|
||
3. each pair's entry is the one recomputed: otherwise `skew_mismatch`.
|
||
|
||
A statement may list a subset of a window's viewers, since one holds at most eight; it states
|
||
the pairs among those it lists. §8.6 requires every viewer of the window to be listed by one.
|
||
|
||
### 8.5 Percentiles and verdicts
|
||
|
||
The *q*-th **percentile** of *n* values, *n* above 0, is the value at index ⌈*q* × *n* / 100⌉
|
||
− 1 of the values in ascending order: the nearest rank. A verdict is 1 **in sync**, 2 **out of
|
||
sync** or 3 **unknown**:
|
||
|
||
- a viewer is **unknown** when it has a due slot with no sample, or no counted sample; it is in
|
||
sync when its `pq` is at most `bound_us`, and out of sync otherwise;
|
||
- a pair is unknown when either of its viewers is unknown or its `n` is 0; it is in sync when
|
||
its `pq` is at most `bound_us`, and out of sync otherwise.
|
||
|
||
"In sync" is so a claim with a stated bound, checked against the statements. A viewer whose
|
||
statements are missing for a slot it was present for is unknown, never in sync: a player that
|
||
stops stating is not counted as keeping up.
|
||
|
||
### 8.6 Completeness
|
||
|
||
A room clock that states a window states all of it. Once every record is read, for each window
|
||
that an accepted skew statement names, every viewer of the window is listed by a skew statement
|
||
of that window: otherwise `skew_incomplete`, at the window's first skew statement in the merged
|
||
order.
|
||
|
||
## 9. Catch-up
|
||
|
||
A player that is off the controlled position corrects by this rule, so that two players given
|
||
the same facts make the same correction, and a test reproduces it. At instant *x*, under the
|
||
policy in effect at *x*, with *c* the controlled position, *r* the controlled rate and *e* the
|
||
player's position less *c*:
|
||
|
||
- When the source does not play, or *c* is the duration (it has ended): **seek** to *c* when
|
||
|*e*| is above `tolerance_us`, and **hold** otherwise.
|
||
- When it plays:
|
||
- |*e*| at most `tolerance_us`: **rate** *r*;
|
||
- |*e*| above `tolerance_us` and at most `seek_us`: **rate** *r* − `nudge` when *e* is above
|
||
0 (ahead: slower), and *r* + `nudge` when it is below (behind: faster), held within
|
||
`min_rate` to `max_rate`;
|
||
- |*e*| above `seek_us`: **seek** to *c*.
|
||
|
||
A player plays while the source plays and pauses while it does not; the rule sets its rate and
|
||
its position. It evaluates the rule each time it states its presentation (§7.3), and keeps the
|
||
outcome until the next. A rate held at a limit corrects nothing, and the error then grows until
|
||
the rule seeks. A seek lands where the room is when it is made; what it took to land is left to
|
||
the next evaluations' rates.
|
||
|
||
## 10. Where each rule is enforced
|
||
|
||
- **A room service or relay that carries the room's events** carries a control only when §5.2
|
||
and §5.3 accept it, and a presentation statement only when §7.2 does (§5.3 asks it of
|
||
controls). It carries each viewer's events in its chain order. A skew statement it signs
|
||
names, as `held`, what it held when it made it, so a statement that arrives later changes
|
||
nothing that statement says.
|
||
- **The room clock** signs the lifecycle (live-rooms §7.2), and so when a viewer is present,
|
||
and signs skew statements (§8.4): what the room states about how closely it watched
|
||
together. The service that serves the source states the policy (§3) and serves the objects
|
||
the policy names.
|
||
- **The room authority** signs the policy and gives the control attribute (§4.4).
|
||
- **Players** apply controls at their instants (§7.3), correct by the catch-up rule (§9) and
|
||
state their presentation (§7). **Sinks** show the verdicts with the bound they are against.
|
||
|
||
After the fact, a verifier (§11) checks every one of these against the room: a control a
|
||
service carried that §5 refuses, a statement signed by a key that is not its viewer's, and a
|
||
skew statement that does not recompute or leaves a viewer out are each found out.
|
||
|
||
## 11. Verification
|
||
|
||
### 11.1 Inputs
|
||
|
||
A co-watch verifier is given what a room verifier is given (live-rooms §9.1: a descriptor,
|
||
grants, a key-log head or none, and the chains), and:
|
||
|
||
- **policies**: the room's co-watch policies, in a list (§3.4);
|
||
- **sources**: for any policy, the objects of the object sequence it names (§3.5), or none.
|
||
|
||
### 11.2 Steps
|
||
|
||
It runs these steps and stops at the first refusal, which it reports with the reason and where:
|
||
|
||
1. **The room** (live-rooms §9): a refusal, at `room` with the room verifier's own place. A gap
|
||
is not a refusal; the steps go on, and the checks of a skew statement that recompute from the
|
||
room (§8.4 steps 2 and 3, and §8.6) are not made: each depends on events the verifier may
|
||
not hold.
|
||
2. **The policies**, in the list's order (§3.3, §3.4), then the sources given (§3.5,
|
||
`cw_source_mismatch`): at the policy's index.
|
||
3. **The grants' attributes** (§4.4), in the grants' order: at the grant's index.
|
||
4. **The statements**, in the merged order: each `meta` event the room accepted whose payload
|
||
begins with a magic of §4.1: §4.2, then §4.5 (`cw_no_policy`), then §4.3, then the rules of
|
||
its section, in the order each section gives. A skew statement's checks that recompute (§8.4
|
||
steps 2 and 3) are made after every statement has been read, in the merged order, since its
|
||
samples may follow it there. At the record.
|
||
5. **Completeness** (§8.6): at the record.
|
||
6. **The outcome** (§11.3).
|
||
|
||
### 11.3 Outcomes and what a verification states
|
||
|
||
- **refuse**: the reason and where (§11.2).
|
||
- **gap**: no refusal, and the room has a gap: the gaps, under the code `delivery_gap`, and
|
||
what **accept** states, computed from what the verifier holds, with each skew statement
|
||
stated unchecked.
|
||
- **accept**: no refusal and no gap.
|
||
|
||
With accept or gap, the verifier states:
|
||
|
||
- the policies' digests, and each start of a source: its instant and the policy's index;
|
||
- each control, in the merged order: its digest, author, action, `at`, `arg`, and `applied` or
|
||
`overridden`;
|
||
- the **timeline**: each instant at which a control or a start changed the state, in order,
|
||
with the state after it (whether the source plays, the rate, and the anchor's position);
|
||
- each presentation statement, in the merged order: its digest, player, `at`, `position`,
|
||
`state`, the controlled position at `at`, its error, and whether it is settling;
|
||
- each skew statement, in the merged order: its digest, its window, and for each viewer and
|
||
each pair the entry recomputed, or `unchecked`.
|
||
|
||
## 12. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `cw_policy_malformed` | §3.3 |
|
||
| `cw_policy_unlinked` | §3.4 |
|
||
| `cw_source_mismatch` | §3.5, §11.2 step 2 |
|
||
| `cw_attr_malformed` | §4.4, §11.2 step 3 |
|
||
| `cw_statement_malformed` | §4.2, §5.1, §7.1, §8.4 |
|
||
| `cw_no_policy` | §4.5 |
|
||
| `cw_not_permitted` | §4.3 |
|
||
| `control_not_permitted` | §5.2 |
|
||
| `control_not_ahead` | §5.3 step 1 |
|
||
| `control_beyond_horizon` | §5.3 step 2 |
|
||
| `control_out_of_range` | §5.3 step 3 |
|
||
| `presentation_wrong_key` | §7.2 step 2 |
|
||
| `presentation_ahead` | §7.2 step 4 |
|
||
| `presentation_wrong_source` | §7.2 step 5 |
|
||
| `skew_mismatch` | §8.4 |
|
||
| `skew_incomplete` | §8.6 |
|
||
|
||
A co-watch verifier also raises every code a room verifier raises (live-rooms §11 and the
|
||
live-events codes it lists), at the room; `truncated`, `malformed` and `unknown_version` for a
|
||
policy that does not read; `wrong_room`, `bad_key` and `bad_signature` for a policy of another
|
||
room or whose signature fails, and for a presentation statement (§7.2); and `delivery_gap` for a
|
||
gap, which is not a refusal.
|
||
|
||
## 13. Conformance
|
||
|
||
[`live-conformance/cowatch/`](../live-conformance/cowatch/) holds the corpus, in one file,
|
||
`vectors.json`, written from fixed seeds by `wai_co_watch_vectors gen`. It has rooms, each a
|
||
room (descriptor, grants, key-log head or none, chains), its policies and sources, and the
|
||
outcome §11.3 gives them with everything it states; and catch-up cases.
|
||
|
||
- **Accepted**: a watch party of three viewers, a host who may control every action, a guest
|
||
who may play and pause, and a viewer who joins late and may control nothing: the host plays,
|
||
the guest pauses and plays, the host seeks and sets a rate and sets it back, two controls for
|
||
one instant resolve by the order of §5.4, the late joiner's first samples settle, and the
|
||
clock's skew statements state every viewer in sync; a room whose second policy starts a new
|
||
source; and a room in which one viewer drifts out of sync, one misses a slot and is unknown,
|
||
one leaves partway, and a window is stated in two statements.
|
||
- **Gap**: the watch party with one viewer's record not delivered: its skew statements
|
||
unchecked.
|
||
- **Refused**: each reason of §12 at least once, each vector a control of one defect: among
|
||
them a control from a party without the control power, a control effective in the past, a
|
||
presentation statement signed by another key than its viewer's and one lifted from another
|
||
viewer's chain, a skew statement that does not recompute, and a viewer whose statements are
|
||
missing reported in sync; and policies that do not read or are of another room.
|
||
- **Catch-up cases**: a policy, the controlled state at an instant and a player's position,
|
||
and the correction §9 gives, for every branch of the rule and both limits of the rate.
|
||
|
||
A conforming verifier gives every room the outcome, the reason, the place, and everything else
|
||
the corpus states, and every catch-up case its correction. `wai_co_watch_vectors verify` and the
|
||
test `tests/co_watch.rs` check that the file regenerates byte for byte, that `wai-rs` gives
|
||
every room its outcome, that every reason of §12 appears, and that §12 lists exactly the
|
||
reference's codes. `live-conformance/co_watch_verify.py` is the second implementation, written
|
||
from this text: it shares no code with `wai-rs`; it verifies each room with the live-rooms
|
||
corpus's second implementation (`live_rooms_verify.py`) and every signature with WAI's strict
|
||
rule (`claim-conformance/_ed25519.py`); and it mutates copies of the corpus and requires every
|
||
copy to fail. The browser client's co-watch module (`web/room/cowatch.js` of `joule-moq-web`)
|
||
gives every catch-up case its correction and writes the corpus's controls and presentation
|
||
statements byte for byte.
|
||
|
||
## 14. Security and privacy considerations
|
||
|
||
- Every control and every statement is signed by its author and rides in its author's chain,
|
||
and every policy is signed by the room authority. A service can withhold an event but alter
|
||
none, and a withheld event is a gap in its author's chain. A withheld control is the room's
|
||
to notice: its author sees its control missing from what the room carries.
|
||
- A control's instant is ahead of its sending by at least the policy's lead, so every viewer
|
||
holds it before it applies, as long as it reaches them within the lead. One that reaches a
|
||
viewer late is applied at once, and the viewer catches up by §9; the skew it causes settles
|
||
within the policy's settle time, and a lead too short for the room's delivery shows as
|
||
viewers out of sync.
|
||
- A presentation statement is a player's claim: it proves that the holder of the viewer's key
|
||
stated it, not that it is true. A modified player can state any position, as a player of
|
||
ad-presentation §13 can. Skew measures how well the room's players say they kept together;
|
||
a relay's per-hop records ([live-metering](live-metering.md) §3) of when it delivered the
|
||
source's objects to each session are independent evidence against a statement of a position
|
||
the session had not received.
|
||
- A player's `at` is on its own estimate of the room clock (§7.3). An estimate that is off
|
||
shifts its errors by as much, in both directions alike, so a viewer that keeps its own clock
|
||
wrong is measured as out of sync, never as in sync on another's behalf.
|
||
- The skew statement is the room clock's, and is recomputed by anyone holding the room: a clock
|
||
that states a viewer in sync without the samples, or leaves an out-of-sync viewer out of a
|
||
window, is refused (§8.4, §8.6). What remains to the clock is to omit a viewer's events from
|
||
the room, which leaves a gap in the viewer's chain (live-events §6.2), and every skew
|
||
statement of a room with a gap is unchecked.
|
||
- A viewer's statements say what it watched and when, which is personal data. A viewer that
|
||
needs its watching unlinkable across rooms uses a key per room, as live-events §11 has every
|
||
principal do. Taking part in a room under a grant is taking part in its mechanics, and this
|
||
extension adds no consent step to them. Where a regime asks the operator for a lawful basis
|
||
for processing what a room's players state, the operator records it as a `lawful-basis`
|
||
claim ([jwp-receipts](jwp-receipts.md) §2.4.6).
|