open-standards/wai/extensions/live-contribution.md
Transaction Science 36f4842a0f
Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 29s
wai-sink-android native sink library / does this change touch it (push) Successful in 32s
wai-sink-swift native sink package / does this change touch it (push) Successful in 34s
wai-quantum for JavaScript / does this change touch it (push) Successful in 34s
wai-fleet service / does this change touch it (push) Successful in 34s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m29s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m18s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m30s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m31s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m20s
WAI operation catalogue / does this change touch it (push) Successful in 1m22s
wai-ledger service / does this change touch it (push) Successful in 1m22s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 13s
WAI Studio / does this change touch it (push) Successful in 2m19s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 3m24s
wai-fleet service / service · linux (push) Successful in 2m46s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 42s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 7m2s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 5m31s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 6m27s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 8m14s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m37s
wai-audit collector and player integration / collector · linux (push) Successful in 3m29s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 10m5s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 10m3s
wai-ledger service / service · linux (push) Successful in 3m20s
wai-quantum studio and desktop / studio · browser (push) Successful in 7m58s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 2m54s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 10m7s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m39s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 50s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 13m15s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 11m31s
wai-fleet service / service · macos (push) Successful in 58s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 5m11s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m2s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m5s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 17m41s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 9m4s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 3m35s
wai-ledger service / service · macos (push) Successful in 1m8s
WAI Studio / Studio in WebKit · linux (push) Successful in 21m29s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 13m3s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 18m13s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 9m57s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 12m33s
Every MoQ catalog we write is MSF's, wai-link is defined, and the catalog check holds MSF's own rules
The deviations from MSF that draft-charlot-moq-wai-packaging-01 listed, and two more the
extended check found, fixed in every writer:

- version: each writer wrote MSF's root version as the number 1; MSF §5.1.1 requires a
  string, "draft-01" for draft-ietf-moq-msf-01, which wai-pack, joule-moq-ingest,
  joule-moq-remote, joule-moq-interop and joule-moq-web now write. No reader compared it.
  The readers of a room's catalogs (the room page, catalogVersionKnown; the gateway's WHEP
  session, catalog::version_known) now refuse a version they do not know (MSF §5.1.1),
  reading "draft-01" and the number 1, which the room deployed at
  wai.transaction.science/room/ (live since 2026-10-19) writes until it is deployed again;
  the interop subscriber requires "draft-01", which its own publisher writes.
- the packager's cmaf floor tracks: isLive false and initRef (the floor name), with the
  floor's initialization segment inline in initDataList (CMSF §3.1, the field name of
  draft-ietf-moq-cmsf-01), after tracks (MSF §5.1.7): moq/catalog.json is RFC 8785
  canonical but for that member, written last (packaging §8.4). FloorView carries the
  init segment's bytes; pack_verify.py writes the same catalog independently;
  catalogs/expected.json names each case's floor by its sequence header OBU and
  timescale, from which both write the init segment.
