open-standards/wai/extensions/co-watch.md
Transaction Science 25bb230db5
Some checks are pending
wai release binaries build on Windows / the seven release binaries · windows (push) Waiting to run
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 35s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 49s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 34s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 47s
WAI Studio / does this change touch it (push) Successful in 44s
wai-app and Create / wai-app · does this change touch it (push) Successful in 47s
wai-fleet service / does this change touch it (push) Successful in 46s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 23s
WAI examples / WAI examples · does this change touch them (push) Successful in 18s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m49s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m4s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 4m35s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 56s
wai-fleet service / service · linux (push) Successful in 6m5s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 10m6s
WAI Studio / Studio in WebKit · linux (push) Successful in 22m55s
WAI examples / WAI examples · portable · linux (push) Successful in 34m44s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 49s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 57s
wai-fleet service / service · macos (push) Successful in 34s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 11m22s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 8m59s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m30s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m20s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 5m10s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m25s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 14m44s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 11m30s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 22m23s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 16m18s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 11m5s
joule-moq-web: the room page reads the source's playout timing, verifies each object on its run, and presents each frame at its instant
Co-watch §15.7 listed as not done a page of the browser client that reads the TEMPO properties
the room's own source states. The room page now does, where GET /room names a source.

- web/room/tempo.js: playout timing as a page reads it (draft-nandakumar-moq-tempo-00, co-watch
  §15): MOQT -22's varints and property lists, CaptureTimestamp and PlayoutDelay inside
  Immutable Properties and HopTime beside them (tempo_malformed otherwise), short timestamps,
  wai_tempo (a number written as a fraction refused), a missing delay from the track or the
  catalog, the bound, the instant by scenario, HopTime's checks (tempo_hop_backwards,
  tempo_hop_step), and Lateness, which counts each object once: on time, late, refused with its
  reason, or in flight. tempo.test.mjs holds it to wai/moqt-conformance/tempo/vectors.json, the
  corpus the room service's reading is held to; eight weakenings of the reader each fail it.
- web/room/source.js and app.js: the page subscribes to the source's video and sound with Sync
  Support (a sync_support request parameter of the wasm codec) and their receipts tracks;
  verifies each object on its running receipt (a wasm RunVerifier over joule-moq's running,
  fed from the source's events track) or, where none covers it, its group's receipts, the
  receipts held to the run; reads CMAF headers and chunks; decodes with WebCodecs; draws each
  frame at its object's instant (capture plus delay, on the room service's clock) plus the
  frame's offset in its object, and starts sound on the audio clock at the same instants. An
  object ready only after its instant is drawn at once and reported late, by how much; one
  whose timing is refused is shown untimed and counted; one that does not verify is not shown
  and is counted. HopTime, the one-way delay and its baseline are reported. Running receipts
  are not listed in the timeline. GET /room's source states its property types.
- e2e/room.e2e.mjs, scenario source: a room service with [source] behind the same-origin
  proxy, through a relay whose door holds each publisher's media to its key; then the proxy
  holds every byte the relay sends the page for the delay and 700 ms more
  (serveSameOrigin's stall), and lets it through.

- The door (joule-moq-relay) and the page take no run for a group they began reading inside
  (live-http-egress §3.5: "An egress that joins inside a group takes no run for it"): such a
  record is no running_skips_object refusal. joule-moq's running::Join. Found by this test,
  whose page joins mid-group; the door's unit test now begins reading inside a group too.

The browser test on the Studio's installed browser (Google Chrome, the M3 Ultra, 11 November
2026, one run, 12 of 12 checks): 285 frames sent in time each presented within 50 ms of its
instant, |error| median 2.1 ms, at most 16.7 ms; with the relay's bytes held 1700 ms against a
1000 ms delay, 7 video and 7 audio objects reported late (by 518 to 733 ms) and nothing else;
every object on time, late, refused (3, before the first key frame) or in flight; HopTime
one-way 2 to 3 ms, baseline 1 ms, none refused; 259 running receipts taken, none refused; the
relay's door refused nothing. That host's record on that day (D8).

co-watch §15.7 says what the page does; decision D40.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 16:59:49 -05:00

