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, anEXT-X-DATERANGEtag, a CMAFemsgbox, 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 thelive_eventsmodule ofwai-rs(featurelive_events); the corpus islive-conformance/events/, and its second implementation islive-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-00anddraft-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
- 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 givent0as microseconds since 1970-01-01T00:00:00Z; one that needs a media time is given the presentation time oft0. - Principal: an Ed25519 public key (RFC 8032), 32 bytes. Its
did:keyform 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:
- Fewer than 4 bytes:
truncated. A magic other thanWEV1:malformed. - 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. - The bytes end before a field the layout requires:
truncated. Ananchor_flagother than 0 or 1, when it is read:malformed. - 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:
- The bytes are UTF-8 (RFC 3629): no overlong form, no surrogate, nothing above U+10FFFF.
Otherwise
bad_utf8. - They hold at most the policy’s
max_text_cpcode points (§5; 500 by default). Otherwisetext_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
One text field: the message.
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 |
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), thenncounts (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:
- The record reads (§3.3):
truncated,malformedorunknown_version. - Its
kindis one §4 defines: otherwiseunknown_kind. - Its
roomis the verifier’s room: otherwisewrong_room. - Its signature verifies (§3.2): otherwise
bad_keyorbad_signature. - Its principal’s policy permits its kind: otherwise
kind_not_permitted. payload_lenis at most the policy’smax_payload: otherwisepayload_too_large.- Its payload reads (§4), with the policy’s
max_text_cp: otherwisemalformed_payload,bad_utf8ortext_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:
- If the verifier holds an event with this
seqand 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. - With a head:
seqat or below the head’s:seq_replaywhen an event with thisseqis held (a second, different event claims aseqalready used), andseq_not_increasingotherwise (it arrives behind the head);seqone above the head’s:prevMUST be the head’s digest, otherwisechain_broken;seqmore than one above the head’s: the seqs between are a gap;- in each of the last two cases,
tMUST NOT be below the head’st, otherwisetime_reversed.
- Without a head, the first record of the principal the verifier accepts: when
seqis 0,prevMUST be 32 zero bytes, otherwisechain_broken; whenseqis above 0, seqs 0 toseq − 1are 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):
- Counting this event, more than
max_eventsevents whosetis less thanwindow_usbefore this event’st:rate_exceeded. - For a standalone reaction (§4.5), counting it, more than
max_reactionsstandalone reactions whosetis less thanreaction_window_usbefore itst: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 itspoll_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 thepoll_closenaming it with the lowestseqamong those its author wrote. Apoll_closeby any other principal closes nothing. - A
votecounts when the verifier holds its poll, itsoptionis below the poll’sn, itstis at or after thepoll_open’st, and the poll has no close or the vote’stis before the close’st. - 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:
CREATEwithInt(1)andncellsInt(0), the actor the poll’s author and the Lamport counter thepoll_open’sseq;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", anddependsnaming 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
groupandobjectare 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; datais{"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 writesm: an event has no media timestamp of its own, and an anchor names its object exactly. An events track usestandlrecord by record, asdraft-wilaw-moq-scte35-event-timeline-00§4.2 also does; MSF §8.1’s preference for one index type is a SHOULD. - an anchored event whose
-
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 stringdata.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 oft,landm. It refuses asbinding_mismatchan index other than the one this binding writes for the event:ta JSON integer equal to the event’s, orlan 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, asdraft-jennings-moq-mocha-chat-00§3.4 groups messages by minute. Group IDs increase witht, sincetnever 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, itsscte35_payloadthe 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 typeurn:scte:scte35:2022:bin, and MSF §14.2’s initial table spells iturn: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-00on a timeline of its own. This revision defines no mapping between the two. -
Registration. WAI requests
urn:wai:event:1in 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’semsgscheme (§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_openwhose poll has nopoll_closeof its author before the instant (MUST); - in each of those polls, the principal’s last vote (MUST);
- every
captionwith aduration_msabove 0 whose interval,ttot + duration_ms, contains the instant (MUST); - every
cueof type out or break with no laterinof the samecue_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 ownID, because two tags of a playlist that share anIDmust agree on every attribute both carry, and theirX-WAI-EVENTvalues differ.cue_idties the pair. - RFC 8216 asks a client attribute’s name to follow reverse-DNS form.
X-WAI-EVENTappears only under itsCLASS, 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 ast0. - 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 ofA–Z,0–9and-, each at most once, and a quoted value that runs to the next quote. It refuses asbinding_malformed: another tag; an attribute list that does not read; noX-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 asbinding_mismatchunless 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 |
idlets 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, whosesizeis not its length, whose type is notemsgor whose version is not 1; a box without two strings each ended by a zero byte; andmessage_datathat §3.3 does not read as a record of a kind §4 defines. It then renders the box for that record and refuses asbinding_mismatchunless 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-DATERANGEtag andemsgbox (§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 replayedseq,tgoing 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 and a stated tally the replay does not reach. - 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).
tis 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’stnever goes back.- Two different events with one
seqare refusedseq_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 lifecycledetailand atake_commitare carried, not interpreted, asdraft-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
decisionthat 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 |