- event timelines: the room page and the remote host wrote "mimetype" for MSF's mimeType,
  and no depends (MSF §8.2); they write mimeType and depends [] (the events track is the
  only track of its namespace, and MSF §5.2.14 names dependencies in the declaring
  track's namespace). live-events §8.1 says so.
- wai-link: the contribution gateway's link track carries WLK1 link records, signed,
  chained and verified by live_contribution_verify.py: neither envelopes ("wai") nor JSON
  events ("eventtimeline"), and read by a subscriber that verifies a room's tracks. A real
  interop surface, so it is defined rather than mapped: live-contribution §5.4 defines
  it, moq-streaming-format §3 names it, the registry lists it as a catalog wire
  identifier (export regenerated), and the -01 draft defines it in a new §9 (IANA table,
  required fields, correspondence). The gateway writes it as before.

The corpus is regenerated by `wai-pack conformance regen` (pack-conformance/ and the
composed package of wai-web/compose); each other writer's test holds a committed sample
(joule-moq-ingest/tests/catalogs/{whip,contribution}.json,
joule-moq-remote/tests/catalogs/host.json, joule-moq-interop/tests/catalogs/wai-receipts.json,
joule-moq-web/e2e/catalogs/room.json), written by its JOULE_MOQ_*_WRITE_CATALOGS=1 run. The
room page's catalog moves into web/room/catalog.js, tested by catalog.test.mjs with the
page's other unit tests.

wai/ietf/catalog_check.py reads every writer's catalogs and, under --strict, holds MSF's
and CMSF's own rules: the version string, a name and isLive on every track, an MSF field
in another case, an event timeline's eventType, mimeType and depends, eventType elsewhere,
a cmaf track's initRef naming an inline initDataList entry that follows tracks; wai-link
tracks are held to the draft's §9. The repository checks run it with --strict. 23
catalogs of 6 writers pass with no note; the self-test plants 43 faults (a numeric
version, a cmaf track without isLive, mimetype and an undefined packaging value among
them) and catches each.

The draft stays -01 (the wire meaning of "wai" is unchanged): Implementation Status says
the deviations are fixed. xml2rfc 3.35.0 renders it with no warning; idnits 2.17.1: 0
errors, 0 flaws, 1 warning (no first-submission date on the datatracker).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 14:05:27 -05:00

40 KiB
Raw Permalink Blame History

WAI Extension: Live Contribution over SRT and RIST

Status: Draft. Profiles two contribution transports with retransmission onto the tracks of a room, through the ingest gateway of live-ingest: SRT (draft-sharabayko-srt-01) as a listener and as a caller, and RIST (VSF TR-06-1:2020, the Simple Profile; VSF TR-06-2:2024, the Main Profile). A publisher obtains a one-time session under its room grant; its encoder sends an MPEG transport stream over the transport; the gateway demultiplexes it and carries each access unit as one envelope on the room's tracks with first-hop receipts, as it carries a WHIP publisher's frames. Registers the record wai.live.contribution: the contribution link record, WLK1, which the gateway signs part by part while the link runs: what became of every sequence number (delivered at the first try, recovered within the receive window, dropped when its time passed, or outstanding when the link ended), the retransmission requests and the retransmissions, the round trips and how long each recovery took, chained part to part and naming the head of every media track's receipt chain. No new transport: WAI defines none by charter. The reference implementation is joule-contrib (the transports and the transport stream) and joule-moq-ingest (the gateway) in joule-code/joule-code-rs/crates/; the corpus is live-conformance/links/, and its second implementation is live-conformance/live_contribution_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: draft-sharabayko-srt-01 (SRT, an Internet-Draft that has expired without publication, and the text SRT implementations are written to); SRT 1.5.7's stream ID guidelines and rejection codes, as live-ingest §13 reads them; VSF TR-06-1:2020 (RIST Simple Profile) and VSF TR-06-2:2024 (RIST Main Profile); RFC 3550, RFC 3551, RFC 4585 §6.2.1, RFC 5761 §4, RFC 8086, RFC 2784; RFC 3394; RFC 8018 §5.2; RFC 3686 §2.1; NIST SP 800-38A §6.5; ISO/IEC 13818-1 (transport streams) and ISO/IEC 13818-7 §6.2 (ADTS); RFC 8032. A section number of one of those texts here is that revision's.

1. Scope

In scope: how a publisher obtains a contribution session and how the gateway admits its link (§3); how the gateway uses each transport, the settings it requires and the defaults it takes (§4); how the transport stream becomes the room's tracks (§5); the link record, its account, its parts and their chain, how it is read and verified (§6); the refusals (§7); the bounds (§8); what is implemented (§9); the patents the RIST texts declare (§10).

Out of scope: the transports themselves, which their texts define; SRT's rendezvous mode, its file transfer and buffer modes, its packet filter and its socket groups; RIST's bonding (TR-06-1 §5.4), multicast, SMPTE 2022-1 FEC, the Main Profile's DTLS (TR-06-2 §6), full datagram mode (§5.3.1), EAP-SRP authentication (§7.5), null packet deletion and the extended sequence number (§8), and the on-the-fly passphrase change (§7.4); egress over SRT or RIST (live-ingest §13's m=request, which this revision refuses, §3.2); transcoding.

Why an extension of its own. live-ingest maps contribution protocols onto a room's tracks, and §13 maps SRT. This text profiles the two retransmitting transports as a gateway runs them, adds RIST, and defines the record of what each link lost and recovered, which WHIP's transport does not need: a WebRTC session's losses are below the frame, and what reaches the gateway is whole frames. live-ingest is changed only where it named SRT "specified, not implemented" (live-ingest §13, §17).

Relationship to the core spec: one record is registered, wai.live.contribution (SPEC §5). The payload capabilities a contribution track carries are SPEC §5's: wai.video.avc, wai.video.hevc and wai.audio.aac.

2. Terms

  • Room, room clock, grant, principal, owner, gateway, first hop: as in live-ingest §2.
  • Link: one SRT connection, or one RIST flow (a Simple Profile flow on its port pair, or a Main Profile tunnel and the flow in it), that the gateway receives.
  • Session: a contribution session (§3.1), the link it admits, and the tracks the gateway publishes for it.
  • Sequence number: SRT's 31-bit packet sequence number, or RTP's 16-bit one, each extended into a 64-bit integer by taking, for every value read, the extension nearest the highest one read so far (RFC 3550 §A.1). The first value read is extended to one wrap above zero, so a value read before it in sequence can lie below it.
  • Delivery time: when the receiver hands a packet on. SRT: its TSBPD time, draft-sharabayko-srt-01 §4.5.1. RIST: the time it was expected, plus the receiver's buffer (§4.2).
  • Receive window: SRT's negotiated latency (§4.4 of the draft); RIST's receiver buffer.
  • µs: microseconds. Integers are little-endian where this text writes bytes.

3. Admission

3.1 The session resource

A publisher opens a contribution session with an HTTP POST to the gateway's …/srt or …/rist resource, with the bearer token and the proof of possession of live-ingest §3.1, admitted by the checks of live-ingest §3.2 in their order and refused with their reasons and statuses. The grant MUST give the publish power for audio or for video (live-ingest §3.3): otherwise 403, publish_not_permitted. The body is a JSON object, empty for the defaults (§3.2, §3.4); one that is not a JSON object is 400, body_malformed.

