open-standards/wai/extensions/live-contribution.md
Transaction Science d1b4bd0c01
All checks were successful
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 13s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 14s
wai-quantum docs, course and locales / does this change touch it (pull_request) Successful in 6s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 8s
WAI Studio / does this change touch it (pull_request) Successful in 7s
WAI energy receipts / does this change touch it (pull_request) Successful in 28s
WAI operation catalogue / does this change touch it (pull_request) Successful in 30s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 32s
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 42s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (pull_request) Successful in 45s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 31s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m17s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m31s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m44s
energy meters on real hardware / sink decode table · per-task energy and the GPU rail on macOS (pull_request) Successful in 2m33s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m11s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 2m16s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 26s
energy meters on real hardware / sink decode table · RAPL on jetson-hub (NVML surveyed) (pull_request) Successful in 3m11s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 3m59s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 2m58s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 4m9s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 4m19s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 6m5s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 7m18s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m51s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 7m53s
WAI conformance / does this change touch it (pull_request) Successful in 10m3s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 10m6s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 7m40s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 10m11s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 10m22s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 27s
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Successful in 10m14s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 49s
standards workspaces build + test / arl-rs (arl/arl-rs, arl-rs, linux) (pull_request) Successful in 55s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 1m24s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m25s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m4s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m43s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 12m4s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 10m59s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 27s
standards workspaces build + test / pool-rs (pool/pool-rs, pool-rs, linux) (pull_request) Successful in 36s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m17s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m36s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 10m23s
standards workspaces build + test / proof-rs (proof/conformance/v1/joulespec.toml, proof-conformance, proof/proof-rs, proof-rs, linux, true) (pull_request) Successful in 1m3s
standards workspaces build + test / eoc-rs (eoc/eoc-rs, -p eoc-meter --features cuda, eoc-rs, linux) (pull_request) Successful in 2m28s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m40s
standards workspaces build + test / sense-rs (sense/sense-rs, sense-rs, linux) (pull_request) Successful in 1m30s
standards workspaces build + test / joulespec-rs (true, joulespec/joulespec-rs, true, joulespec-rs, linux) (pull_request) Successful in 1m9s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, -p sandbox-tee --features amd-kds,intel-pcs, sandbox-rs, linux) (pull_request) Successful in 2m10s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 14m19s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 15m14s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 14m8s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 14m16s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 35s
standards workspaces build + test / joulespec-proofs (joulespec/joulespec-rs, joulespec-proofs, true, macos) (pull_request) Successful in 1m24s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 6m53s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 49s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 7m47s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m18s
standards workspaces build + test / smart-byte-rs (smart-byte/smart-byte-rs, smart-byte-rs, linux) (pull_request) Successful in 7m17s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 10m8s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 8m32s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 13m53s
wai-quantum WebGPU member / member · GPU, Vulkan (thor) (pull_request) Successful in 24m45s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 14m21s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 26m3s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 13m44s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 7m1s
wai-quantum WebGPU member / member · GPU, Vulkan (jetson-hub) (pull_request) Successful in 23m54s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 18m31s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m56s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 20m12s
standards workspaces build + test / aac-rs (aac-conformance, aac/aac-rs, aac-rs, linux-arm64) (pull_request) Successful in 3m27s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 20m43s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 1m1s
standards workspaces build + test / map-rs (map-conformance, map/map-rs, map-rs, linux-arm64) (pull_request) Successful in 1m3s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 59s
standards workspaces build + test / openpay (openpay, openpay, linux-arm64) (pull_request) Successful in 3m30s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 6m32s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h31m25s
joule-contrib: the SRT link tests again require every recovered packet taken before its delivery time; a recovery is pinned to when its gap was found, and a held packet keeps its margin at the link's end
The loss test's last assertion had become `margin_min <= window`, which the
receiver's clamp and the verifier's rule already guarantee: the end-to-end test
checked no timing at all. It again asserts `margin_min > 0`, the only check
across the real link (the sender's NAK handling, the connection's loop, the
proxy) that every recovered packet was taken before its own delivery time. The
ordering defect fixed in 066d82edf delivered a retransmission taken after its
drop point as recovered with a margin of 0, with nothing dropped and every
payload in order: exactly CI's failure at the old line 148. That failure fits
the defect, a busy runner's lag, or both (with the starved timer fixed in
76264fae6 lengthening the first window's recoveries); the part was not printed,
so it cannot be told which, and the earlier commit's "the bound measured the
host, not the protocol" says more than the evidence does. The window stays a
second, which absorbs the receiving task's lag seen under load. The recovery
time stays unbounded there: it counts from when the gap was found, not from the
packet's timestamp (live-contribution §6.3).

What pins where a recovery counts from is now the receivers' unit tests: they
find each gap at 20 ms rather than at the clock's origin, so a recovery counted
from 0, or from the last tick, fails them. The loss test also asserts what does
not depend on the host: every recovered packet came back as a retransmission
(the proxy neither delays nor reorders), and every NAK names a sequence number.

The caller that receives takes its time base from the listener's CONCLUSION
reply, wiring no unit test reaches. Its lossy test now checks its window, its
account (nothing dropped, a margin above 0), and a new test holds one packet
for the caller's window as the listener's test does.

A recovered packet still held when a link ends, SRT or RIST, was recorded with
a margin of 0, which reads as one taken after its delivery time; it now keeps
the time it arrived before that. A unit test for each.

A receiver unit test feeds ACKACKs through the connection's loop on a clock of
its own and pins the periodic NAK after the RTT is measured: every
(RTT + 4 x RTTVar) / 2, 30 ms at a 60 ms round trip, and the 20 ms floor at
1 ms, until the drop point. The comment on the timer test now says the periodic
NAK counts from the link's start; the NAK sent at once does not restart it.

live-contribution: §5.3 and §6.8 said a recovered packet arrives on time, which
under §6.1 does not hold for SRT: one taken after its own delivery time, before
the first packet after it that has arrived is due, is delivered late, counted
recovered with a margin of 0, and the record does not count how many. They now
say so, and §6.8 that the record does not say when a packet delivered at the
first try was taken. §4.1 and §6.1 name the drop point as the delivery time of
the first packet after it that has arrived, since the next one can itself be
missing. The CHANGELOG entry says the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 08:13:40 -05:00

610 lines
39 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](live-ingest.md): 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/`](../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.
## 6. The link record
### 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.
### 6.4 Parts, the chain, the link track
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' `count`s 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.