open-standards/wai/extensions/co-watch.md
Transaction Science af4b1529f7
Some checks are pending
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Waiting to run
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 23s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 28s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Waiting to run
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 19s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 38s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Waiting to run
WAI Studio / does this change touch it (push) Successful in 16s
WAI Studio / Studio in Chromium and Firefox · macos (push) Waiting to run
wai-app and Create / wai-app · does this change touch it (push) Successful in 57s
wai-fleet service / does this change touch it (push) Successful in 59s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Waiting to run
wai-fleet service / service · macos (push) Waiting to run
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 24s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 5m34s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 53s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 2m12s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 3m2s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 4m19s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m9s
wai-fleet service / service · linux (push) Successful in 2m34s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 7m36s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 9m46s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 20m23s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 8m53s
WAI Studio / Studio in WebKit · linux (push) Successful in 14m30s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Has started running
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Has started running
Relay robustness: denial-of-service limits, HopTime and Sync Support, the DVR store as MoQ files, a load harness with a signed record, and a switch through the relay
The relay is held to draft-englishm-moq-relay-dos-02 consideration by consideration (the
README's table, which also says what is not bounded). New limits: the sessions refused at
SETUP that are held open, in all and by address, the oldest shed first; subscriptions,
published tracks and namespaces a session; requests a second a session, refused
EXCESSIVE_LOAD with a Retry Interval.

What the MoQ timing package left unwired:
- the relay rewrites TEMPO's HopTime as it forwards, for a subscription that carried the
  draft's Sync Support parameter, negotiated by a Setup Option at -23 and -22 (-18 and -16
  carry none; `stamp = "always"` covers them). The draft's code points are unassigned: the
  placeholders are in joule_moq::tempo alone, and [tempo] sets the three property types;
- joule-moq-room's tempo gains a Stamper and a Receiver, and a test publishes stamped
  objects on a session and holds each to the room at a subscriber through the relay;
- the DVR store writes its window as .moq and .dat files, read back and verified against
  the receipts from the files alone, agreeing with a FETCH byte for byte.

A load harness to draft-evens-moq-bench-00's method (joule_moq_relay::bench,
examples/relay_bench.rs): profiles, the draft's messages and metrics, and a signed, dated
measurement record of the host that ran it, used as a regression guard with stated
tolerances; it compares a host only with itself. A subscriber is switched between two
renditions through the relay by joule-moq's switching plan: every frame once, on a Group
boundary.

Planted faults, each caught: a flood of refused SETUPs (and, with the bounds removed, all
held); a session past each count and past its rate (and, with the limit removed, served); a
switch mid-group; timing claims off the instant, past the bound and outside Immutable
Properties; a data file and a .moq file cut short, a data file outside its directory, a
changed payload; a record changed after signing and a run claimed for another host.

Left: the rewind subscription filter (draft-duke-moq-subscribe-rewind-02), probe tracks at
the relay (the library is in joule-moq), TOO_FAR_BEHIND, FEC tracks and V3C publishing.

Co-Authored-By: Claude <[email protected]>
2026-11-10 23:31:46 -05:00

51 KiB
Raw Permalink Blame History

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/, 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-rooms, live-recording, live-metering and ad-presentation), synthesis §10.3 and delivery-telemetry; 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 and live-events 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 §5): signed records a WAI tool emits and verifies. It names no payload format and is not dispatchable.

Relationship to live-events: 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: 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: 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 and synthesis §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: 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 §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 §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 §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:

  1. 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/ 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 §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 §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 §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 along a track (tempo_hop_backwards) or jump by more than its own limit (tempo_hop_step), and does not use a HopTime it refuses. 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 track's previous object carried.
tempo_hop_step A HopTime is past the previous one 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: 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.

Not done in this revision: the room service's own source is served over HTTP and stamps nothing, so no deployed room publishes stamped objects yet; and no page of the browser client reads these properties.