The gateway answers 201 with Location …/srt/s/<session> or …/rist/s/<session> and a JSON body naming the session: its session, 32 lowercase hexadecimal digits from 128 random bits; its protocol (srt, rist-simple, rist-main); the host and port to connect to; and the expires_s, the time within which the link must connect, after which the session is dropped (60 seconds by default). A session is one-time: the first link that presents it spends it (§3.2), and a link that fails after that (a wrong passphrase) does not get it back.

The session's resource answers only the token that opened it, with a proof scoped to it (live-ingest §3.4): DELETE ends the session, or cancels one not yet connected, 200; GET and HEAD are 204; another token is 403, not_the_session_token; an unknown or ended session is 404, session_unknown. A gateway at its bound of sessions answers a POST 503 with Retry-After, session_limit (§8).

3.2 SRT: the stream ID and the listener's checks

With an empty body, the answer to POST …/srt carries the stream_id the caller presents, #!::r=<room id>/<owner>,m=publish,s=<session> as live-ingest §13 gives it, the room id and the owner in 64 lowercase hexadecimal digits each; the passphrase, 32 characters of base64url from 192 random bits; the pbkeylen, the AES key length in bytes the caller SHOULD use; and the listener's latency_ms. The gateway's SRT listener checks a caller's CONCLUSION handshake in this order, and rejects it with the first code that applies, as a handshake type of 1000 plus the code (draft-sharabayko-srt-01 Table 7, and live-ingest §13's codes):

  1. The HSREQ extension is present and the handshake is version 5: otherwise 1004 (REJ_ROGUE) or 1008 (REJ_VERSION). The STREAM flag (buffer mode) is clear: otherwise 1012 (REJ_MESSAGEAPI).
  2. The stream ID is in the #!:: key-value syntax: otherwise 1401.
  3. Its m is publish: otherwise 1405. m=request, live-ingest §13's play mode, is refused 1405 in this revision, which implements no egress.
  4. Its s is a session this gateway opened for SRT, not spent and not past its time: otherwise 1401.
  5. Its r names a room the gateway holds: otherwise 1404; and that session's room and owner: otherwise 1403.
  6. The session's grant is good now (live-rooms §4.3): otherwise 1401.
  7. The gateway holds fewer contribution sessions than its bound: otherwise 1402.
  8. The owner has no other contribution session running: otherwise 1409.

The session is then spent, and the handshake's key material is checked: a caller that sends none, or one of a cipher other than AES-CTR, is refused 1011 (REJ_UNSECURE), since every listener session is encrypted; one whose wrap does not unwrap under the session's passphrase is refused 1010 (REJ_BADSECRET). A listener that holds no passphrase for a caller that sends key material refuses it 1011 too: encryption is enforced both ways (draft-sharabayko-srt-01 §6.1.5).

3.3 SRT: the gateway calling

