Skip to main content

WAI Extension: Live Events

Mirrored from the canonical text at commit f871af55 ().

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/, 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 §5 defines; moderation workflow, which live-moderation defines; and how a sink renders any event.

Relationship to the core spec: wai.live.events is a record (SPEC §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.

2. Terms

3. The event record

3.1 Layout

Integers are little-endian.

offsetsizefieldmeaning
04magicWEV1 (ASCII)
41version1 in this revision
532roomthe room’s id
3732principalthe author’s Ed25519 public key
698sequ64: the event’s position in its chain, from 0
7732prevthe digest (§3.2) of the chain’s event seq − 1; 32 zero bytes when seq is 0
1098tu64: the room clock at the event
1171anchor_flag0: no anchor; 1: an anchor follows
11848anchoronly when anchor_flag is 1: track (32 bytes), group u64, object u64
a1kind§4
a + 12payload_lenu16
a + 3payload_lenpayload§4
a + 3 + payload_len64sig§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 §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

codekindpayload
0x01meta§4.2
0x02cue§4.3
0x03chat§4.4
0x04reaction§4.5
0x05poll_open§4.6
0x06, 0x07vote, poll_close§4.7
0x08marker§4.8
0x09control§4.9
0x0Acaption§4.10
0x0Bdecision§4.11
0x0Ctake_commit§4.12
0x10–0x16join, 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

fieldsizemeaning
typeu81 out (leave the program for a break), 2 in (return to it), 3 chapter, 4 break (out and back, its duration known)
cue_idu32ties an out to its in; an SCTE-35 splice_event_id where the message has one
duration_msu32out and break: the planned duration; in: the actual duration since its out; chapter: its length; 0 when not known
formu80: no SCTE-35 message; 1: an SCTE 35-1 splice_info_section(); 2: an SCTE 35-2 event-based signaling message
scte_lenu16only when form is not 0
sctescte_lenthe 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

One text field: the message.

4.5 reaction

fieldsizemeaning
typeu81 emoji, 2 vote, 3 raise, 4 applause, 5 custom: the reaction types of draft-jennings-moq-mocha-reactions-00 §4.4
valueu16not 0: for emoji and raise, that draft’s §4.5 integer encodings; for the others, the application’s
flagsu8bit 0: a target follows; bit 1: this retracts the reaction it repeats; every other bit 0
target32only when bit 0 is set: the digest of the event reacted to

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

§7 says which votes count, which poll_close closes a poll, and when a stated tally is checked.

4.8 marker

fieldsizemeaning
taxonomyu32what the marker observes, in the application’s taxonomy
polarityu80 neutral, 1 positive, 2 negative
texttexta note
has_attachmentu80 or 1
attachment32only when has_attachment is 1: a digest naming an attached object, such as a voice note sealed as a WAI envelope (jwp-receipts §2.1 content hash)

4.9 control

target (the principal the command is for), command (u8), then the command’s argument:

commandnameargument
1record_startcount_in_ms u32: recording starts this long after t
2record_stopnone
3zoomq16 u32: the zoom factor in Q16.16, not 0
4muteon u8: 1 mute, 0 unmute
5layer_requestlayer 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

fieldsizemeaning
duration_msu32how long the caption shows; 0 when not known
originu81 human, 2 machine, 3 machine-reviewed
model32only when origin is 2 or 3: a digest naming the model that produced the text
lang_lenu8
langlang_lena BCP 47 language tag (RFC 5646), in ASCII
texttextthe 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 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 §5 defines its contents, a device’s rolling commitment to the master it is capturing, and a take verifier reads them.

4.13 Lifecycle

codekind
0x10join
0x11leave
0x12publish
0x13unpublish
0x14grant_exchange
0x15takeover
0x16failover

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.

offsetsizefieldmeaningdefault
04magicWEP1 (ASCII)
41version1 in this revision
532kindskind k is permitted when bit k mod 8 of byte k div 8 is setevery kind of §4 but the lifecycle kinds
372max_payloadthe longest payload, in bytes1024
392max_text_cpthe most code points in one text field500
414max_eventsthe most events in one window (§6.3)50
458window_usthe window, in microseconds of room clock1 000 000
534max_reactionsthe most standalone reactions in one reaction window1
578reaction_window_usthe reaction window1 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:

7. Polls replay as feed state

A poll’s tally is a record of wai.feed.state cells (state-feeds): 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:

The feed: each poll is the node mint_node_id(author, seq) of its poll_open (interactive-worlds §5.4), whose record is the Int cells state, count_0, …, count_{n−1}. Its ops are:

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 §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

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:

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:

attributewhenvalue
IDalwaysquoted: the event’s digest, 64 lowercase hexadecimal digits
CLASSalwaysquoted: science.transaction.wai.event
START-DATEalwaysquoted: t0 + t as YYYY-MM-DDTHH:MM:SS.mmmZ, in UTC, the milliseconds floored, always three digits after the point
DURATIONa cue of type in or chapter, or a caption, with duration_ms above 0duration_ms as decimal seconds with three digits after the point (30.000)
PLANNED-DURATIONa cue of type out or break with duration_ms above 0as DURATION
SCTE35-OUT, SCTE35-IN, SCTE35-CMDa cue of form 10x 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-EVENTalwaysquoted: the record in base64url without padding

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:

fieldvalue
sizethe box’s length in bytes
typeemsg
version, flags1, 0
timescale1000
presentation_timeu64: pt0 + ⌊t / 1000⌋, where pt0 is the presentation time of t0 in milliseconds on the track’s timeline
event_durationa cue or a caption: duration_ms, or 0xFFFFFFFF (not known) when it is 0; any other kind: 0
idthe first four bytes of the event’s digest, as a u32
scheme_id_uriurn:wai:event:1, then a zero byte
valuethe kind’s name (§4), then a zero byte
message_datathe record

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 §3.3 defines. A recording verifies with the outcome accept, or states its gaps.

9. Reasons

reasonraised 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/ holds the corpus, in one file, vectors.json, written from fixed seeds by wai_live_events_vectors gen. It has:

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

Appendix A — Registration request

For the “MSF Event Timeline Types” registry (MSF §14.2):

Event TypeDescriptionSpecification
urn:wai:event:1WAI live event records (WEV1), one per record, in data.wev1this extension