open-standards/wai/extensions/live-events.md
Transaction Science ab23c8ab6b live rooms: AI participants are marked in their grant, and chat and reactions state their origin
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]>
2026-10-21 05:17:43 -04:00

36 KiB
Raw Permalink Blame History

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/, 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; the ad breaks a room's cues open, and their interstitials and asset lists, which live-ads 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

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

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): 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 §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 §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 §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/ 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'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