live-events §4.4 and §4.5: a chat and a reaction may state an origin after their last field (1 human, 2 machine, 3 machine-reviewed, with the model's digest for 2 and 3), as a caption does. live-rooms §4.4: the grant attribute `ai` names the model that acts for an AI participant; §9.2 step 6 refuses its chat, reaction or caption that does not state origin 2 or 3 with that model (`ai_unmarked`), and a relay does not forward one. Both corpora gain vectors (additions only); both second implementations read the new fields. Co-Authored-By: Claude Opus 5.5 <[email protected]>
693 lines
36 KiB
Markdown
693 lines
36 KiB
Markdown
# WAI Extension: Live Events
|
||
|
||
> Status: Draft. Registers the record `wai.live.events`: a signed, timeline-bound event
|
||
> track. Timed metadata, ad and chapter cues, chat, reactions, polls, markers,
|
||
> remote-control commands, captions, moderation decisions and room lifecycle are all
|
||
> *events*, and an event is one record, `WEV1`: signed by its principal, hash-chained to
|
||
> that principal's previous event, stamped with the room clock, and optionally anchored to
|
||
> one media object. The same bytes ride an MSF event timeline over MoQ, an
|
||
> `EXT-X-DATERANGE` tag, a CMAF `emsg` box, a WebRTC data channel and a recording. No new
|
||
> transport: WAI defines none by charter, and this extension does not change that. Field
|
||
> semantics and rules over them only. The reference implementation is the `live_events`
|
||
> module of `wai-rs` (feature `live_events`); the corpus is
|
||
> [`live-conformance/events/`](../live-conformance/events/), and its second implementation
|
||
> is `live-conformance/live_events_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Drafts read for this revision: `draft-ietf-moq-msf-01` (MSF),
|
||
> `draft-wilaw-moq-scte35-event-timeline-00`, `draft-wilaw-moq-webvtt-msf-00`,
|
||
> `draft-jennings-moq-mocha-chat-00` and `draft-jennings-moq-mocha-reactions-00`, all
|
||
> Internet-Drafts, none past Working Group Last Call; with RFC 8216 §4.3.2.7 and AOM
|
||
> id3-emsg v1.0.0. A section number of one of those drafts in this text is that
|
||
> revision's.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the event record and its digest and signature (§3); the kinds and their
|
||
payloads (§4); the event policy a verifier holds each principal to (§5); verification of
|
||
one record and of each principal's chain, with a delivery gap told apart from corruption
|
||
(§6); poll tallies as `wai.feed.state` cells (§7); and the field mappings onto MoQ, LL-HLS,
|
||
CMAF and DASH, WebRTC and a recording, with what a subscriber joining mid-stream receives
|
||
(§8).
|
||
|
||
Out of scope: a transport; the room descriptor, which fixes a room's id and its clock, and
|
||
the room grant, which carries a principal's policy (both belong to a later extension);
|
||
the contents of a `take_commit` (§4.12), which [capture binding](capture-binding.md)
|
||
§5 defines; moderation
|
||
workflow, which [live-moderation](live-moderation.md) defines; the ad breaks a room's cues
|
||
open, and their interstitials and asset lists, which [live-ads](live-ads.md) defines; and how a
|
||
sink renders any event.
|
||
|
||
Relationship to the core spec: `wai.live.events` is a **record** ([SPEC](../SPEC.md) §5):
|
||
a signed record a WAI tool emits and verifies. It names no payload format and is not
|
||
dispatchable.
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**: a multi-party live session. Its id is 32 bytes. The room extension defines it
|
||
as the digest of the signed room descriptor; here it is opaque, and a verifier is given
|
||
it.
|
||
- **Room clock**: microseconds since the room's `t0`, a UTC instant the room fixes. A
|
||
binding that needs a wallclock is given `t0` as microseconds since 1970-01-01T00:00:00Z;
|
||
one that needs a media time is given the presentation time of `t0`.
|
||
- **Principal**: an Ed25519 public key (RFC 8032), 32 bytes. Its `did:key` form names the
|
||
same key.
|
||
- **Event**, **record**: one `WEV1` (§3).
|
||
- **Chain**: one principal's events in one room, ordered by `seq`.
|
||
- **Head**: the event of a chain a verifier accepted last.
|
||
- **Stream order**: the order in which a verifier is offered one principal's records. Each
|
||
binding says what it is (§8).
|
||
|
||
## 3. The event record
|
||
|
||
### 3.1 Layout
|
||
|
||
Integers are little-endian.
|
||
|
||
| offset | size | field | meaning |
|
||
|---|---|---|---|
|
||
| 0 | 4 | `magic` | `WEV1` (ASCII) |
|
||
| 4 | 1 | `version` | 1 in this revision |
|
||
| 5 | 32 | `room` | the room's id |
|
||
| 37 | 32 | `principal` | the author's Ed25519 public key |
|
||
| 69 | 8 | `seq` | u64: the event's position in its chain, from 0 |
|
||
| 77 | 32 | `prev` | the digest (§3.2) of the chain's event `seq − 1`; 32 zero bytes when `seq` is 0 |
|
||
| 109 | 8 | `t` | u64: the room clock at the event |
|
||
| 117 | 1 | `anchor_flag` | 0: no anchor; 1: an anchor follows |
|
||
| 118 | 48 | `anchor` | only when `anchor_flag` is 1: `track` (32 bytes), `group` u64, `object` u64 |
|
||
| *a* | 1 | `kind` | §4 |
|
||
| *a* + 1 | 2 | `payload_len` | u16 |
|
||
| *a* + 3 | `payload_len` | `payload` | §4 |
|
||
| *a* + 3 + `payload_len` | 64 | `sig` | §3.2 |
|
||
|
||
*a* is 118 without an anchor and 166 with one.
|
||
|
||
An **anchor** binds an event to one MoQ object, so that a marker, a cue or a caption lands
|
||
on an exact frame. `track` is SHA-256 over the track's namespace as its MSF catalog entry
|
||
has it (its own, or the catalog track's that it inherits, MSF §5.2.2), one zero byte, and
|
||
the track's name; `group` and `object` are the object's MOQT Group ID and Object ID. An
|
||
anchor names an object on MoQ; the other bindings carry it unchanged.
|
||
|
||
`payload_len` lets a payload be up to 65535 bytes, the format's limit; the bound a
|
||
verifier applies is the policy's `max_payload` (§5).
|
||
|
||
### 3.2 Digest and signature
|
||
|
||
The **digest** of an event is SHA-256 (FIPS 180-4) over its record from `magic` through
|
||
`payload`: every byte but `sig`. It is written as 64 lowercase hexadecimal digits. An event
|
||
is named by its digest wherever another event refers to it: `prev`, a vote's poll, a
|
||
reaction's or a decision's target.
|
||
|
||
The **signature** is Ed25519 (RFC 8032, pure Ed25519) by `principal`, over the 15 bytes
|
||
`wai:live-event` (ASCII) and 0x01, followed by the bytes the digest covers. It is verified
|
||
strictly, as [SPEC](../SPEC.md) §7 *Signatures* requires: a key that fails step 1 or 2 is
|
||
`bad_key`, and a signature that fails a later step is `bad_signature`.
|
||
|
||
### 3.3 Reading a record
|
||
|
||
A reader reads exactly the bytes it is given as one record, 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 `WEV1`: `malformed`.
|
||
2. No byte after the magic: `truncated`. A version other than 1: `unknown_version`. A
|
||
reader reads no further field, since a later version's layout is its own.
|
||
3. The bytes end before a field the layout requires: `truncated`. An `anchor_flag` other
|
||
than 0 or 1, when it is read: `malformed`.
|
||
4. Bytes after `sig`: `malformed`.
|
||
|
||
## 4. Kinds and payloads
|
||
|
||
| code | kind | payload |
|
||
|---|---|---|
|
||
| `0x01` | `meta` | §4.2 |
|
||
| `0x02` | `cue` | §4.3 |
|
||
| `0x03` | `chat` | §4.4 |
|
||
| `0x04` | `reaction` | §4.5 |
|
||
| `0x05` | `poll_open` | §4.6 |
|
||
| `0x06`, `0x07` | `vote`, `poll_close` | §4.7 |
|
||
| `0x08` | `marker` | §4.8 |
|
||
| `0x09` | `control` | §4.9 |
|
||
| `0x0A` | `caption` | §4.10 |
|
||
| `0x0B` | `decision` | §4.11 |
|
||
| `0x0C` | `take_commit` | §4.12 |
|
||
| `0x10`–`0x16` | `join`, `leave`, `publish`, `unpublish`, `grant_exchange`, `takeover`, `failover` | §4.13 |
|
||
|
||
No other code is defined in this revision; a record of another kind is refused
|
||
`unknown_kind` (§6.1). Codes `0x17` to `0x1F` are reserved for lifecycle kinds.
|
||
|
||
A payload is read in the order its fields are written, and each field's rule applies when
|
||
it is read. A payload that ends before a field, that holds bytes after its last field, or
|
||
whose field holds a value this section does not allow, is `malformed_payload`; a text
|
||
field's own rules are §4.1's. Integers are little-endian. A digest field is 32 bytes.
|
||
|
||
### 4.1 Text
|
||
|
||
A text field is a u16 length followed by that many bytes. Its rules, in order:
|
||
|
||
1. The bytes are UTF-8 (RFC 3629): no overlong form, no surrogate, nothing above U+10FFFF.
|
||
Otherwise `bad_utf8`.
|
||
2. They hold at most the policy's `max_text_cp` code points (§5; 500 by default).
|
||
Otherwise `text_too_long`.
|
||
|
||
Each text field is bounded on its own. Empty text is allowed.
|
||
|
||
### 4.2 `meta`
|
||
|
||
Opaque application metadata: the whole payload, read for its length alone. It is the timed
|
||
metadata a player hands to its application.
|
||
|
||
### 4.3 `cue`
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `type` | u8 | 1 out (leave the program for a break), 2 in (return to it), 3 chapter, 4 break (out and back, its duration known) |
|
||
| `cue_id` | u32 | ties an out to its in; an SCTE-35 `splice_event_id` where the message has one |
|
||
| `duration_ms` | u32 | out and break: the planned duration; in: the actual duration since its out; chapter: its length; 0 when not known |
|
||
| `form` | u8 | 0: no SCTE-35 message; 1: an SCTE 35-1 `splice_info_section()`; 2: an SCTE 35-2 event-based signaling message |
|
||
| `scte_len` | u16 | only when `form` is not 0 |
|
||
| `scte` | `scte_len` | the message, verbatim |
|
||
|
||
Form 1 is read only as far as this: `table_id` is 0xFC, and `section_length` (the low 12
|
||
bits of bytes 1 and 2) plus 3 is `scte_len`. WAI reads no command, descriptor or CRC of
|
||
it. Form 2 is at least one byte, carried opaque. A `type` or `form` not in the table is
|
||
`malformed_payload`. The summary (`type`, `cue_id`, `duration_ms`) is what a sink acts on
|
||
without parsing SCTE-35. Where a message is carried too, both describe one splice, and a
|
||
producer **MUST NOT** write a summary its message contradicts.
|
||
|
||
### 4.4 `chat`
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `text` | text | the message |
|
||
| `origin` | u8 | optional: 1 human, 2 machine, 3 machine-reviewed |
|
||
| `model` | 32 | only when `origin` is 2 or 3: a digest naming the model that produced the message |
|
||
|
||
`origin` is present exactly when a byte follows `text`; a chat without it states no origin.
|
||
`origin` and `model` mean what a caption's mean (§4.10): they are the message's
|
||
machine-generation disclosure, and a sink that labels machine-generated messages takes the
|
||
label from them. A room may require them of a principal: [live-rooms](live-rooms.md) §4.4
|
||
marks an AI participant, whose every chat states origin 2 or 3.
|
||
|
||
### 4.5 `reaction`
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `type` | u8 | 1 emoji, 2 vote, 3 raise, 4 applause, 5 custom: the reaction types of `draft-jennings-moq-mocha-reactions-00` §4.4 |
|
||
| `value` | u16 | not 0: for emoji and raise, that draft's §4.5 integer encodings; for the others, the application's |
|
||
| `flags` | u8 | bit 0: a target follows; bit 1: this retracts the reaction it repeats; every other bit 0 |
|
||
| `target` | 32 | only when bit 0 is set: the digest of the event reacted to |
|
||
| `origin` | u8 | optional, as a chat's (§4.4): present exactly when a byte follows the field before it |
|
||
| `model` | 32 | only when `origin` is 2 or 3 |
|
||
|
||
A reaction without a target is **standalone**. Standalone reactions are held to the
|
||
policy's reaction rate (§6.3), as that draft holds them to one per second per device
|
||
(§5.1 there). A room's polls use `poll_open` and `vote`, not reaction type 2.
|
||
|
||
### 4.6 `poll_open`
|
||
|
||
`question` (text), `n` (u8, at least 2), then `n` option texts. A poll is named by the
|
||
digest of its `poll_open`.
|
||
|
||
### 4.7 `vote` and `poll_close`
|
||
|
||
- `vote`: `poll` (a poll's digest), `option` (u8).
|
||
- `poll_close`: `poll`, `n` (u8, at least 2), then `n` counts (u32 each): the author's
|
||
statement of the final tally.
|
||
|
||
§7 says which votes count, which `poll_close` closes a poll, and when a stated tally is
|
||
checked.
|
||
|
||
### 4.8 `marker`
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `taxonomy` | u32 | what the marker observes, in the application's taxonomy |
|
||
| `polarity` | u8 | 0 neutral, 1 positive, 2 negative |
|
||
| `text` | text | a note |
|
||
| `has_attachment` | u8 | 0 or 1 |
|
||
| `attachment` | 32 | only when `has_attachment` is 1: a digest naming an attached object, such as a voice note sealed as a WAI envelope ([jwp-receipts](jwp-receipts.md) §2.1 content hash) |
|
||
|
||
### 4.9 `control`
|
||
|
||
`target` (the principal the command is for), `command` (u8), then the command's argument:
|
||
|
||
| `command` | name | argument |
|
||
|---|---|---|
|
||
| 1 | `record_start` | `count_in_ms` u32: recording starts this long after `t` |
|
||
| 2 | `record_stop` | none |
|
||
| 3 | `zoom` | `q16` u32: the zoom factor in Q16.16, not 0 |
|
||
| 4 | `mute` | `on` u8: 1 mute, 0 unmute |
|
||
| 5 | `layer_request` | `layer` u8: the layer asked for |
|
||
|
||
A control event takes effect at its `t` on the room clock, so a count-in lines up for every
|
||
participant. Whether the target acts on it is the target's; the event records that it was
|
||
asked, by whom and when.
|
||
|
||
### 4.10 `caption`
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `duration_ms` | u32 | how long the caption shows; 0 when not known |
|
||
| `origin` | u8 | 1 human, 2 machine, 3 machine-reviewed |
|
||
| `model` | 32 | only when `origin` is 2 or 3: a digest naming the model that produced the text |
|
||
| `lang_len` | u8 | |
|
||
| `lang` | `lang_len` | a BCP 47 language tag (RFC 5646), in ASCII |
|
||
| `text` | text | the caption |
|
||
|
||
`lang` is read here only as far as this: 2 to 35 characters; a first subtag of 2 to 8
|
||
letters; then subtags of 1 to 8 letters or digits, each after one hyphen. A tag that fails
|
||
is `malformed_payload`. `origin` is the event's machine-generation disclosure: a sink that
|
||
labels machine-generated content, in its player, in a playlist characteristic or in a
|
||
provenance assertion, takes the label from it.
|
||
|
||
### 4.11 `decision`
|
||
|
||
`verdict` (u8: 1 allow, 2 deny, 3 redact), `target` (the digest of the event decided on),
|
||
`reason` (u16: a reason code the room defines). A decision is an event of the principal
|
||
that decided, and the target stays in its chain unchanged. A sink **SHOULD NOT** show
|
||
content a decision of a principal the room authorises to moderate denies or redacts.
|
||
[Live-moderation](live-moderation.md) defines the statement that follows a decision in its
|
||
author's chain, what else a target may name, and how one decision supersedes another.
|
||
|
||
### 4.12 `take_commit`
|
||
|
||
Read here for its length alone, which `max_payload` bounds: a room's verifier carries it
|
||
opaque. [Capture binding](capture-binding.md) §5 defines its contents, a device's rolling
|
||
commitment to the master it is capturing, and a take verifier reads them.
|
||
|
||
### 4.13 Lifecycle
|
||
|
||
| code | kind |
|
||
|---|---|
|
||
| `0x10` | `join` |
|
||
| `0x11` | `leave` |
|
||
| `0x12` | `publish` |
|
||
| `0x13` | `unpublish` |
|
||
| `0x14` | `grant_exchange` |
|
||
| `0x15` | `takeover` |
|
||
| `0x16` | `failover` |
|
||
|
||
Payload: `subject` (the principal the event is about), then `detail`, the rest of the
|
||
payload, opaque in this revision: a track name, or the digest of a grant. The room
|
||
extension defines `detail` for each kind. A room authority, or a relay acting for it,
|
||
authors these; the default policy permits them to no one (§5).
|
||
|
||
## 5. The event policy
|
||
|
||
An event policy states what one principal may send. A verifier holds every principal to a
|
||
policy: a default one, and one of the principal's own where it has one. In this revision
|
||
the verifier's caller gives it the policies. The room grant of a later extension carries
|
||
these same fields, per principal.
|
||
|
||
The policy record, `WEP1`, is 65 bytes; integers are little-endian.
|
||
|
||
| offset | size | field | meaning | default |
|
||
|---|---|---|---|---|
|
||
| 0 | 4 | `magic` | `WEP1` (ASCII) | |
|
||
| 4 | 1 | `version` | 1 in this revision | |
|
||
| 5 | 32 | `kinds` | kind *k* is permitted when bit *k* mod 8 of byte *k* div 8 is set | every kind of §4 but the lifecycle kinds |
|
||
| 37 | 2 | `max_payload` | the longest payload, in bytes | 1024 |
|
||
| 39 | 2 | `max_text_cp` | the most code points in one text field | 500 |
|
||
| 41 | 4 | `max_events` | the most events in one window (§6.3) | 50 |
|
||
| 45 | 8 | `window_us` | the window, in microseconds of room clock | 1 000 000 |
|
||
| 53 | 4 | `max_reactions` | the most standalone reactions in one reaction window | 1 |
|
||
| 57 | 8 | `reaction_window_us` | the reaction window | 1 000 000 |
|
||
|
||
A reader refuses a policy record: with fewer than 4 bytes, `truncated`; with another magic,
|
||
`policy_malformed`; with no version byte, `truncated`; with another version,
|
||
`unknown_version`; shorter than 65 bytes, `truncated`; longer, `policy_malformed`; that
|
||
permits a kind §4 does not define, or in which any bound is 0, `policy_malformed`.
|
||
|
||
Every bound this extension gives a number for is one of these defaults, and a room
|
||
**MAY** set any other. A room authority's policy is the default with the lifecycle kinds
|
||
also permitted.
|
||
|
||
## 6. Verification
|
||
|
||
A verifier verifies one room's records, against the room id it is given. It is offered
|
||
records one at a time, each principal's in stream order. A record it refuses changes
|
||
nothing it holds.
|
||
|
||
### 6.1 One record
|
||
|
||
In this order; the first check that fails names the refusal:
|
||
|
||
1. The record reads (§3.3): `truncated`, `malformed` or `unknown_version`.
|
||
2. Its `kind` is one §4 defines: otherwise `unknown_kind`.
|
||
3. Its `room` is the verifier's room: otherwise `wrong_room`.
|
||
4. Its signature verifies (§3.2): otherwise `bad_key` or `bad_signature`.
|
||
5. Its principal's policy permits its kind: otherwise `kind_not_permitted`.
|
||
6. `payload_len` is at most the policy's `max_payload`: otherwise `payload_too_large`.
|
||
7. Its payload reads (§4), with the policy's `max_text_cp`: otherwise `malformed_payload`,
|
||
`bad_utf8` or `text_too_long`.
|
||
|
||
### 6.2 A principal's chain
|
||
|
||
For each principal the verifier keeps the digests of the events it holds, by `seq`, and
|
||
the chain's head. For a record that passed §6.1:
|
||
|
||
1. If the verifier holds an event with this `seq` and this digest, the record is a
|
||
**duplicate**: it is ignored, counted nowhere, and checked no further. The records a
|
||
group repeats at its start (§8.2) are duplicates to a subscriber that holds them.
|
||
2. With a head:
|
||
- `seq` at or below the head's: `seq_replay` when an event with this `seq` is held (a
|
||
second, different event claims a `seq` already used), and `seq_not_increasing`
|
||
otherwise (it arrives behind the head);
|
||
- `seq` one above the head's: `prev` **MUST** be the head's digest, otherwise
|
||
`chain_broken`;
|
||
- `seq` more than one above the head's: the seqs between are a gap;
|
||
- in each of the last two cases, `t` **MUST NOT** be below the head's `t`, otherwise
|
||
`time_reversed`.
|
||
3. Without a head, the first record of the principal the verifier accepts: when `seq` is
|
||
0, `prev` **MUST** be 32 zero bytes, otherwise `chain_broken`; when `seq` is above 0,
|
||
seqs 0 to `seq − 1` are a gap.
|
||
|
||
Then §6.3. An event accepted becomes the head, and a gap it opens is recorded.
|
||
|
||
A gap is a **detectable delivery gap**, reported with the code `delivery_gap`. The
|
||
verifier knows exactly which seqs it does not hold, and that the events it does hold are
|
||
the principal's, in order. A gap is not a refusal, and a verifier **MUST NOT** report it as
|
||
corruption. A receiver **MAY** fill a gap by fetching the events it lacks (MOQT FETCH of
|
||
earlier groups, or a recording) and verifying the chain again from its start. It **MUST
|
||
NOT** splice an event behind its head into a chain it has moved past: that record is
|
||
refused `seq_not_increasing`, by step 2.
|
||
|
||
### 6.3 Rates
|
||
|
||
Over the events of the principal the verifier has accepted (a duplicate or a refused
|
||
record is not counted):
|
||
|
||
1. Counting this event, more than `max_events` events whose `t` is less than `window_us`
|
||
before this event's `t`: `rate_exceeded`.
|
||
2. For a standalone reaction (§4.5), counting it, more than `max_reactions` standalone
|
||
reactions whose `t` is less than `reaction_window_us` before its `t`:
|
||
`rate_exceeded`.
|
||
|
||
An earlier event at `t_f` is within a window `w` of an event at `t` when `t − t_f < w`:
|
||
two events exactly one window apart do not share one. A verifier with a gap counts only
|
||
what it holds; one that holds the whole chain checks the rate exactly.
|
||
|
||
### 6.4 Outcomes
|
||
|
||
Verifying a sequence of records gives one outcome:
|
||
|
||
- **refuse**: the reason and the index of the first record refused. The verifier stops
|
||
there.
|
||
- **gap**: no record was refused, and the verifier does not hold every seq of every chain
|
||
it saw. It states each gap as the principal and the first and last seq it lacks, in the
|
||
order found, under the code `delivery_gap`.
|
||
- **accept**: no record was refused, there is no gap, and every stated tally equals the
|
||
replay (§7). A stated tally that does not is **refuse**, `tally_mismatch`, at the index
|
||
of its `poll_close`.
|
||
|
||
## 7. Polls replay as feed state
|
||
|
||
A poll's tally is a record of `wai.feed.state` cells ([state-feeds](state-feeds.md)): the
|
||
state an op log of `wai.world.sim.feed0` replays to. A tally is therefore
|
||
replay-equivalent, and a tally an author states is checkable.
|
||
|
||
Over the events a verifier accepted:
|
||
|
||
- A **poll** is a `poll_open`. Its **close** is the `poll_close` naming it with the lowest
|
||
`seq` among those its author wrote. A `poll_close` by any other principal closes
|
||
nothing.
|
||
- A `vote` **counts** when the verifier holds its poll, its `option` is below the poll's
|
||
`n`, its `t` is at or after the `poll_open`'s `t`, and the poll has no close or the
|
||
vote's `t` is before the close's `t`.
|
||
- A principal's vote in a poll is its last vote that counts, by `seq`; an earlier one is
|
||
replaced.
|
||
|
||
The feed: each poll is the node `mint_node_id(author, seq)` of its `poll_open`
|
||
([interactive-worlds](interactive-worlds.md) §5.4), whose record is the `Int` cells
|
||
`state, count_0, …, count_{n−1}`. Its ops are:
|
||
|
||
- `CREATE` with `Int(1)` and `n` cells `Int(0)`, the actor the poll's author and the
|
||
Lamport counter the `poll_open`'s `seq`;
|
||
- `ADD_INT(1 + option, 1)` for each principal's vote;
|
||
- `SET_CELL(0, Int(0))` when the poll has a close.
|
||
|
||
Every verifier derives these ops from signed events; they are not sent and not signed. A
|
||
vote one principal changes is, equally, `ADD_INT −1` on the old option and `+1` on the new,
|
||
and the replay reaches the same state. The tally's state hash is feed0's,
|
||
`BLAKE3("wai:world-state\x01" ‖ canonical state bytes)` ([state-feeds](state-feeds.md)
|
||
§4), over every poll the verifier holds.
|
||
|
||
A `poll_close`'s counts are its author's statement. A verifier that holds every chain
|
||
whole (no gap) checks each close: its `n` equals the poll's, and each count equals the
|
||
votes for that option. Otherwise it refuses with `tally_mismatch`, at the lowest index of
|
||
such a close. With a gap a verifier does not make this check: a tally it computes is
|
||
unchecked.
|
||
|
||
## 8. Bindings
|
||
|
||
Field semantics only. Each binding says how the record is carried, what it derives from
|
||
the record, its stream order (§6), and how a reader refuses what it cannot read
|
||
(`binding_malformed`) or what disagrees with the record it carries (`binding_mismatch`).
|
||
The record a reader takes from a binding is then verified by §6.
|
||
|
||
### 8.1 MoQ: an MSF event timeline
|
||
|
||
- Each principal publishes its events on a track of its own, as
|
||
`draft-jennings-moq-mocha-chat-00` §3.3 has each device publish on its own track. A
|
||
chain and a track are then one unit, and a relay fans tracks out without merging them.
|
||
The room extension names the tracks.
|
||
- The track is an MSF event timeline (MSF §8). Its catalog entry has `packaging`
|
||
`"eventtimeline"` (MSF §5.2.4), `eventType` `"urn:wai:event:1"` (MSF §5.2.5), `mimetype`
|
||
`"application/json"`, and `depends` naming the tracks its anchors name (MSF §8.2).
|
||
- A **record** of the timeline (MSF §8.1) has one index member and `data`:
|
||
- an anchored event whose `group` and `object` are each at most 2^53 − 1 has
|
||
`"l": [group, object]`, the MOQT Location of the anchored object;
|
||
- any other event has `"t"`: `⌊(t0 + t) / 1000⌋`, the wallclock in milliseconds since
|
||
1970 (MSF §7.1.1), which **MUST** be at most 2^53 − 1;
|
||
- `data` is `{"wev1": "<base64url>"}`, the record in base64url without padding
|
||
(RFC 4648 §5).
|
||
|
||
It is written `{"t":<t>,"data":{"wev1":"<b64>"}}` or
|
||
`{"l":[<group>,<object>],"data":{"wev1":"<b64>"}}`, with no whitespace. This binding
|
||
never writes `m`: an event has no media timestamp of its own, and an anchor names its
|
||
object exactly. An events track uses `t` and `l` record by record, as
|
||
`draft-wilaw-moq-scte35-event-timeline-00` §4.2 also does; MSF §8.1's preference for
|
||
one index type is a **SHOULD**.
|
||
- A reader parses the record as JSON and ignores members this binding does not define.
|
||
It refuses as `binding_malformed`: anything but a JSON object; no string `data.wev1`;
|
||
one that is not the canonical base64url of its bytes (the RFC 4648 §5 alphabet, no `=`,
|
||
and the encoding of what it decodes to); bytes that §3.3 does not read as a record of a
|
||
kind §4 defines; and a record with other than exactly one of `t`, `l` and `m`. It refuses
|
||
as `binding_mismatch` an index other than the one this binding writes for the event:
|
||
`t` a JSON integer equal to the event's, or `l` an array of exactly two integers equal
|
||
to its anchor's.
|
||
- **Groups.** An event's group is `⌊t / 60 000 000⌋`, the minute of room clock it falls
|
||
in, as `draft-jennings-moq-mocha-chat-00` §3.4 groups messages by minute. Group IDs
|
||
increase with `t`, since `t` never decreases along a chain. Object 0 of a group is the
|
||
independent object of MSF §8.3: a JSON array of the join set (§8.2) at the group's
|
||
start. Each event of the group is then the next object, an array of its one record, an
|
||
incremental update (MSF §8.3). MSF §8.3 has the independent object hold every record
|
||
"accumulated and accessible" so far. For an events track the records accessible at a
|
||
group boundary are its join set; earlier records stay reachable by FETCH of earlier
|
||
groups.
|
||
- **Stream order** is Location order: by group, then by object, and the records of one
|
||
object in the order written.
|
||
- **Cues.** A publisher **MAY** also publish each cue of form 1 on a timeline of
|
||
`draft-wilaw-moq-scte35-event-timeline-00`, its `scte35_payload` the base64 (RFC 4648
|
||
§4) of the cue's section and its index chosen by that draft's §4.2, so that a consumer
|
||
of SCTE-35 alone reads it unchanged. That draft registers the event type
|
||
`urn:scte:scte35:2022:bin`, and MSF §14.2's initial table spells it
|
||
`urn:scte:scte35:2013:bin`; this revision takes no position between the two.
|
||
- **Captions.** A publisher **MAY** also publish each caption as a cue of
|
||
`draft-wilaw-moq-webvtt-msf-00` on a timeline of its own. This revision defines no
|
||
mapping between the two.
|
||
- **Registration.** WAI requests `urn:wai:event:1` in the "MSF Event Timeline Types"
|
||
registry (MSF §14.2, Expert Review). The URN is the form of that registry's entries,
|
||
and it is also this extension's `emsg` scheme (§8.4). Until it is registered it is a
|
||
custom type in MSF §10.5's sense.
|
||
|
||
### 8.2 Joining mid-stream
|
||
|
||
The **join set** of a principal's track at the start of group *g* (room clock *g* × 60 s)
|
||
is drawn from the principal's events whose `t` is before that instant. In `seq` order it
|
||
holds:
|
||
|
||
- every `poll_open` whose poll has no `poll_close` of its author before the instant
|
||
(**MUST**);
|
||
- in each of those polls, the principal's last vote (**MUST**);
|
||
- every `caption` with a `duration_ms` above 0 whose interval, `t` to
|
||
`t + duration_ms`, contains the instant (**MUST**);
|
||
- every `cue` of type out or break with no later `in` of the same `cue_id` (**MUST**);
|
||
- any other event (**MAY**).
|
||
|
||
The records are repeated byte for byte, so each is a duplicate (§6.2 step 1) to a
|
||
subscriber that holds it and the state a joining subscriber starts from. This is how
|
||
`draft-wilaw-moq-webvtt-msf-00` §5 has the first object of each group carry the cues
|
||
accumulated so far. A subscriber that joins verifies from the first record it holds and
|
||
reports what it lacks as gaps (§6.2), so it knows a tally it computes is unchecked. It
|
||
learns a final tally from the poll author's `poll_close`, or checks one by fetching the
|
||
voters' earlier groups.
|
||
|
||
### 8.3 LL-HLS: `EXT-X-DATERANGE`
|
||
|
||
An event in a Media Playlist is one `EXT-X-DATERANGE` tag (RFC 8216 §4.3.2.7, which
|
||
low-latency HLS keeps). Its attributes are written in this order, separated by commas with
|
||
no spaces:
|
||
|
||
| attribute | when | value |
|
||
|---|---|---|
|
||
| `ID` | always | quoted: the event's digest, 64 lowercase hexadecimal digits |
|
||
| `CLASS` | always | quoted: `science.transaction.wai.event` |
|
||
| `START-DATE` | always | quoted: `t0 + t` as `YYYY-MM-DDTHH:MM:SS.mmmZ`, in UTC, the milliseconds floored, always three digits after the point |
|
||
| `DURATION` | a cue of type in or chapter, or a caption, with `duration_ms` above 0 | `duration_ms` as decimal seconds with three digits after the point (`30.000`) |
|
||
| `PLANNED-DURATION` | a cue of type out or break with `duration_ms` above 0 | as `DURATION` |
|
||
| `SCTE35-OUT`, `SCTE35-IN`, `SCTE35-CMD` | a cue of form 1 | `0x` and the section in uppercase hexadecimal: `SCTE35-OUT` for out and break, `SCTE35-IN` for in, `SCTE35-CMD` for chapter (RFC 8216 §4.3.2.7.1) |
|
||
| `X-WAI-EVENT` | always | quoted: the record in base64url without padding |
|
||
|
||
- RFC 8216 §4.3.2.7.1 represents a splice out/in pair with tags that share one `ID`. Here
|
||
each tag carries an event of its own under its own `ID`, because two tags of a playlist
|
||
that share an `ID` must agree on every attribute both carry, and their `X-WAI-EVENT`
|
||
values differ. `cue_id` ties the pair.
|
||
- RFC 8216 asks a client attribute's name to follow reverse-DNS form. `X-WAI-EVENT`
|
||
appears only under its `CLASS`, whose semantics every date range of that class follows
|
||
(RFC 8216 §4.3.2.7).
|
||
- A playlist that carries one of these tags **MUST** carry `EXT-X-PROGRAM-DATE-TIME`
|
||
(RFC 8216 §4.3.2.7), on the same UTC timeline as `t0`.
|
||
- **Stream order** is the order of the tags in the playlist, across its updates.
|
||
- A reader takes the tag as `#EXT-X-DATERANGE:` and an attribute list (RFC 8216 §4.2):
|
||
attribute names of `A`–`Z`, `0`–`9` and `-`, each at most once, and a quoted value that
|
||
runs to the next quote. It refuses as `binding_malformed`: another tag; an attribute list
|
||
that does not read; no `X-WAI-EVENT`, one not quoted, or one that is not canonical
|
||
base64url (§8.1); and bytes that §3.3 does not read as a record of a kind §4 defines. It
|
||
then renders the tag for that record and refuses as `binding_mismatch` unless each
|
||
attribute of the table above is, in the tag and in the rendering, either absent from
|
||
both or present in both with the same value as written. Attributes the table does not
|
||
name are ignored, and their order is not compared.
|
||
|
||
### 8.4 CMAF and DASH: `emsg` version 1
|
||
|
||
An event is one Event Message box, version 1 (ISO/IEC 23009-1, the syntax AOM id3-emsg
|
||
v1.0.0 §2.2.2 reproduces). Integers are big-endian:
|
||
|
||
| field | value |
|
||
|---|---|
|
||
| `size` | the box's length in bytes |
|
||
| `type` | `emsg` |
|
||
| `version`, `flags` | 1, 0 |
|
||
| `timescale` | 1000 |
|
||
| `presentation_time` | u64: `pt0 + ⌊t / 1000⌋`, where `pt0` is the presentation time of `t0` in milliseconds on the track's timeline |
|
||
| `event_duration` | a cue or a caption: `duration_ms`, or 0xFFFFFFFF (not known) when it is 0; any other kind: 0 |
|
||
| `id` | the first four bytes of the event's digest, as a u32 |
|
||
| `scheme_id_uri` | `urn:wai:event:1`, then a zero byte |
|
||
| `value` | the kind's name (§4), then a zero byte |
|
||
| `message_data` | the record |
|
||
|
||
- `id` lets a player drop a box it has seen. A receiver identifies an event by its digest
|
||
(§6.2), since four bytes of a digest can repeat.
|
||
- **Stream order** is presentation order: the order of the boxes in the segments, by
|
||
`presentation_time`, and within one time in the order the segment holds them.
|
||
- A reader refuses as `binding_malformed`: a box shorter than 32 bytes, whose `size` is
|
||
not its length, whose type is not `emsg` or whose version is not 1; a box without two
|
||
strings each ended by a zero byte; and `message_data` that §3.3 does not read as a
|
||
record of a kind §4 defines. It then renders the box for that record and refuses as
|
||
`binding_mismatch` unless the two are equal byte for byte.
|
||
- A publisher whose players surface only ID3 timed metadata **MAY** also carry the record
|
||
in ID3 as AOM id3-emsg v1.0.0 has it (scheme `https://aomedia.org/emsg/ID3`). This
|
||
revision defines no ID3 frame for it.
|
||
|
||
### 8.5 WebRTC
|
||
|
||
A data channel (RFC 8831), reliable and ordered, with binary messages. One message is one
|
||
record, whole. **Stream order** is message order. A sink that needs an event's media time
|
||
takes it from the anchor or from the room clock; the RTP absolute capture time header
|
||
extension, where a sender adds it, is experimental.
|
||
|
||
### 8.6 Recording
|
||
|
||
A recording holds each principal's chain whole: its records concatenated in `seq` order,
|
||
each record's length following from its own fields (§3.1). **Stream order** is that order.
|
||
In a sealed package the events are one component, which
|
||
[live-recording](live-recording.md) §3.3 defines. A recording verifies with the outcome accept, or states its gaps.
|
||
|
||
## 9. Reasons
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `truncated` | §3.3, §5 |
|
||
| `malformed` | §3.3 |
|
||
| `unknown_version` | §3.3, §5 |
|
||
| `unknown_kind` | §6.1 step 2 |
|
||
| `wrong_room` | §6.1 step 3 |
|
||
| `bad_key` | §3.2, §6.1 step 4 |
|
||
| `bad_signature` | §3.2, §6.1 step 4 |
|
||
| `kind_not_permitted` | §6.1 step 5 |
|
||
| `payload_too_large` | §6.1 step 6 |
|
||
| `malformed_payload` | §4, §6.1 step 7 |
|
||
| `bad_utf8` | §4.1 |
|
||
| `text_too_long` | §4.1 |
|
||
| `seq_replay` | §6.2 step 2 |
|
||
| `seq_not_increasing` | §6.2 step 2 |
|
||
| `chain_broken` | §6.2 steps 2 and 3 |
|
||
| `time_reversed` | §6.2 step 2 |
|
||
| `delivery_gap` | §6.2, §6.4: a gap, not a refusal |
|
||
| `rate_exceeded` | §6.3 |
|
||
| `tally_mismatch` | §7 |
|
||
| `policy_malformed` | §5 |
|
||
| `binding_malformed` | §8.1, §8.3, §8.4 |
|
||
| `binding_mismatch` | §8.1, §8.3, §8.4 |
|
||
|
||
## 10. Conformance
|
||
|
||
[`live-conformance/events/`](../live-conformance/events/) holds the corpus, in one file,
|
||
`vectors.json`, written from fixed seeds by `wai_live_events_vectors gen`. It has:
|
||
|
||
- **vectors**: a room, the policies its principals are held to, and records in stream
|
||
order, with the outcome §6.4 gives them. An accepted vector also states each record's
|
||
digest, the tally (§7) and each record's MSF record, `EXT-X-DATERANGE` tag and `emsg`
|
||
box (§8); a vector with a gap states its gaps. Every kind has an accepted vector, and
|
||
chains of several principals are interleaved in one. Every refusal of §6 has a vector
|
||
refused with it, each a control of one defect: a flipped signature bit and three
|
||
forgeries that a lenient Ed25519 verifier accepts, a broken chain, a reorder, a replayed
|
||
`seq`, `t` going back, an oversize payload, a kind not permitted, both rates exceeded,
|
||
invalid UTF-8 and overlong text, truncated and malformed records, an unknown version and
|
||
kind, the wrong room, malformed payloads (a chat stating a machine origin without its
|
||
model among them) and a stated tally the replay does not reach. Chats and a reaction
|
||
stating each origin have an accepted vector.
|
||
- **policy cases**: policy records, read (§5) or refused with their reason.
|
||
- **binding cases**: tags, boxes and MSF records, read back to their record (§8) or
|
||
refused with their reason.
|
||
|
||
A conforming verifier gives every vector, policy case and binding case the outcome, the
|
||
reason, the index, the gaps, the digests, the tally and the renderings the corpus states.
|
||
`wai_live_events_vectors verify` and the test `tests/live_events.rs` check that the file
|
||
regenerates byte for byte and that `wai-rs` gives every case its outcome.
|
||
`live-conformance/live_events_verify.py` is the second implementation, written from this
|
||
text: it shares no code with `wai-rs`, verifies signatures with WAI's strict rule written
|
||
from RFC 8032 (`claim-conformance/_ed25519.py`) and computes BLAKE3 from its specification
|
||
(`claim-conformance/_blake3.py`). It gives every case the same outcome.
|
||
|
||
## 11. Security and privacy considerations
|
||
|
||
- Every field of an event is signed. A relay can drop an event but cannot alter one, and
|
||
a drop is a gap the receiver detects (§6.2).
|
||
- `t` is its author's claim. A room clock authority and per-hop receipts, both of later
|
||
extensions, witness when an event was seen; this revision checks only that a chain's
|
||
`t` never goes back.
|
||
- Two different events with one `seq` are refused `seq_replay`. A receiver that holds
|
||
both holds two signatures by one key over one position: evidence the author
|
||
equivocated.
|
||
- Payloads are untrusted data: an SCTE-35 message, `meta`, a lifecycle `detail` and a
|
||
`take_commit` are carried, not interpreted, as
|
||
`draft-wilaw-moq-scte35-event-timeline-00` §6.2 has SCTE-35 payloads treated.
|
||
- The policy's payload, text and rate bounds limit how much one principal can make every
|
||
receiver hold and check.
|
||
- A principal's events are linkable to one another, since one key signs them all. A room
|
||
that needs participation that cannot be linked across rooms gives principals a key per
|
||
room. Events persist in recordings, and a `decision` that redacts an event does not
|
||
remove it from the chains that hold it (§4.11).
|
||
- Confidentiality is the carrying transport's, and end-to-end encryption of tracks is
|
||
[confidentiality](confidentiality.md)'s; this extension signs events and does not
|
||
encrypt them.
|
||
|
||
## Appendix A — Registration request
|
||
|
||
For the "MSF Event Timeline Types" registry (MSF §14.2):
|
||
|
||
| Event Type | Description | Specification |
|
||
|---|---|---|
|
||
| `urn:wai:event:1` | WAI live event records (`WEV1`), one per record, in `data.wev1` | this extension |
|