A contribution encoder is often itself the SRT listener. A POST …/srt whose body names call, the encoder's ip:port, has the gateway call it: the body MAY name the passphrase, the pbkeylen, the stream_id the gateway presents and the latency_ms. A gateway calls only the addresses its configuration lists (an address and port, or an address for any port): another is 403, call_not_permitted, so a grant cannot make the gateway send to an address of the requester's choosing. The gateway answers once the handshake completes: 201 as §3.1, or 502 with call_rejected (the encoder's code in the detail) or call_failed.

3.4 RIST: the port, the CNAME, the PSK

RIST has no stream ID, so a RIST session is a port of its own. The body of POST …/rist MAY name the profile, simple (the default) or main; another is 400, body_malformed. The answer gives the port P (for the Simple Profile an even port, whose RTCP is P + 1), the cname, which is the session ID, and the receiver's buffer_ms; for the Main Profile, the PSK passphrase and its key length aes, 128 or 256 bits.

The receiver serves one flow: the first whose RTCP SDES CNAME (TR-06-1 §5.2.5) is the session's, and, in the Main Profile, whose tunnel packets decrypt under the passphrase and read (§4.3). Until then it takes nothing as the session's; a flow with another CNAME is counted and ignored. From then on it takes the flow's packets from that address alone (the Simple Profile: the sender's address and its SSRC, the low bit clear; the Main Profile: the tunnel's address and port). The Simple Profile has no encryption: on a path an attacker sees, its CNAME is visible and its flow can be raced; a deployment that needs more uses the Main Profile under PSK, or SRT.

4. The transports

4.1 SRT

The gateway's listener serves many callers on one UDP port, each connection found by the destination socket ID its packets carry (draft-sharabayko-srt-01 §4.1). It speaks the caller-listener handshake of §4.3.1 with the HSv5 extensions; its INDUCTION reply carries a SYN cookie made from the caller's address, port and the minute under a secret of the listener's, and a CONCLUSION whose cookie is neither this minute's nor the last is ignored. It sends its CONCLUSION reply again for a repeated CONCLUSION.

Live mode, as the draft's §7.1 gives it, and nothing else: every data packet one message (PP = 11); TSBPD and too-late packet drop (the HSREQ flags TSBPDSND, TSBPDRCV, TLPKTDROP); periodic NAKs; CRYPT and REXMITFLG set. The connection's latency each way is the larger of the two ends' (§4.4); the gateway's is its configured latency (120 ms by default), and a deployment SHOULD set it to several round trips of the paths it serves (live-ingest §13).

  • Delivery (§4.5): a packet is delivered at base + timestamp + latency + drift, the timestamp extended past its 32 bits, base taken when the CONCLUSION arrived. A first data packet more than 2 s from where base puts it re-takes base from itself (a peer that stamps its handshake on another clock than its data). The drift tracer (§4.7) adds the mean, over 1 000 ACKACKs, of how far each arrived (less half the RTT) from where base put it, once the mean passes 5 ms.
  • Too-late drop (§4.6): when the next packet to deliver is missing and the first packet after it that has arrived is due, the missing ones are dropped.
  • ACK (§4.8.1): a full ACK every 10 ms while the acknowledged position moves, and at least every 100 ms; a light ACK every 64 packets.
  • NAK (§4.8.2): at once for each gap, and periodically every (RTT + 4 × RTTVar) / 2 with a 20 ms floor, for every packet still missing.
  • A stalled tail, sending: a receiver finds a gap only behind a later arrival, so when the acknowledged position has not moved for 2 × RTT + 20 ms and the newest packet held went out that long ago, the newest is sent again with the R flag; the wait doubles with each such probe, to at most 16 times the first, until an ACK takes a packet.
  • RTT (§4.10) from ACK/ACKACK pairs, from 100 ms and 50 ms.
  • Encryption (§6): AES-CTR, the SEK of 16, 24 or 32 bytes wrapped (RFC 3394) under a KEK from PBKDF2-HMAC-SHA1 over the last 64 bits of the salt with 2 048 iterations; the counter block the salt's first 112 bits XOR the packet index at its bytes 10 to 13, the block counter in its last two. A refreshed key (§6.1.6, a user-defined control packet of subtype KMREQ) is taken and answered with the same message as KMRSP.
  • The end: a SHUTDOWN ends the link (the gateway sends three, since none is acknowledged), and so does 5 s with nothing received.

What the link then holds is delivered at once; what is missing among it is outstanding (§6.3).

4.2 RIST Simple Profile

RTP on an even port P and RTCP on P + 1 (TR-06-1 §5.1.1), the payload type 33 (MP2T, RFC 3551) with a transport stream of seven packets a datagram as a rule. The receiver:

  • holds every packet for its buffer (§5.3.1): a sequence number is delivered buffer after it was expected, which is its arrival, or, for one found missing, the arrival that showed the gap; in order; one still missing then is dropped. The buffer is the gateway's configured one (1 000 ms by default, TR-06-1 Appendix B);
  • requests what is missing (§5.3.2), as range requests (the RTCP APP packet named RIST, subtype 0) or bitmask requests (RFC 4585 §6.2.1): first a reorder interval (20 ms) after it was found missing, then again every interval, 1.5 × the RTT where the RTT echo has measured it, otherwise (buffer − reorder) / 7, at most 7 times, and not once its delivery time is closer than half a round trip;
  • reports every 100 ms (§5.2.1): an RR with its report block once data has arrived, and an SDES with its CNAME; once a second an RTT echo request (§5.2.6); and answers the sender's echo requests at once. RTCP goes to the address of the last valid RTCP received (§5.1.1 rule 3);
  • counts as a retransmission a packet whose SSRC has its low bit set (§5.3.3).

A RIST receiver cannot know of packets lost before the first it receives: its account begins there (§6.8). A flow ends when nothing of it has arrived for 5 s; RIST has no shutdown message.

4.3 RIST Main Profile

One UDP port P, GRE over UDP (RFC 8086) as TR-06-2 §5 constrains it; the gateway is the tunnel's server. It reads:

  • the GRE header's C, K and S bits, to find its size; its Reserved0 bits 10 to 12 as RV (000 TR-06-2:2020, 001 TR-06-2:2021, 010 TR-06-2:2022 and later; 011 and 100 read as 010; 101 to 111 discarded) and bit 9 as H (a 256-bit PSK key where RV ≥ 001), §5.1;
  • the VSF EtherType 0xCCE0 and its packet header (§5.2): type 0x0000, subtype 0x0000 a reduced overhead packet, 0x8000 a keep-alive, 0x8001 a future nonce announcement; and the legacy EtherTypes 0x88B6 (reduced overhead) and 0x88B5 (keep-alive), §5.3.2, §5.6.3;
  • a reduced overhead packet as its UDP ports and payload (§5.3.2): RTP or RTCP of a Simple Profile flow, told apart as RFC 5761 §4 tells them, served as §4.2 serves one;
  • with K set, PSK (§7.2, §7.3): the GRE payload (everything after the GRE header, the VSF header among it) in AES-CTR (RFC 3686 §2.1) under PBKDF2-HMAC-SHA256 of the passphrase over the four bytes of the GRE nonce, 1 024 iterations, 16 or 32 bytes by H, from the counter block of the GRE sequence number and twelve zero bytes. A key is derived once per nonce. RV 000's counter block, which §7.2 calls insecure, is refused: counted, never decrypted.

The server sends a keep-alive (§5.6.3: a MAC address, the capability flags, here V) once the client's first message arrives and every second after (§5.6.2), in the client's format (the VSF EtherType or the legacy one) and, under PSK, encrypted under a nonce of its own. Its RTCP goes back through the tunnel from the virtual port the client's RTCP was sent to, to the one it came from. A keep-alive whose D flag is set ends the link (§5.6.5); so does 5 s with nothing received. Packets of full datagram mode, EAPOL and unknown subtypes are discarded, and keep the link alive (§5.1).

5. The stream onto the tracks

5.1 Demultiplexing

The link delivers a transport stream (ISO/IEC 13818-1) at its delivery times. The gateway reads its PAT and the first program's PMT, each section's CRC checked, and reassembles each elementary stream's PES packets, resynchronising on the sync byte. It carries the first video stream the PMT declares of stream type 0x1B (wai.video.avc) or 0x24 (wai.video.hevc), and the first of 0x0F (wai.audio.aac), each to the media track of its kind (live-ingest §4.1), where the grant gives the publish power for that kind; other streams are counted and not carried.

A PES is complete at the next packet of its PID that starts one, or when it holds the length its header states. A PES of which a packet did not arrive, a jump in its PID's continuity counter without the discontinuity indicator, is dropped whole and counted: the gateway never carries a unit cut short. A packet whose continuity counter repeats the one before is a duplicate and is ignored. A lost packet that began a PES also ends the PES before it, which can then not be told whole, and is dropped with it.

5.2 Objects

stream capability an object's payload
0x1B wai.video.avc the PES payload: one access unit, Annex B, as the encoder wrote it
0x24 wai.video.hevc the PES payload: one access unit, Annex B
0x0F wai.audio.aac one ADTS frame (ISO/IEC 13818-7 §6.2), whole, its header included

A PES of AAC holding several ADTS frames gives one object for each. Groups are live-ingest §5.2's: a video group begins at each IDR access unit (H.264) or IRAP (HEVC, NAL unit types 16 to 23), an audio group holds one window of audio_group_ms.

5.3 Time

A PES's PTS, 33 bits at 90 kHz, is extended past its 33 bits and is the media time of live-ingest §7's mapping, with a rate of 90 000, for video and audio alike; the k-th ADTS frame of a PES whose PTS is p at sampling rate f is at p + ⌊k × 1024 × 90 000 / f⌋. A frame's arrival is the time its last TS packet left the link, on the room clock. The manifest's live member (live-ingest §6) carries the extended PTS as rtp and 90 000 as rate.

Because the link delivers each packet at its sender's time plus a constant (SRT's TSBPD, RIST's buffer), the arrivals follow the publisher's clock, and §7's bound applies to the variation of the path and the encoder; a packet recovered with a margin above 0 (§6.2) arrives on time. An SRT packet recovered after its own delivery time but before it would be dropped (a margin of 0, §6.1) is delivered late by as much, as is a packet delivered at the first try that the receiver took after its delivery time (§6.8). A frame whose time lies past max_skew_ms from its arrival re-anchors the mapping, as live-ingest §7.1 says.

5.4 Tracks, receipts and the catalog

The tracks, their names and priorities, the groups, the receipts and their chains are live-ingest §4, §5 and §8's, under the session ID of §3.1 (live-ingest §8.2), so a session signs chains of its own. The gateway publishes the owner's catalog, the media tracks the grant lets it publish, and the link track, (room id, owner) / link, with its receipts track link_receipts, before any frame. The catalog (live-ingest §4.3) declares, after the media tracks, the link track with packaging wai-link, wai_receipts true, its wai_receipt_alias and wai_receipts_track, then its receipts track.

The packaging value wai-link. This extension defines "wai-link" as a packaging value of MSF (MSF §5.2.4), for the link track; draft-charlot-moq-wai-packaging defines it for MSF in the same terms. On a track of packaging "wai-link":

  • every MOQT object is exactly one link record (§6.2), whole, from magic through sig, with nothing before or after it; each group carries one, a part (§6.4);
  • its catalog entry carries MSF's namespace, name and packaging, isLive (MSF §5.2.7), and the receipts fields of moq-streaming-format §4: wai_receipts, required, and, where it is true, wai_receipt_alias and wai_receipts_track, which names the track of packaging "wai-receipts" in its namespace that carries its receipts, with moq-streaming-format §4.1's rules for both (an alias and a receipts track of its own). It carries no other field: no role, no codec field, no wai_capabilities;
  • each record's receipt commits to the object as jwp-receipts §2.1's content hash does to an envelope, over the record's bytes;
  • a receiver that does not recognise "wai-link" does not interpret the objects; and a record is not an envelope, so a receiver never dispatches one as a "wai" object.

A receiver reads and verifies each record by §6.5 and a session's records by §6.6.

6.1 What it states

The link record is the gateway's statement of what its link carried, as a receiver sees it. A receiver passes sequence numbers in ascending order, each at its delivery time, and each comes to exactly one end:

  • delivered at the first try: its first send arrived before anything found it missing;
  • recovered: it was found missing (a gap behind a later arrival), then arrived, by a retransmission or late from reordering, before it was dropped; or its first arrival was a retransmission (SRT's R flag, RIST's retransmission SSRC) with nothing behind it yet to show the gap, as when a sender probes a stalled tail (§4.1), and it is found missing as it arrives (a recovery of 0 µs);
  • dropped: its time passed without it: for SRT, the delivery time of the first packet after it that has arrived (§4.1's too-late drop); for RIST, its own delivery time (§4.2);
  • outstanding: the link ended before it arrived and before its delivery time.

A packet arrives when the receiver takes it, which on a busy host can be well after it reached the receiver's socket. An SRT packet that arrives at or after its own delivery time, but before the first packet after it that has arrived is due, has not been dropped: it is delivered at once, in order, and is recovered with a margin of 0 (§6.2). One that arrives at or after that point is late (§6.3), whether or not the receiver had dropped its sequence number before taking it: a receiver resolves what has fallen due before it takes each packet. For RIST a packet's own delivery time is where it is dropped, so there is no such case.

The record is cut into parts. A part covers the sequence numbers passed while it was open, and the events (requests sent, retransmissions and duplicates received, late arrivals, round trips) that happened while it was open, on the room clock from start to end; the next part begins where it ends. The gateway signs a part every record_interval_ms (10 000 by default) that has anything in it, once the link's first sequence number has been passed, and the last part when the session ends; round trips measured before the first sequence number fall into the first part.

6.2 Layout

field size meaning
magic 4 WLK1 (ASCII)
version 1 1
room 32 the room id
hop 32 the gateway's key, which signs the record and the tracks' receipts
principal 32 the owner
grant 32 the grant's digest (live-rooms §4.2)
session 16 the session ID (§3.1)
part 4 u32: the part's index, from 0
parent 32 the digest of part part − 1; 32 zero bytes for part 0
protocol 1 1 SRT, 2 RIST Simple Profile, 3 RIST Main Profile
mode 1 1 the gateway listened, 2 it called
cipher 2 u16: the AES key length in bits; 0 in the clear
last 1 1 for the session's last part, 0 otherwise
ended 1 the last part: why the session ended (§6.3); 0 otherwise
start, end 8 each u64: the part's span on the room clock, µs, [start, end)
window 8 u64: the receive window, µs
seq_first, seq_count 8 each u64: the sequence numbers the part covers, [seq_first, seq_first + seq_count)
delivered, first_try, dropped, outstanding 8 each u64 (§6.3)
late, duplicates 8 each u64 (§6.3)
bytes 8 u64: the payload bytes delivered
requests, requested, retransmitted 8 each u64 (§6.3)
rtt 16 four u32: the round trips' samples, min, median, max, µs
recovery 16 four u32: the recoveries' samples, min, median, max, µs
margin_min 4 u32: the least time, µs, a recovered packet arrived before its delivery time, each such time taken at most window, and as 0 for one that arrived at or after it (§6.1); 0 with none recovered
tracks 1 + 80 × n a u8 count n, then each track's head (§6.4): the track digest (32; live-events §3.1), its receipt chain's track_alias (u64; live-ingest §8.2), the groups signed on it so far (u64), the link hash of the latest of their group receipts (32; jwp-receipts §2.4.1), zero with none
runs 3 + 20 × k a u16 count k, a u8 truncated, then each loss run: first (u64), count, recovered, dropped (u32 each)
sig 64 Ed25519 by hop (§6.5)

A median is the lower middle sample of an even count. A loss run is a maximal run of consecutive sequence numbers of the part none of which arrived at the first try; its count − recovered − dropped are outstanding. A part lists at most 512 runs, in ascending order; with more, it lists the first 512 and sets truncated.

6.3 The account

  • delivered counts the part's sequence numbers delivered, first_try those of them that arrived at the first try, so delivered − first_try were recovered; dropped and outstanding count the rest. delivered + dropped + outstanding = seq_count.
  • late counts packets that arrived after their sequence number was dropped; duplicates those that arrived again for a sequence number already held or delivered.
  • requests counts the retransmission requests the gateway sent (SRT NAKs, RIST bitmask and range requests), requested the sequence numbers they named, repeats counted; retransmitted the packets that arrived marked as retransmissions (SRT's R flag, RIST's SSRC low bit).
  • rtt: the round trips the gateway measured (SRT: from each ACK to its ACKACK; RIST: from each echo request to its response, less the stated processing delay).
  • recovery: for each recovered packet, from its being found missing to its arrival; recovery.samples = delivered − first_try. An SRT recovery is not bounded by window: it runs from when the packet was found missing, and the window from where its timestamp puts it on the receiver's clock (§4.1), which can be later.
  • ended: 1 the peer ended the link (SRT SHUTDOWN, a RIST keep-alive with D); 2 nothing arrived for the idle time; 3 the session was DELETEd; 4 the gateway stopped; 5 the relay was lost; 6 the relay fell behind; 7 another reason.

Each part names its predecessor by digest (parent), so a part withheld, altered or reordered breaks the chain (§6.6). Each part's tracks names, at its end, the head of the receipt chain of every media track the session published, in ascending order of track digest: the record binds what the link carried to the receipts that cover the objects made from it. A verifier that holds a track's receipts checks that the head is the link hash of the last group receipt it has, and that groups is how many it has.

The gateway publishes each part, as signed, as the one object of a group of the link track (§5.4), its group IDs following live-ingest §5.3's rule, and its receipts on link_receipts, signed by the gateway as the media's are: a subscriber that verifies the room's tracks verifies the link record with them. The gateway also keeps each part, and MAY append it to a file.

6.5 Reading and verifying a part

The digest of a record is SHA-256 over its bytes before sig; its signature is Ed25519 by hop over the 17 bytes wai:contrib-link (ASCII) and 0x01, followed by the bytes the digest covers. A verifier takes these steps in order and refuses at the first that fails, with its reason:

  1. The record is at most 11 295 bytes (§6.7): otherwise record_too_large.
  2. It reads, field by field in §6.2's order, to exactly its last byte, with tracks at most 8 and runs at most 512: truncated when it ends early; unknown_version for a version other than 1; malformed for another magic (live-recording §5.1's clip record, WCL1, among them), a byte after sig, a count past its bound, or a last or truncated other than 0 and 1.
  3. The signature verifies under hop, strictly, as SPEC §7 Signatures requires: a key that fails that rule's steps 1 or 2 is bad_key, a signature that fails a later step bad_signature.
  4. The link, else link_invalid: protocol is 1, 2 or 3; mode 1 or 2; cipher 0, 128, 192 or 256 for SRT, 0 for the Simple Profile, 0, 128 or 256 for the Main Profile; last is 1 exactly when ended is not 0, and ended is at most 7; start < end; window > 0; part is 0 exactly when parent is zero; the tracks are in strictly ascending order of digest, digests compared byte by byte from the first, none zero, and each head is zero exactly when its groups is 0.
  5. The account, else account_invalid: first_try ≤ delivered; delivered + dropped + outstanding = seq_count; outstanding is 0 unless last; recovery.samples = delivered − first_try; for each of rtt and recovery, with no sample all four values are 0, and with samples min ≤ median ≤ max; margin_min is 0 with nothing recovered, and at most window; requested ≥ requests; bytes is 0 when delivered is.
  6. The runs, else runs_invalid: with truncated set there are exactly 512; each run's count is above 0, its recovered + dropped at most its count, it lies within [seq_first, seq_first + seq_count), and it begins after the one before ends, with at least one sequence number between. Not truncated, the runs' counts sum to seq_count − first_try, their recovered to delivered − first_try, their dropped to dropped, and their outstanding to outstanding; truncated, each sum is at most that.

6.6 A series

A session's parts, in order of part, are a series. A verifier takes the parts in turn and refuses at the first that fails, giving its index: each part verifies (§6.5), then, for the first, its part is 0, otherwise chain_broken; and for each after the first:

  1. room, hop, principal, grant, session, protocol, mode, cipher and window are the part before's; the part before is not last; part is its part plus one; parent is its digest; every track the part before names is named again, with the same track_alias and either more groups or the same groups and head: otherwise chain_broken. A part MAY name a track the part before did not.
  2. start is the part before's end, and seq_first its seq_first + seq_count: otherwise sequence_gap.

So the parts of a series account for every sequence number from the first part's seq_first to the last part's end, and every microsecond from its start to its end, each once. A series that does not end with a last part is the record of a session still running, or one whose later parts are withheld: a verifier that needs the whole session requires the last part. The rule first_try ≤ delivered of §6.5 step 5 is implied by the rule on recovery.samples; it is stated so that no subtraction there is negative.

6.7 The size bound

A record is at most 415 + 80 × 8 + 20 × 512 = 11 295 bytes: 415 the fixed fields and the signature, eight track heads, 512 runs. The bound does not depend on the stream's rate or length: a part's losses beyond 512 runs are counted in its sums and marked truncated, and a long session has more parts, not larger ones.

6.8 What the record does not say

  • It is the receiver's account: what the gateway received, in its window. A packet the sender never sent, or dropped from its own buffer, is a loss here like any other.
  • A RIST receiver cannot know of packets lost before the first it received, nor after the last: RIST states neither. An SRT receiver knows the first from the handshake's initial sequence number.
  • A recovered packet arrived before it was dropped. For SRT that can be after its own delivery time (§6.1): such a packet counts a margin of 0 (§6.2), and the record does not say how many there were, nor how late each was delivered. A margin_min above 0 says that every packet recovered in the part arrived before its delivery time.
  • It does not say when a packet delivered at the first try arrived against its delivery time. One the receiver took after it (a receiver behind its socket by more than the window, or a path slower than the latency), with nothing missing before it, is delivered at once and counted at the first try.
  • It does not say whether the unit a recovered packet was part of was later carried (a PES can still be cut by a packet dropped elsewhere, §5.1). The session record (live-ingest §16) counts the units dropped.
  • The figures are the gateway's clock's. The record states no energy: live-ingest §16's session record and live-metering's per-hop record do.

7. Refusals

At the session resource (§3), beside live-ingest §14's:

reason status raised at
publish_not_permitted 403 §3.1, a grant that publishes neither audio nor video
body_malformed 400 §3.1, §3.3, §3.4
call_not_permitted 403 §3.3
call_rejected, call_failed 502 §3.3
session_limit 503, with Retry-After §8
session_unknown 404 §3.1
not_found 404 a gateway without an SRT listener, or serving no RIST

At the SRT listener (§3.2): 1401, 1402, 1403, 1404, 1405, 1409 of live-ingest §13; 1004, 1008, 1010, 1011 and 1012 of draft-sharabayko-srt-01 Table 7.

Of the link record (§6.5, §6.6): record_too_large, truncated, malformed, unknown_version, bad_key, bad_signature, link_invalid, account_invalid, runs_invalid, chain_broken, sequence_gap.

8. Bounds

bound default past it
contribution sessions at once, open or running 16 503; the listener 1402
a session's time to connect 60 s dropped
a session's rate, all streams, over bitrate_window_ms 80 000 kbit/s the session is ended, bitrate_exceeded
a link's time with nothing received 5 s the link is ended, idle
the largest frame live-ingest §10's dropped and counted
a part's loss runs 512 the first 512, truncated
the tracks a record names 8 —
a RIST session's rate, its buffer holding fewer than 32 768 packets 32 768 × 1 316 × 8 bits a buffer: 344 981 kbit/s with 1 000 ms the session is ended, bitrate_exceeded

RTP's sequence numbers are 16 bits, and a receiver places each packet within half their space of the highest it has received (§2): a packet more than 32 768 behind it is placed a wrap ahead. So a RIST buffer must hold fewer than 32 768 packets, and the gateway bounds a RIST session's rate by its buffer as the table gives; a deployment that needs more shortens the buffer, or uses SRT, whose sequence numbers are 31 bits.

9. Status and conformance

mapping status where
SRT listener (live-ingest §13, §3.2, §4.1) implemented joule-contrib srt, joule-moq-ingest contrib
SRT caller (§3.3) implemented the same
RIST Simple Profile (§3.4, §4.2) implemented, feature rist joule-contrib rist, joule-moq-ingest contrib
RIST Main Profile, reduced overhead, PSK (§4.3) implemented, feature rist the same
the link record (§6) implemented joule-moq-ingest linkrec; corpus live-conformance/links/

The reference implementation's tests run on loopback. Each transport against itself, with datagrams dropped each way, reordered and in bursts, and every payload delivered whole and in order and every sequence number accounted for; SRT with each key length, a wrong and a missing passphrase, and every rejection code of §3.2; RIST with range and bitmask requests, the Main Profile in the clear, under PSK of each length, and in the legacy format. Through the gateway: an SRT publisher and a RIST publisher of each profile whose access units and ADTS frames a relay subscriber receives whole with every receipt verified (and the relay's door verifying them too), the link record verifying as a series and naming each track's receipt-chain head as the subscriber found it, the record on the link track; the gateway calling an encoder's listener and carrying HEVC. Against the reference tools of each transport run locally (joule-contrib's tests/interop.rs, with the tools named by environment variables): their SRT caller and their RIST senders of both profiles into this implementation, this implementation into their listener and receivers, byte for byte, in the clear and encrypted, and through a lossy path.

The corpus live-conformance/links/ holds accepted parts and series and refused ones, each refused for the reason it is meant to show, and every reason of §7's last list refuses at least one; live-conformance/live_contribution_verify.py verifies it from this text alone.

Not implemented in this revision, beside §1's exclusions: SRT egress (m=request), a RIST receiver as the tunnel's client, the Main Profile's virtual port multiplexing of several flows in one tunnel, and SRT's key refresh as a sender.

10. Patents

VSF TR-06-1:2020 and TR-06-2:2024 each carry an intellectual property notice: a patent is claimed to read on their protocol sections (TR-06-1 §4, §5 but §5.1.2; TR-06-2 §5 but §5.6.4, §6, §7), and its holder has assured the forum that it will license the claims to anyone implementing the recommendation. The forum takes no position on them. The RIST code of the reference implementation is therefore built only with its feature rist, which no default build turns on; whether a distributed build carries it is a licensing decision for the distributor. No such notice was found for draft-sharabayko-srt-01.

11. Security and privacy considerations

  • A session's stream ID, CNAME and passphrase are its credentials, single-use and short lived; the POST that gives them is authenticated as WHIP's is, and its answer travels inside the gateway's TLS front (RFC 9725 §5).
  • SRT encrypts the payload, not the headers (draft-sharabayko-srt-01 §8): sequence numbers, timestamps and sizes are visible on the path; so is a RIST Simple Profile flow entire, and a Main Profile tunnel's GRE header.
  • Neither AES-CTR here authenticates: a packet altered on the path decrypts to other bytes. The receipts are signed over what the gateway received (live-ingest §18), so they are no stronger than the path; a TS packet that does not read is dropped with its PES (§5.1), but a well-formed alteration is carried. A deployment that needs origin authentication of the media has the encoder sign its own receipts over MoQ (live-ingest §8.1).
  • The gateway calls only the addresses its configuration lists (§3.3).
  • The link record says how much was lost and when: a viewer of the link track learns the path's quality. A deployment that treats that as confidential does not grant the link track's subscription.
  • Every bound of §8 limits what one session can make a gateway hold; a RIST session holds a port for at most its connect time before its flow arrives.