WAI Extension: Uncompressed Essence Contribution
Mirrored from the canonical text at commit c5e7133e ().
Status: Draft. Carries a production’s uncompressed IP essence into a room through the ingest gateway of live-ingest: the video, the PCM sound and the ancillary data of a SMPTE ST 2110 contribution, each an RTP stream a description (SDP) names. This text says how each stream is read and what it is held to (its description, its sequence numbers, its timestamps), what a room receives of it, what is not carried, and every reason a description, a packet or a session is refused for. A whole video frame is encoded onto the video floor and published as
wai.video.av1; sound is published aswai.audio.flac; an SCTE 104 message in the ancillary data becomes the room’s signed cues; and the gateway signs, in its session record, a statement of each stream with every loss counted. It registers no capability and no record. No new transport: WAI defines none by charter. The reference implementation isjoule-moq-ingest(modulest2110; the session behind itsst2110feature) injoule-code/joule-code-rs/crates/; the corpus islive-conformance/st2110/, and its second implementation islive-conformance/st2110_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision, each whole unless a clause is named: RFC 3550 §5; RFC 4175; RFC 3190 §4; RFC 3551 §4.5.11; RFC 8331; RFC 8866; RFC 7273 §4 and §5; RFC 4570 §3; RFC 9639 §8.2 and §9; SMPTE ST 2110-10:2022; ST 2110-20:2022; ST 2110-30:2025; ST 2110-40:2023; ST 291-1:2011 (the ancillary data packet’s words and checksum); ST 2010:2008; and, for §9 alone, ST 2059-1:2021 (the epoch and the alignment of signals to it). The SMPTE documents were read as their publisher serves them at
pub.smpte.orgon 11 November 2026. A section number of one of those texts here is that revision’s. §3 says what was not read.
1. Scope
In scope: reading a description of streams (§4); the RTP header and the sequence a stream is held to (§5); video rows into frames (§6), PCM into blocks (§7) and ancillary data packets into records, with the SCTE 104 messages they carry (§8); what a description says of its clocks, and what a receiver with no reference clock may say of them (§9); a session that puts the streams into a room’s tracks (§10); and the reasons (§11).
Out of scope: sending essence; a reference clock, and any lock to one; the traffic shaping and delivery timing of a sender; redundant legs; and everything a room, a grant, a track, a receipt and a catalog are, which live-rooms, live-ingest and moq-streaming-format define and this text uses unchanged.
Relationship to the core spec: this extension names no payload format. Its objects are
wai.video.av1 and wai.audio.flac envelopes as SPEC §5 frames them, and its
receipts are the first hop’s, as live-ingest §8 states them.
2. Terms
- Description: an SDP object (RFC 8866) naming one or more streams.
- Stream: one RTP stream, one media section of a description: video, sound or ancillary data.
- Reader: what turns a stream’s packets into frames, blocks or records and holds the stream to its description.
- Frame: the samples of one picture. Block: the sample frames of one packet of sound. Record: one ancillary data packet.
- Mark: something a reader says of a frame, a block or a stream that it kept, as distinct from a refusal.
- Statement: what the gateway signs about one stream (§10.7).
3. Texts, and what this revision does not read or do
Every rule of §4 to §9 is a rule of a text the status note lists, or says that it is this extension’s own. Where this text and one of those disagree on a format, that text governs the format and this text is in error.
Not read, and so not relied on: AES67, which ST 2110-30 builds on (§7 holds sound to the
RFCs’ payload formats and to a description’s own packet time, and to nothing AES67 alone
states); ST 2110-21 beyond the names of its sender types; ST 2059-2 (the PTP profile);
IEEE 1588; ST 2022-7; ST 334 and CTA-708 (captions in ancillary data); ST 12-2 (time code
in ancillary data); ST 352 (the payload identifier a description’s VPID_Code quotes).
Not done, each refused or counted where it arises, never read as something else:
| what | where it shows |
|---|---|
| a sender’s traffic shaping and delivery timing (ST 2110-21: narrow, narrow linear and wide senders) | a description’s TP is kept and stated as not checked (§10.7) |
| lock to a reference clock; traceability of a clock | §9: stated as the description states it, never as held |
| interlaced and segmented scanning | sdp_scan_unsupported |
| constant-luminance, ICtCp, XYZ and key samplings; 16-bit floating point samples | sdp_sampling_unsupported, sdp_depth_unsupported |
| redundant legs (ST 2022-7) | sdp_redundancy_unsupported |
| compressed video (ST 2110-22), AES3 transport (ST 2110-31) and any other subtype | sdp_media_unsupported |
| decoding captions and time code out of their ancillary data packets | counted as not carried, by DID and SDID (§10.6) |
| RTCP | ignored where it arrives |
4. The description
4.1 Lines
A description is UTF-8 text of lines ending in CRLF or LF, each <letter>=<value> (RFC 8866
§5). It MUST have v=0, an o= and an s= line (sdp_malformed otherwise) and at
least one m= line (sdp_no_media). Lines before the first m= are the session’s; the
lines after an m= are that stream’s. A session whose attributes hold a=group:DUP (ST
2110-10 §8.5) is sdp_redundancy_unsupported.
For each m=<media> <port> <proto> <fmt> line: fewer than four fields, or a port, a format
or its rtpmap’s clock rate that is not a decimal number, is sdp_malformed; a protocol other than RTP/AVP, or
more than one format, is sdp_media_unsupported. The stream’s payload type is its format.
Its a=rtpmap:<pt> <name>/<rate>[/<channels>] names it; none for its payload type is
sdp_rtpmap_missing. Its a=fmtp:<pt> parameters are <name>=<value> pairs and standalone
names separated by ; (ST 2110-20 §7.1); an empty name or value is sdp_fmtp_value.
The media and the name decide the reader: video and raw (§4.2), audio and L16 or
L24 (§4.3), video and smpte291 (§4.4); any other pair is sdp_media_unsupported.
Then, in this order: a payload type outside 96 to 127 (ST 2110-10 §6.2) is
sdp_payload_type; a stream with no connection address of its own or the session’s (c=IN IP4 <address> or IP6, RFC 8866 §5.7) is sdp_connection; TSMODE other than NEW,
SAMP or PRES, and a TSDELAY or MAXUDP that is no decimal number, are
sdp_fmtp_value.
An inclusive a=source-filter (RFC 4570 §3) names the stream’s source address. a=mid,
TP, MAXUDP and a sound stream’s channel-order are kept as written.
4.2 Video
The clock rate MUST be 90000 (ST 2110-20 §7.1; sdp_clock_rate). The parameters
sampling, depth, width, height, exactframerate, colorimetry, PM and SSN are
required (ST 2110-20 §7.2); one absent is sdp_fmtp_missing.
interlaceorsegmentedpresent:sdp_scan_unsupported.sampling:YCbCr-4:4:4,YCbCr-4:2:2,YCbCr-4:2:0andRGBare read. The other values of ST 2110-20 §7.4.1 aresdp_sampling_unsupported; any other value issdp_fmtp_value.depth: 8, 10, 12 and 16 are read;16fissdp_depth_unsupported; any other value, and 16 with 4:2:0 (for which ST 2110-20 Table 3 builds no pgroup), issdp_fmtp_value.width,height: 1 to 32767; otherwisesdp_fmtp_value.exactframerate: one decimal number, or two joined by/with the second above 1 and no common factor, the smallest numerator there is (ST 2110-20 §7.2); otherwisesdp_fmtp_value.PM:2110GPMor2110BPM; otherwisesdp_fmtp_value.
TCS is SDR and RANGE is NARROW where a description states none (ST 2110-20 §7.3,
§7.6).
4.3 Sound
L16 (RFC 3551 §4.5.11) and L24 (RFC 3190 §4). The clock rate, which is the sampling rate
(ST 2110-30 §6.1), is 44100, 48000 or 96000 (sdp_clock_rate otherwise). The channel count
is 1 where rtpmap gives none, and 1 to 64 (sdp_channels otherwise). a=ptime is a
decimal number of milliseconds with at most three fractional digits, above 0 and at most
1000 (sdp_fmtp_value otherwise). Where the packet time is a whole number of sample frames
at the sampling rate, that number is the stream’s sample frames a packet; a description
with no ptime, or one that is not a whole number of them, states none.
4.4 Ancillary data
The clock rate MUST be 90000 (ST 2110-40 §5.3; sdp_clock_rate). Each
DID_SDID={0x..,0x..} (RFC 8331 §4) and a VPID_Code of 0 to 255 are kept; a malformed one
is sdp_fmtp_value.
5. RTP and the sequence
A packet is read as RFC 3550 §5.1 lays it out: fewer than 12 octets, or a CSRC list or a
header extension (§5.3.1) that runs past its end, is rtp_short; a version other than 2 is
rtp_version. A header extension is passed over (ST 2110-10 §6.2). Where the padding bit is
set, the packet’s last octet counts the padding: 0, or more than the payload holds, is
rtp_padding. A payload type that is not the stream’s is rtp_payload_type. The first
packet read fixes the stream’s SSRC; a packet of another is ssrc_changed.
The sequence. A video or an ancillary data stream’s sequence number is 32 bits: the header’s 16, below the 16 its payload header begins with (RFC 4175 §4.1; RFC 8331 §2.1). A sound stream’s is the header’s 16, extended by its distance from the number expected: within half its space ahead, it is ahead. The first packet read sets the number expected to its own plus one. For each later packet, after the checks above and before any other:
- the number expected: the number expected advances by one;
- ahead of it: every number skipped is counted lost and remembered as missing (the newest 4096 of them), and the number expected becomes the packet’s plus one;
- behind it and remembered as missing: it is no longer lost; it is counted behind, and
the stream’s format says whether it is kept (§6.3) or refused
late(§7, §8.1); - anything else behind it: refused
duplicate.
A packet refused before its sequence number is read is not counted in the sequence, and the packet after it then shows it as lost.
6. Video rows into frames
6.1 Pgroups
Samples are packed most significant bit first, each depth bits, in the order ST 2110-20
§6.2 gives (Tables 1 to 3), which RFC 4175 §4.3 gives too:
| sampling | one cluster of samples | spans |
|---|---|---|
YCbCr-4:4:4 | Cb, Y, Cr | one pixel |
RGB | R, G, B | one pixel |
YCbCr-4:2:2 | Cb, Y0, Cr, Y1 | two pixels of a row |
YCbCr-4:2:0 | Y00, Y01, Y10, Y11, Cb, Cr | two pixels of each of two rows |
A pgroup is the fewest clusters that make whole octets: one at 8 and 16 bits; at 10
bits four (4:4:4, RGB), one (4:2:2) or two (4:2:0); at 12 bits two (4:4:4, RGB) or one. A
frame’s samples are three planes: luma (or red) at width × height, and two more at the
same size (4:4:4, RGB), half the width rounded up (4:2:2), or half of each rounded up
(4:2:0).
6.2 A packet
After §5, in this order:
- A payload of fewer than 2 octets is
srd_truncated. The sequence number is read (§5): aduplicateis refused; a packet behind is kept only if the frame of its timestamp is still open, and refusedlateotherwise. - The frame (§6.3): a packet that is not behind and whose timestamp is not the open
frame’s ends that frame. A packet whose timestamp is that of the frame ended last is
after_marker. - The sample row data headers (ST 2110-20 §6.1.4), six octets each: a length, the field
bit and a 15-bit row, the continuation bit and a 15-bit offset. Fewer than six octets
left for a header is
srd_truncated; a fourth header issrd_count; a field bit of 1 isfield_in_progressive. A length of 0 is allowed in a packet’s only header, which then has no data, and issrd_lengthbeside another header. Data shorter than the headers’ lengths together issrd_truncated. - Each segment, in order: a row not below
heightisrow_out_of_range; in 4:2:0 an odd row issrd_offset; an offset not belowwidthisoffset_out_of_range. Then its place: a length that is not a whole number of pgroups issrd_length; an offset that is not a multiple of a pgroup’s pixels issrd_offset; an end (the offset plus the pgroups’ pixels) pastwidthrounded up to a whole pgroup isoffset_out_of_range. Then, where no packet was lost or refused since the segment before it, the segment MUST start where that one ended (the next pixel of its row; or, where it reachedwidth, the first pixel of the next row or row pair; a frame’s first segment at row 0, offset 0 where the frame before it arrived whole): one that starts earlier issrd_order, one that starts laterformat_mismatch. That rule is this extension’s: ST 2110-20 §6.1.4 and §6.1.5 say rows and offsets only increase, and a sender that leaves samples out with nothing lost has described another picture than it sends. - Depth. A segment refused in step 4 for
srd_length, for its end, or forformat_mismatch, whose place holds entirely at another depth the sampling has a pgroup for (its length, its offset, its end, and its start against the segment before it read at that depth), is refuseddepth_mismatchinstead: the packets are of a depth the description does not state.
A refused packet contributes no sample, and the packet after it is held to no place; nor
is the first packet after a frame that no marker ended. An
accepted packet’s samples are placed; samples a segment holds past width are fill and are
not read (ST 2110-20 §6.2.1).
6.3 Frames
A frame is the packets of one RTP timestamp. It ends at a packet with the marker bit (ST
2110-20 §6.1.2), or, with the mark marker_missing, when a packet of another timestamp
arrives or the stream ends. A packet kept from behind (§5) gives its frame the mark
reordered.
A frame is complete when every sample of it arrived. A frame that is not complete says so, and lists every row with a sample missing; a missing sample is not replaced, repeated or interpolated. What a receiver does with an incomplete frame is §10.3.
7. PCM into blocks
Each accepted packet is one block: its payload’s samples, signed two’s complement, most
significant octet first, the channels of a sample frame interleaved, at the RTP timestamp of
its first sample frame. After §5, in this order: the sequence (duplicate; a packet behind
is late, since blocks leave in order); a payload that is empty or not a whole number of
sample frames is depth_mismatch where it is a whole number of them at the other of the two
depths, and pcm_length otherwise; where the description states the sample frames a packet
(§4.3) and the packet holds another number, depth_mismatch where its octets hold the
stated number at the other depth, and ptime_mismatch otherwise.
A block that does not start at the timestamp where the block before it ended carries the
mark timestamp_jump; where it starts later, the sample frames between are counted
missing. No sample is made for them.
8. Ancillary data
8.1 Records
A payload (RFC 8331 §2.1) is the 16 high bits of the sequence number, Length, ANC_Count,
the two bits F, and 22 reserved bits; then each ancillary data packet: C (1 bit),
Line_Number (11), Horizontal_Offset (12), S (1), StreamNum (7), the 10-bit words
DID, SDID, Data_Count, as many user data words as Data_Count’s low 8 bits say, and
Checksum_Word, then zero bits to the next 32-bit word.
After §5, in this order: a payload of fewer than 8 octets is anc_truncated; the sequence
(duplicate, late); F of 0b01 is anc_field; a count of 0 beside a length that is
not 0 is anc_count; a payload whose octets after the header are not Length is
anc_length. Then each packet: data that ends inside it is anc_truncated; a Data_Count
whose bit 8 is not the even parity of bits 7 to 0, or whose bit 9 is not the inverse of bit
8 (ST 291-1), is anc_parity, since the count is then no count. Packets that end before
Length does, or after, are anc_length. Each of these refuses the whole payload.
One ancillary data packet is dropped, and the others of its payload kept, where its
DID or SDID fails that parity (anc_parity) or its Checksum_Word is not the nine low
bits of the sum of the nine low bits of every word from DID to the last user data word,
with bit 9 the inverse of bit 8 (anc_checksum). Every other packet is a record: the
payload’s timestamp and F, its five location fields, its DID and SDID (their low 8
bits) and its user data words whole.
8.2 SCTE 104 messages
A record of DID 0x41 and SDID 0x07 carries an SCTE 104 message (ST 2010 §5). Each user
data word holds one octet in bits 7 to 0 under the parity of §8.1; a word that fails it
refuses the record, scte104_parity. The first octet is the payload descriptor: bits 7 to 5
zero and bits 4 and 3 01, or scte104_version; bit 2 says the message continues in the
next packet, bit 1 that this packet continues one, bit 0 that the message repeats one of an
earlier frame. A packet that continues nothing, a first packet while a message is still
open, and a message still open when a record of another timestamp arrives or the stream
ends, are scte104_sequence. A whole message’s length MUST be the messageSize its
third and fourth octets state (scte104_size).
9. Clocks
What a description says of a stream’s clock is read from a=ts-refclk (RFC 7273 §4; ST
2110-10 §8.2) and a=mediaclk (RFC 7273 §5; ST 2110-10 §8.3), the stream’s own or the
session’s, and is one of four classes:
| class | the description states |
|---|---|
ptp_traceable_stated | ts-refclk:ptp=<version>:traceable and mediaclk:direct=0 |
ptp_grandmaster_stated | ts-refclk:ptp=<version>:<EUI-64>[:<domain>] and mediaclk:direct=0 |
not_traceable | any other pair: localmac=, another source, mediaclk:sender, an offset that is not 0, a rate beside the offset |
unstated | either attribute absent |
TSMODE (ST 2110-10 §8.7) says what a timestamp is a time of: SAMP the sampling instant,
PRES a timestamp kept from an input that did not say, NEW one made at the sender’s
egress; a description that states none means NEW.
A class is a statement of the description. A receiver that holds no reference clock
MUST NOT state that a stream is locked, that its clock is traceable, or that a timestamp
is a sampling instant: it states the class and the mode as what the description says. It
MUST state a stream of class not_traceable or unstated as such wherever it states
anything of the stream’s time.
Reading an instant from a timestamp. For the two ptp_ classes, ST 2110-10 §7.3 makes
an RTP timestamp the count of the RTP clock since the reference clock’s epoch, modulo 2^32,
and ST 2059-1 puts that epoch at 1970-01-01T00:00:00 on the PTP timescale. A receiver
MAY read an instant from a timestamp: the instant nearest its own clock’s whose count is
the timestamp. It needs the seconds the PTP timescale is ahead of UTC, which it is given (37
since 2017) and does not learn from the stream. The result is the description’s claim
evaluated, not a measurement: a receiver MUST check it against its own clock and state
it only where the two agree within a bound it states (§10.5). For the other two classes no
instant is read from a timestamp.
10. Into a room
10.1 Admission
A publisher POSTs /st2110 with its room grant as the bearer token and the proof of its key,
admitted as live-ingest §3 admits a WHIP publisher, with a JSON body:
| member | meaning |
|---|---|
sdp | the description, as a string. REQUIRED |
source | the digital source type the publisher states for the essence: digitalCapture, computationalCapture, screenCapture or digitalCreation (synthesis §3.2). OPTIONAL |
The gateway reads the description (§4); a refusal of §4 is answered 422 with its reason.
It then refuses what it does not carry (§10.8), binds a UDP socket for each stream (§10.2),
and answers 201 with Location: /st2110/s/<session> and a JSON object: session; for each
stream its index, its media, the port to send it to, its clock class, its TSMODE and
the receive buffer its socket was given (§10.9); whether an origin claim will be signed;
and, where there is a video stream, encoder: which encoder will encode it, the one that
was asked for, and why the two differ where they do (§10.3). The session’s resource answers its own token:
DELETE ends the session.
10.2 Streams
- Unicast. A stream whose destination is not a multicast address is received at a port the gateway chooses and states; the description’s own port is not used.
- Multicast. A stream whose destination is a multicast address is received at the
description’s port, with the group joined, only where the gateway’s configuration lets it
join groups; otherwise the session is refused
multicast_not_permitted. - The source. A stream’s source is the address its description’s source filter names,
or else the address of its first datagram. A datagram from another address is counted
(
from_other_sources) and not read. - A session ends when its resource is deleted, when no datagram has come for the gateway’s
idle time (
idle), when none ever came within the session’s time (no_stream), when the relay is lost, or when the gateway stops.
10.3 Video
A complete frame (§6.3) is encoded as AV1 in the Main profile at the Main tier, at level
4.1 or below (the video floor, SPEC §5), and published as one wai.video.av1
object, a group at each keyframe, exactly as live-ingest §5 publishes a frame. The Main
profile is 4:2:0: a 4:2:2 frame’s two chroma planes are halved in height first, each output
row the mean, rounded half up, of the two rows it replaces (the last row alone where the
height is odd). An 8-bit frame is encoded at 8 bits and a 10-bit frame at 10.
The encoder is held to the stream’s frame rate. A contribution is live: an encoder that does not keep the stream’s rate is not carrying it. A gateway:
- encodes off the receive path, on threads of its own, behind a queue of a stated, bounded number of frames (§10.9);
- runs its encoder with no reordering and no lookahead past the frame in hand, so that each frame’s temporal unit leaves before the next frame’s, and asks for a keyframe at a configured interval of the stream’s own timestamps;
- times every frame against the frame interval, and lowers quality before it drops a frame: where the encoder has faster settings, the gateway moves to one when frames take most of their interval, when frames wait, or when a single frame (a keyframe, as a rule) takes more than half of the time its queue absorbs; and back only after the stream has been held with room to spare in both, a keyframe among the frames seen. The mean alone is not enough: a setting whose ordinary frames fit and whose keyframes fill the queue drops the frames behind each keyframe;
- drops by one stated policy when the fastest setting still does not hold the rate: a frame that arrives while the queue is full is not encoded. Frames already waiting keep their turn, so what is published stays in order and no older than the queue. An encoder is run with no frame dropping of its own: nothing is dropped where it is not counted.
Which encoder. A gateway may hold more than one encoder onto the floor (a GPU’s AV1 encode engine through the operating system’s own interface, a software encoder built for real time, a slower one that needs nothing but the gateway’s own build), and takes one by configuration. One asked for by name that the host does not have is a refusal to start, with the reason; or, only where the operator has said so, the next one that is present, stated at start, in the session’s answer (§10.1) and in its record (§10.7). An encoder is never used in another’s name.
The floor is checked, not assumed. Before it is used, an encoder encodes pictures the
gateway computes, at each depth, and the sequence header of what it makes is read: the
Main profile, the Main tier, seq_level_idx 9 or below, the picture’s size as its largest
frame. One whose stream is outside the bound is refused with what was found, whatever it
is and however fast. Every keyframe of every session is held to the same reading before it
is published: a session whose encoder leaves the floor ends encoder_outside_floor, and
that unit is not published.
The picture’s size. An encode engine may code its frames a few rows or columns larger than the picture, to its own block alignment (one measured codes 1082 rows for a picture of 1080). Such a stream is within the level, and it is the stream’s picture only where it says so: the gateway requires every keyframe to state the picture’s size as its render size (AV1 §5.9.6) and to be coded at that size or less than 64 samples more a side, refuses the encoder for the stream otherwise, and states both sizes in the session’s record. The track’s framing states the size the frames are coded at, as for any publisher’s stream.
Two things are never published, and each is counted in the session’s record (§10.7):
- a frame that is not complete (
frames_incomplete_not_published): the gateway draws nothing in place of a sample that did not arrive; - a frame that found the encoder’s queue full (
frames_encoder_behind_not_published): an encoder slower than the stream at its fastest setting leaves frames out, and says how many.
A sink sees either as a longer time between two objects (§10.5). The AV1 a session writes is lossy, and is lossier where the gateway lowered quality to hold the rate; nothing here says how lossy, and a receipt attests the object published, not the essence.
10.4 Sound
Blocks are gathered into objects of wai.audio.flac: each a FLAC stream (RFC 9639) of one
frame whose subframes are verbatim (§9.2.4 there), so every sample is the sample that
arrived, at its depth. An object ends when it holds the gateway’s object duration (20 ms by
default) and before a block marked timestamp_jump: an object never spans samples that did
not arrive, and none is made for them. One to eight channels are carried (§8.2 there).
10.5 Time
- Media time. An object’s
live.rtp(live-ingest §6) is the RTP timestamp of its frame, or of its first sample frame, extended past its 32 bits;live.rateis the stream’s RTP clock rate;live.tis live-ingest §7’s mapping of it to the room clock. - Capture instant. Each object carries, inside its Immutable Properties, the
CaptureTimestamp of playout timing, and beside them the gateway’s clock as the first
HopTime (moq-streaming-format §4.4; co-watch §15). The capture instant is
the one §9 reads from the object’s RTP timestamp, where the stream’s class is one of the
two
ptp_classes and the instant lies within the gateway’s bound (1 s by default) of the gateway’s own clock at the packet’s arrival; otherwise it is that arrival. - The catalog states
wai_tempoon each media track (moq-streaming-format §4.4), itsmax_delay_msandplayout_delay_msthe gateway’s configured delay. - What is stated. The session’s record gives, for each stream, how many capture instants were read from a timestamp, how many are arrivals, and how many timestamps disagreed with the gateway’s clock past the bound; and it says that the gateway holds no reference clock. A CaptureTimestamp read from a timestamp is the sender’s statement, checked against the gateway’s own clock to within the bound and no closer.
10.6 Ancillary data
- SCTE 104. A message of §8.2 is handed to the room’s injector
(cue-injection §10) as a message an API connection delivered, with no
response; its arrival is the instant it was completed. The injector translates it and
signs the room’s
cueevents as cue-injection §6 and §7 say. A message that names a single operation asks nothing of the room’s cues and is not injected. Where the gateway runs no injector for the room, the message is countedno_injector. - Everything else is counted as not carried, by its
DIDandSDID: captions (ST 334) and time code (ST 12-2) among them, whose packets this revision does not decode (§3). A room has a form for captions (timed-text) and none for time code.
10.7 What the gateway signs
Receipts. Every object’s receipt, as live-ingest §8 states them: the first hop’s.
The session record (live-ingest §16), when the session ends, with protocol st2110. Its
detail holds, beside the track counts of every ingest: the description’s digest (SHA-256
of its octets); for each stream its statement and the statement’s digest, its packets, the
sequence numbers lost and arrived behind, the packets refused by reason (§11), the
datagrams from other sources, what its reader made (frames and incomplete frames; blocks,
jumps and sample frames missing; records and packets dropped by reason), what was not
published by reason (§10.3), the receive buffer its socket was given, and where its capture
instants came from (§10.5); the ancillary data not carried, by DID and SDID; the SCTE
104 messages by outcome; the datagrams dropped because the session’s queue was full; the
origin claims; and encoder: which encoder encoded (backend, its version, whether a
hardware engine, and where it was loaded from), the encoder the configuration asked for,
fallback (why the two differ, or null), and what its stage counted: the picture’s size
and the size its frames were coded at, the frames offered,
encoded and not encoded because the queue was full, the drop policy in words, the queue’s
length and the most it held, the keyframes and octets made, the mean and the longest time
a frame took against the frame interval, and every move between quality settings with the
frame it was made at and why. Every frame offered is encoded or counted there. The counts
are integers; no rate is stated.
A stream’s statement is a JSON object with exactly these members: kind
("wai-essence-statement"), description (the description’s digest), stream (its
index), media, format (the parameters of §4.2, §4.3 or §4.4 as read), payload_type,
destination, source (the address and port its datagrams came from, or null), ssrc,
clock (§9’s class), tsmode, sender_type (the description’s TP, or null),
sender_type_checked ("no"), and declared_source (§10.1’s source, or null). Its
digest is SHA-256 over its RFC 8785 form.
The origin claim. Where the publisher stated a source, the gateway signs, for each
media track, an origin claim (synthesis §3.1) whose subject is the track from its first
group, of step capture and class captured, source the stated one, operation
st2110:contribution, and record the digest of the stream’s statement. The gateway signs
as the encoder; that the essence is a capture is the publisher’s statement, made under its
grant, and the claim carries the gateway’s authority and no more (synthesis §3.1). With no
source stated, no claim is signed. The claims are in the session’s record; this revision
defines no carriage of them in the room.
10.8 Refusals of a session
A description that reads (§4) is refused, with the HTTP status shown, where the gateway does not carry it:
| reason | status | when |
|---|---|---|
essence_streams_unsupported | 422 | more than one video stream, one sound stream or four ancillary data streams |
essence_format_not_carried | 422 | video that is not 4:2:2 or 4:2:0 at 8 or 10 bits; sound of more than eight channels |
essence_outside_floor | 422 | a frame size or rate past level 4.1’s picture size or display rate |
essence_encoder_refused | 422 | the gateway’s encoder does not take the stream (its size or its depth), with the encoder’s reason |
publish_not_permitted | 403 | the grant does not let its principal publish a stream’s media kind |
multicast_not_permitted | 403 | a multicast destination where the gateway joins none |
session_conflict | 409 | the principal already contributes to the room |
body_malformed | 400 | no sdp, or a source that is not one of the four |
10.9 Bounds
A session holds one bounded queue of datagrams and one bounded queue of frames for its encoder; a datagram or a frame that finds its queue full is dropped and counted, and never waits. A stream’s socket is asked for a receive buffer that holds more than an instant of it, and the session states what the host gave: a high-definition stream is of the order of a hundred thousand datagrams a second, and a reader scheduled a moment late loses to a small buffer what no later stage can count as anything but an incomplete frame. Sessions are bounded as live-contribution §8 bounds contribution sessions. Uncompressed video is of the order of a gigabit a second at high definition (RFC 4175 §8): a gateway’s host, its network path and its encoder each bound what a session can carry, and a throughput figure is a dated measurement of one host, never a property of this extension.
11. Reasons
Refusals of a description (§4):
| reason | when |
|---|---|
sdp_malformed | not lines of <letter>=<value>; no v=0, o= or s=; an m= line that does not read |
sdp_no_media | no m= line |
sdp_media_unsupported | a protocol, a number of formats or a media subtype this text does not read |
sdp_rtpmap_missing | no rtpmap for the payload type |
sdp_payload_type | a payload type outside 96 to 127 |
sdp_clock_rate | a clock rate the format does not have |
sdp_fmtp_missing | a required format parameter absent |
sdp_fmtp_value | a parameter value its text does not allow |
sdp_sampling_unsupported | a sampling of ST 2110-20 this text does not read |
sdp_depth_unsupported | 16-bit floating point samples |
sdp_scan_unsupported | interlaced or segmented scanning |
sdp_redundancy_unsupported | redundant legs |
sdp_channels | a channel count outside 1 to 64 |
sdp_connection | no connection address |
Refusals of a packet of any stream (§5):
| reason | when |
|---|---|
rtp_short | fewer octets than its header says |
rtp_version | a version other than 2 |
rtp_padding | a padding count of 0 or past the payload |
rtp_payload_type | a payload type that is not the stream’s |
ssrc_changed | an SSRC that is not the stream’s |
duplicate | a sequence number seen before |
late | behind the highest sequence number seen, where the stream no longer takes it |
Refusals of a packet of video (§6.2):
| reason | when |
|---|---|
srd_truncated | shorter than its row data headers or their lengths |
srd_count | a fourth row data header |
srd_length | a length that is no whole number of pgroups; a length of 0 beside another header |
srd_offset | an offset inside a pgroup; an odd row in 4:2:0 |
srd_order | with nothing lost, a segment that starts before the one before it ended |
row_out_of_range | a row not in the picture |
offset_out_of_range | a segment that starts or ends past its row |
field_in_progressive | the field bit set |
after_marker | the timestamp of a frame the marker already ended |
depth_mismatch | packets of a depth the description does not state (video, §6.2 step 5; sound, §7) |
format_mismatch | with nothing lost, a segment that starts after the one before it ended |
Refusals of a packet of sound (§7):
| reason | when |
|---|---|
pcm_length | a payload that is no whole number of sample frames |
ptime_mismatch | a packet of another number of sample frames than the description’s packet time |
Refusals of a payload of ancillary data, and of one packet in it (§8.1):
| reason | when |
|---|---|
anc_truncated | data that ends inside the header or a packet |
anc_length | Length that is not the octets the payload holds, or not the octets its packets take |
anc_count | a count of 0 beside a length |
anc_field | F of 0b01 |
anc_parity | a Data_Count whose parity fails (the payload); a DID or SDID whose parity fails (the packet) |
anc_checksum | a checksum word that is not the words’ |
Refusals of an SCTE 104 message in ancillary data (§8.2):
| reason | when |
|---|---|
scte104_parity | a user data word whose parity fails |
scte104_version | a payload descriptor of another mapping version |
scte104_sequence | a continuation of nothing; a message begun while another is open; a message left open at its frame’s end |
scte104_size | a message that is not as long as it states |
Marks (§6.3, §7): the frame or the block is kept, and says so.
| reason | when |
|---|---|
reordered | a frame that took a packet from behind |
marker_missing | a frame no marker bit ended |
timestamp_jump | a block that does not start where the one before it ended |
12. Status and conformance
| part | status | where |
|---|---|---|
| the readers (§4 to §9) | implemented, with a reference and a second implementation | joule-moq-ingest st2110; live-conformance/st2110/ and st2110_verify.py |
| the session (§10) | implemented | joule-moq-ingest st2110::session, feature st2110; tests/essence_st2110.rs |
| the encode stage: its queue, its real-time budget, its drop policy and counts, the choice of an encoder and its probe (§10.3) | implemented | st2110::encode; its unit tests, and tests/essence_st2110.rs for a named encoder that is absent |
| the sample loops held to a sample-at-a-time reference (§6.1, §10.3) | implemented | st2110::convert |
| a software encoder built for real time, loaded at run time (§10.3) | implemented; measured on two hosts | st2110::encode::svt; tests/essence_rt.rs |
| a GPU’s AV1 encode engine through the Video Acceleration API (§10.3) | implemented; measured on the one host that has such an engine | st2110::encode::va, st2110::encode::obu; tests/essence_rt.rs |
| hardware AV1 encode on the aarch64 build host and on macOS | none found: the first’s V4L2 encode node enumerates H.264 and H.265 only, and the second’s media framework lists no AV1 encoder (11 November 2026) | decisions D22 |
| SCTE 104 in ancillary data to the room’s cues (§10.6) | implemented | st2110::anc, and the injector of cue-injection |
| captions and time code in ancillary data | not decoded; counted as not carried | §10.6 |
| multicast | a group is joined where the configuration lets it; tested as far as a join on a loopback interface | §10.2 |
| carriage of origin claims in a room | not defined | §10.7 |
The corpus. live-conformance/st2110/vectors.json holds descriptions and the RTP
packets of their streams, written by the reference’s own sender from pictures and sound it
computes from integers (no outside media), with what a reader makes of each: every frame,
block and record with the SHA-256 of its samples, every refusal with its reason, every mark,
every count. It covers each sampling at each depth with fill; sequence numbers and
timestamps across their wraps; and one planted fault a vector, among them a reordered
packet, a lost row, a row past the picture’s height, packets of a depth the description
does not state, a marker bit on the wrong packet, an ancillary data packet with a bad
checksum, and a description whose width disagrees with its packets. Every reason of §11 is
shown by a vector that names it. joule-moq-ingest-st2110-vectors --check regenerates it
byte for byte; st2110_verify.py, written from this text and the texts it names and sharing
no code with the reference, recomputes every expectation, then leaves out each of its own
checks in turn and requires the corpus to catch the absence, and alters the corpus one
stated value at a time and requires every altered copy to fail.
The session’s test runs on loopback with the reference’s own sender: a 4:2:2 10-bit video stream, two channels of 24-bit sound and an ancillary data stream, with one video packet and one sound packet never sent. It holds that every published object verifies against the gateway’s receipts at a relay subscriber and at the relay’s door; that every keyframe’s object is within the video floor; that the sound decodes to the samples sent, bit for bit, with the lost packet’s sample frames counted and none made; that every frame is published or counted with its reason; that each object states its capture instant and the catalog its playout timing; that the SCTE 104 message became the room’s signed cues; and that the record’s statements, counts and origin claims are as §10.7 says. The sender there holds no reference clock: its description’s clock lines are a fixture for the path of §9, not a statement about a host.
The session held to its frame rate. tests/essence_rt.rs drives the same session with
the reference’s own sender pacing whole 4:2:2 10-bit frames over loopback: 1280 × 720 and
then 1920 × 1080 at 30 frames a second, 20 seconds each, with every real-time encoder the
host has. It fails unless every frame sent reached the encode stage whole, the stage
dropped none, every frame was published in order with its own timestamp and verified
against the gateway’s receipts, every keyframe is within the video floor and states the
stream’s picture, and every temporal unit decodes with a conforming decoder (the host’s
libdav1d) to a picture of the stream’s depth whose luma is the luma sent, within a stated
distance. The first frames are decoded a second time by this repository’s own decoder
(wai-av1, written from the specification’s text) and compared sample for sample with the
first decode; what it says is stated. The test reads the host’s load first and refuses to
measure on a loaded host, saying so. It is run on request, on a host that has a real-time
encoder: no continuous-integration job runs it, and none could on a shared runner.
What holds a frame rate, dated. Every figure here is one host’s on one day, with the load that host carried, and none is a property of this extension.
On 11 November 2026, on a 32-thread x86-64 build host whose GPU’s open driver exposes AV1 encode, shared with other work. The host was never quiet: given seven minutes, the test refused to measure (a one-minute load of 14 to 18), as it is written to. The figures below were then taken with its load check overridden by hand and are stated with the load they were taken under: what was held on that host while it carried that load, not a measurement of it idle. 4:2:2 at 10 bits in, 20 seconds a run; every frame published was verified, and every temporal unit decoded by libdav1d 1.5.3 to the picture sent.
| encoder | stream | pictures | published | dropped behind the encoder | a frame took: mean, longest | one-minute load |
|---|---|---|---|---|---|---|
| the GPU’s encode engine | 1280 × 720 at 30 | computed | 600 of 600 | 0 | 2.7 ms, 8.8 ms | 18 |
| 1920 × 1080 at 30 | computed | 600 of 600 | 0 | 4.7 ms, 22.6 ms | 18 | |
| 1280 × 720 at 30 | recorded | 600 of 600 | 0 | 2.6 ms, 10.1 ms | 17 | |
| 1920 × 1080 at 30 | recorded | 600 of 600 | 0 | 4.9 ms, 20.4 ms | 17 to 21 | |
| 1920 × 1080 at 60 | computed | 1200 of 1200 | 0 | 4.5 ms, 20.6 ms | 16 to 20 | |
| 1920 × 1080 at 60 | recorded | 1200 of 1200 | 0 | 4.5 ms, 20.1 ms | 17 to 18 | |
| the software encoder built for real time | 1280 × 720 at 30 | computed | 600 of 600 | 0 | 9.6 ms, 51.9 ms | 17 to 19 |
| 1920 × 1080 at 30 | computed | 600 of 600 | 0 | 10.3 ms, 60.4 ms | 18 to 19 | |
| 1280 × 720 at 30 | recorded | 600 of 600 | 0 | 6.7 ms, 71.6 ms | 20 to 21 | |
| 1920 × 1080 at 30 | recorded | 600 of 600 | 0 | 6.8 ms, 83.5 ms | 18 to 20 | |
| 1920 × 1080 at 60 | computed | 1198 of 1200 | 2 | 8.4 ms, 78.7 ms | 16 to 17 | |
| the fallback (10 seconds) | 1280 × 720 at 30 | computed | 247 of 300 | 53 | 39.8 ms, 160.2 ms | 18 |
- Computed pictures are a panning texture with noise, which no encoder finds easy. Recorded pictures are the first 48 of a 1920 × 1080 film the host already held, played forward and back, its centre for the smaller size, at 10 bits with its chroma rows doubled (Big Buck Bunny, © Blender Foundation, CC BY 3.0; the file is not in the repository).
- Both real-time encoders held 1280 × 720 and 1920 × 1080 at 30 frames a second on that host under that load, on both kinds of pictures. The engine held 1920 × 1080 at 60 as well.
- The software encoder at 1920 × 1080 and 60 frames a second is not claimed. In the set above it dropped 2 frames of 1200 behind a 78.7 ms frame, which is a failure; in a set taken a quarter of an hour before it published 1200 of 1200 on both kinds of pictures (9.8 ms and 6.5 ms a frame). On that host under that load it is at the edge, and what a gateway does there is what §10.3 says: it counts the two. Its run on recorded pictures at 60 in the later set measured nothing: the test’s own sender ran 32 ms behind, and the test says so.
- A failure this test found. The software encoder’s first run at 1920 × 1080 and 30 published 599 of 600: the stage had moved to a slower setting on the mean time alone, and one keyframe there filled the queue. The stage now also holds the longest frame against what the queue absorbs (§10.3), and the run held.
- The engine codes 1920 × 1080 as frames of 1920 × 1082 and states 1920 × 1080 (§10.3).
- The fallback holds no broadcast size at a broadcast rate: its gateway says so at start from its own timing (137 ms and 182 ms a 1280 × 720 and a 1920 × 1080 picture on that host that day), and counted every frame it left out.
- The first eight frames of each run were decoded a second time by
wai-av1. It refuses these streams by default, as it refuses any tool no committed stream holds it to (10-bit samples first among them); asked to decode them anyway, it gave the same luma as libdav1d, sample for sample, in every run.
The same day, on a 14-thread aarch64 build host, no session was measured: under the load it carried when the test was run (a one-minute load of 25 to 32, the test’s check overridden by hand), the receive buffer the host gives a socket (425 984 octets for the 16 MiB asked, under 3 ms of a 1920 × 1080 stream) lost packets on the way in (228 of 242 100 at 1280 × 720, 16 938 of 544 350 at 1920 × 1080), which the test reports as that and not as an encoder’s doing. The software encoder alone, fed pictures as fast as it took them at a load of 10 to 14, took 13.2 ms a 1280 × 720 picture and 22.6 ms a 1920 × 1080 one (128 ms at longest); the fallback 149 ms and 311 ms. That host has no hardware AV1 encode (D22).
History. The session first landed, earlier on 11 November 2026, with the pure-Rust encoder on one thread and a reader that took samples a bit at a time: 5.0, 1.8 and 0.6 frames a second at 640 × 360, 1280 × 720 and 1920 × 1080 on the x86-64 host at a load of 31. Profiled there, a 1920 × 1080 frame took 882.9 ms, 839.2 of them in the encoder and 40.6 in the reader. Those figures describe nothing a gateway does now.
13. Security and privacy considerations
- A description is the publisher’s word about its streams, admitted under its grant. A reader holds the packets to it, and a mismatch is refused or marked; it cannot show that the essence is what the publisher says it is. The origin claim says so in its own terms (§10.7).
- RTP as ST 2110 uses it is unauthenticated and unencrypted: anyone on the path who can send to a stream’s port from its source address can inject packets. A gateway takes one source a stream (§10.2); a deployment keeps contribution traffic on a network it controls.
- A CaptureTimestamp read from an RTP timestamp is as good as the sender’s clock and the description’s word about it. The gateway’s check (§10.5) bounds it to within a second of the gateway’s own clock, which is far coarser than a production’s timing and is stated as what it is.
- Uncompressed video is a large, steady load. Every queue of a session is bounded, and what a full queue drops is counted (§10.9); a gateway bounds its sessions.
- A multicast join is off unless the configuration turns it on (§10.2): a description cannot make a gateway join a group.
- The session record names addresses of the contribution network (a stream’s source and destination). It goes to the gateway’s record sink, not to the room.