# 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": ""}`, the record in base64url without padding (RFC 4648 §5). It is written `{"t":,"data":{"wev1":""}}` or `{"l":[,],"data":{"wev1":""}}`, 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 |