885 lines
54 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# WAI Extension: 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. §15 maps `draft-nandakumar-moq-tempo-00`
> (the latest revision in the IETF archive on 10 November 2026), read whole, onto them; a
> clause of it is cited as "the draft's §*n*".
## 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. An event it refuses it neither carries nor holds: a viewer
whose event was refused because the room had taken it for gone (its presence ended) enters
again and signs the positions of its chain from where the room's copy ends, and the room
holds one event at each, not two different events of one `seq` (live-events §11).
- **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).
## 15. Playout timing over MOQT
`draft-nandakumar-moq-tempo-00` (TEMPO) gives each MOQT object the instant a subscriber
presents it: a **CaptureTimestamp** and a **PlayoutDelay**, whose sum is that instant, and a
**HopTime** each relay rewrites, from which a subscriber without a synchronized clock
estimates one. It leaves to an application who decides the delay and how subscribers report
back (its §3.2). In a room that watches together both are this extension's: the instant an
object plays is the instant the controlled position (§6) reaches it, the reports are
presentation statements (§7), and a change of delay is a control (§5). This section holds a
source's TEMPO timing to the room clock, so that a broadcast audience presents each object at
one stated instant, and that instant is the room's.
### 15.1 Terms
- *t0* is the room's `t0` as microseconds since 1970-01-01T00:00:00Z (live-events §2), so room
clock instant *x* is the wallclock instant *t0* + *x*.
- A source's **object** here is one MOQT object of the source's track, whose media begins at
source position *q* and lasts *len*, both in microseconds of the source (§2). How a source's
objects have positions is the source's (§3.5); a position is given to this section.
- An object's **stated instant** is its CaptureTimestamp plus its PlayoutDelay, in
milliseconds since 1970-01-01T00:00:00Z (the draft's §2.1, §2.2, §5.1).
### 15.2 The properties
The draft's property types are not assigned (its §7, TBD1 to TBD3). Until they are, the
reference uses three types of the range MOQT leaves to an application, each odd, so each
value is bytes (MOQT §8.3): PlayoutDelay `0x3A71`, four octets, big-endian; CaptureTimestamp
`0x3A73` and HopTime `0x3A75`, each one variable-length integer (MOQT §8.1). They are
placeholders, and a peer that uses other values does not read them. The draft is written
against MOQT -18; the properties ride an object of -23, -22 and -18 alike, and at -16, which
calls them extension headers, the same pairs with the same value bytes.
CaptureTimestamp and PlayoutDelay are immutable (the draft's §2.1, §2.2): a publisher writes
them inside the object's Immutable Properties (MOQT §10.7), and a sink reads them nowhere
else. HopTime is each relay's to overwrite (the draft's §2.3), so it is outside them. A sink
refuses an object's timing, `tempo_malformed`, where a property appears twice, a value is not
its form, a CaptureTimestamp or PlayoutDelay is outside Immutable Properties, or a HopTime is
inside them; it presents the object as it would one with no timing, and never at an instant
a relay could have written.
An object states less than both where the draft lets it (its §4): a timestamp below 16 384 is
the low 14 bits of one, completed from the sink's own time (its §4.1); an object without a
PlayoutDelay takes the last one an earlier object of its track stated, then the catalog's
(its §2.2, §4.4); and an object without a CaptureTimestamp takes its capture time from its
track's time-aligned groups (its §4.3). The catalog member `wai_tempo`
([moq-streaming-format](moq-streaming-format.md) §4.4) carries the catalog's part, and the
bound of §15.4. An object left with no capture time is `tempo_capture_absent`, and with no
delay `tempo_delay_absent`. A short timestamp is a sink's to complete while it receives; a
party that checks a claim later is given the capture time in full.
### 15.3 When an object is due
An object at position *q*, of length *len*, is **due** at the first instant *x* of the room
clock, at or after a given instant *from*, at which the controlled position is at least *q*
and below *q* + *len*: the instant every sink presents its first sample. Within one state of
§6.1 that plays from anchor (*a*, *p*) at rate *r*, the position first reaches *q* > *p* at
*a* + ⌈(*q* − *p*) × 1 000 000 / *r*⌉. An object the position is already inside at *from*,
or at a control's instant, is due at that instant, which is how a source that has not begun
to play shows its first object, and how a `seek` shows the object it lands in. An object the
controls never bring the position into, from *from* on, is not due: one behind the position,
one a `seek` passes over, one past the source's end, or any object of a source that does not
play.
*from* says which showing is meant, since a `seek` back shows an object again: a publisher
stamps the showing it is sending for, and a verifier reads a claim from the instant its
object was sent after.
### 15.4 Stamping and checking
A publisher gives a due object a CaptureTimestamp *C*, the capture time it has for it, and
the PlayoutDelay *P* that brings their sum to the millisecond the object is due in:
*C* + *P* = ⌈(*t0* + *x*) / 1000⌉
The draft's instants are whole milliseconds and the room clock's are microseconds, so the
stated instant is the first millisecond at or after the due instant, less than a millisecond
late and never early. A track states the greatest delay its objects carry, `max_delay_ms`,
and a publisher that cannot meet it does not stamp the object. A publisher **SHOULD** stamp
an object no earlier than the policy's `lead_ms` before it is due: a control is sent at least
that far ahead of its instant (§5.3), so nothing accepted later moves an instant that close.
An object stamped earlier, whose instant a later control moves, fails step 4 below; nothing
checks when a publisher stamped.
A party that holds the room's accepted controls checks a **timing claim** (*C*, *P*) of an
object, in this order:
1. *P* is at most the track's `max_delay_ms`: `tempo_delay_out_of_bound`.
2. (*C* + *P*) × 1000 is at least *t0*: `tempo_before_room`.
3. The object is due (§15.3): `tempo_not_due`.
4. *C* + *P* is ⌈(*t0* + *x*) / 1000⌉ for its due instant *x*: `tempo_off_position`.
The claim then **holds**, and its instant is *x*. Only the sum is held to the room: a
capture time is the publisher's statement of when the media was captured, which nothing here
checks, and two claims of one sum are one instant. A sink that holds a claim presents the
object at its stated instant; a sink that refuses one presents the object as the controlled
position has it, by §7.3, and counts the refusal.
### 15.5 The room as the draft's PlaySyncServer
The draft's PlaySyncServer hears each subscriber's sync state and tells the publisher to
change the PlayoutDelay (its §3.6, §5.2). In a room:
- **Feedback** is the viewers' presentation statements (§7) and the clock's skew statements
(§8). A viewer's error (§8.3), divided by the rate, is how far behind it is.
- **The decision** follows the draft's §3.6: while every viewer is within the policy's bound
there is no adjustment; when more than half the viewers are behind by more than the bound,
for as many windows in a row as the room requires (the hysteresis the draft asks for), the
delay grows by the median of what those viewers are behind (the lower of the two middle
ones, of an even count), in whole milliseconds, rounded up.
- **A change of delay is controls.** The draft applies a change from one object on and keeps
the spacing after it (its §3.4.2). A delay longer by *d* from instant `at` is a `pause` at
`at` and a `play` at `at` + *d*: every object due after `at` is then due exactly *d* later,
at any rate, and the controlled position at an instant after the `play` is what it was *d*
earlier. A delay shorter by *d* is a `seek` at `at`, forward by what the source plays in
*d* at its rate. A source that is not playing at `at` has no schedule to shift, and the
change is no control. The controls are sent, accepted and ordered as any others (§5), so
the delay is never changed by a party the room's grants do not let control it, and the
controlled position stays the one account of when the source plays.
- **A sink's clock.** A sink with a synchronized clock presents an object at its stated
instant (the draft's §5.1, scenario 1). One without estimates the reference clock from
HopTime and its own receive times: with *D* its receive time less the HopTime and *B* the
least *D* of its recent objects, at its receive time + *P* − (*D* − *B*) on its own clock
(scenario 2), or at HopTime + *P* + *B* where only relays keep time (scenario 3). It
checks, as the draft's §6 asks, that HopTime does not go back (`tempo_hop_backwards`) or
jump by more than its own limit (`tempo_hop_step`), and does not use a HopTime it refuses.
HopTime is when a relay forwarded the object, and a relay forwards a track's streams beside
one another, so "does not go back" is exact along one subgroup's stream; across the
track's streams a HopTime is held to within the sink's limit of the latest the track has
carried, either way. Its presentation statements are on its estimate of the room clock
(§7.3) either way, so an estimate that is off is measured as skew.
### 15.6 Reasons
| code | when |
|---|---|
| `tempo_malformed` | An object's timing properties do not read (§15.2), or its instant overflows. |
| `tempo_capture_absent` | The object states no CaptureTimestamp and its track has no time-aligned groups. |
| `tempo_delay_absent` | The object states no PlayoutDelay, no earlier object of its track stated one, and the catalog states none. |
| `tempo_delay_out_of_bound` | The PlayoutDelay is above the track's `max_delay_ms`; or, on stamping, the delay the object needs is, or its capture time is after the instant it is due. |
| `tempo_before_room` | The stated instant is before the room's `t0`. |
| `tempo_not_due` | The controlled position does not reach the object (§15.3). |
| `tempo_off_position` | The stated instant is not the millisecond the object is due in. |
| `tempo_hop_backwards` | A HopTime is before the one the previous object of its subgroup's stream carried, or before the latest the track has carried by more than the sink's limit. |
| `tempo_hop_step` | A HopTime is past the latest the track has carried by more than the sink's limit. |
| `catalog_tempo_malformed` | The track's `wai_tempo` does not read (moq-streaming-format §4.4). |
### 15.7 Conformance and what is not done
The reference implementation is the `tempo` module of `joule-moq-room`
(`joule-code/joule-code-rs/crates/joule-moq-room`), over the `co_watch` module's controlled
position. The corpus is
[`moqt-conformance/tempo/vectors.json`](../moqt-conformance/tempo/vectors.json): the
properties' bytes and ones that do not read, short timestamps, `wai_tempo` members with
time-aligned capture times, timing claims under seven scripts of controls with the instant
or the reason each has, changes of delay with the controls that make them and the instants
before and after, and sinks' targets in the three scenarios. Its second implementation,
`moqt-conformance/tempo_verify.py`, is written from this section and §5 and §6; it computes
the controlled position itself, gives every case its outcome, changes cases and requires
each change to be caught. The crate's tests also sweep objects across every script and
require each stamped instant to be the first at which the controlled position is inside its
object.
**Through a relay.** `joule-moq-relay` rewrites HopTime with its own clock as it forwards an
object, for a subscription that carried the draft's Sync Support parameter (its §2.4) or,
where configured, for every subscription, so a sink behind it has the draft's second and
third scenarios as well as the first. The parameter is no parameter of MOQT, and an unknown
message parameter cannot be skipped, so the relay's SETUP declares a Setup Option (`0x3A79`,
empty) and a SUBSCRIBE at `draft-ietf-moq-transport-23` or -22 may then carry Sync Support
(`0x3A77`, length 0); -18 and -16 have no form for it, and a subscriber at those drafts is
stamped only where the relay stamps every subscription. The relay changes nothing else of an
object, adds no HopTime to one that has none, and keeps a short timestamp short; what its
cache and its recording window hold, and so what a FETCH brings, carries the HopTime the
object arrived with. The four code points are placeholders, the draft's being unassigned:
the reference holds them in one module (`joule-moq`'s `tempo`), and the relay's
configuration sets the three property types where a deployment must meet another choice.
The module's `Stamper` is a publisher of a source on a session: it stamps each object it
sends, with its own clock as the first HopTime. Its `Receiver` is a subscriber: it holds each
object received to this section's rules and its HopTime to the draft's §6 checks. The room
service's test publishes stamped objects on a session through the relay and holds each to
the room at a subscriber, with planted claims (an instant one millisecond off, a delay past
the bound, a capture time outside Immutable Properties) each refused with its reason.
**A room's own source.** The room service publishes a source of its own over MOQT where its
operator configures one (`[source]`): a package's video and sound, in real time, as a
principal with a grant of its own. Each object carries a CaptureTimestamp and a PlayoutDelay
inside Immutable Properties and a HopTime beside them, of the types the relay is configured
with, and its track's `wai_tempo` states the delay and its bound. That source is live, not
watched together: no controls govern it and no controlled position is its schedule, so its
instants are not held to this section's §15.3 and §15.4. Each is its capture plus the delay,
and every object of the source states one capture clock: its CaptureTimestamp less its own
media time is one constant across the source's tracks, which the service's test holds every
object received through the relay to, with its delay, its bound, its HopTime of the relay's
clock and its arrival before its instant. The source also signs a running receipt for each
object as it sends it ([live-http-egress](live-http-egress.md) §3.5): a relay's door that
holds the source's tracks to its key reads them and passes each object as its group's run
covers it, and the HTTP egress serves each part under it. The service's test holds the source
to such a door, every object passed on its run and none refused.
**A page that reads the stamps.** The browser client's room page (`joule-moq-web`'s
`web/room/`) reads the source's: it subscribes to its tracks with Sync Support, reads each
object's CaptureTimestamp and PlayoutDelay inside Immutable Properties and its HopTime beside
them under the types `GET /room` names, takes a missing delay from the track's last or the
catalog's and holds it to the bound, and verifies the object against its running receipt or
its group's receipts before anything of it is shown. It presents the object's first sample at
its stated instant on the room service's clock (the clock the page reads with the room), each
later sample its own offset after, as §15.3 has a sink present an object; it holds each HopTime
to §15.5's checks and reports the one-way delay and its baseline. An object ready only after
its instant is presented at once and reported late, by how much; one whose timing is refused
is presented as untimed and counted with its reason; one that does not verify is not presented
and is counted with its reason. Every object is counted once, so none is dropped without a
word. Its reading of the properties is held to this extension's corpus, and a browser test
through the relay holds each frame to within 50 ms of its instant and a path held past the
delay to be reported late.
Not done in this revision: a watch room's file is served over HTTP and stamped by nothing.