open-standards/joule-code/joule-code-rs/crates/joule-moq-ingest/README.md
Transaction Science 1a941943c4
Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 34s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 38s
wai-app and Create / wai-app · does this change touch it (push) Successful in 25s
wai release binaries build on Windows / the seven release binaries · windows (push) Has been cancelled
WAI examples / WAI examples · does this change touch them (push) Has been cancelled
WAI examples / WAI examples · portable · linux (push) Has been cancelled
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (push) Has been cancelled
Essence contribution: each stream read on a thread of its own, several datagrams a call
The ST 2110-20 receive path in joule-moq-ingest no longer makes a system call and an
allocation and a wake-up a datagram on the gateway's runtime (D66's finding at 1920 x 1080
and 60, about 218 000 datagrams a second). st2110::rx reads each stream's socket on a
thread that does nothing else, into the session's one bounded queue: on Linux one recvmmsg
up to 64 buffers with UDP_GRO (a run of datagrams of one size cut back at the size the
kernel states), the kernel's arrival stamps (SO_TIMESTAMPNS) and its count of buffers a
full receive buffer dropped (SO_RXQ_OVFL); elsewhere one recvmsg a datagram with
SO_TIMESTAMP, and on macOS the thread asks for QOS_CLASS_USER_INTERACTIVE. The system calls
are in st2110::sock on libc's structures, each with its SAFETY note. The readers are
untouched: the reorder, loss and completeness rules, the corpus (--check) and every earlier
test pass as they were.

st2110::timing reads a video stream's arrivals against the Network Compatibility Model of
the sender type its description names (ST 2110-21:2022 §6.6.1, C_MAX of §7.1), counting a
frame outside only where C_INST passed C_MAX by more than one, since the drain grid's phase
is the gateway's; it is a reading, never the sender's compliance (sender_type_checked stays
"no"), and the Virtual Receiver Buffer Model is not read. st2110::pace is the test sender
on ST 2110-21's linear schedule, four packets at an instant and never sooner than the model
drains them, by sendmmsg with UDP_SEGMENT on Linux. tests/essence_rx.rs holds the way in to
broadcast rates on request; tests/essence_rt.rs sends with the pacer and takes
WAI_RT_MAXUDP (ST 2110-10's Extended UDP Size Limit, stated as MAXUDP).

Planted faults, each caught: a batch that drops its last datagram; coalesced datagrams cut
a size off either way; a sender bursting whole frames against a description of a narrow
sender (through a session, and over computed arrivals).

WAI essence-contribution §10.10 (new), §3, §10.1, §10.7, §10.9, §12.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 04:27:02 -05:00

484 lines
38 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.

# joule-moq-ingest
A gateway between contribution protocols and the MoQ tracks of a WAI room. A standard
WebRTC encoder publishes into a room over **WHIP** (RFC 9725), and an RTMP encoder over
**Enhanced RTMP v2** (`v2-2026-01-31-r2`), single-track or multitrack, over TCP or TLS; the
gateway becomes their tracks' MoQ publisher at the relay and signs every group's receipts as
the first hop. A standard WebRTC player watches a room's tracks over **WHEP**
(`draft-ietf-wish-whep-05`); the gateway subscribes at the relay and verifies every object's
receipt before it hands the frame on. A broadcast automation system drives the gateway's
**SCTE 104** injector, which translates its requests to SCTE 35 and signs them as the room's
cues. It also takes contribution over **SRT** and **RIST** into the same tracks
(`extensions/live-contribution.md`, below), and signs each such link's record of loss,
recovery and latency. The mappings it implements are WAI's `extensions/live-ingest.md`,
`extensions/cue-injection.md` and `extensions/live-contribution.md`.
It is a library (`GatewayServer`, `Gateway`) and a binary, `wai-ingest` (the crate keeps its
name). The relay is `joule-moq-relay` (binary `wai-relay`); the MOQT codec is `joule-moq`'s. The gateway's sessions with the relay speak the draft `[relay] draft` names, offered alone in the ALPN: `22` by default, or `23`, `18` or `16`, bridged to the gateway's -22 model by `joule-moq-relay`'s `transport::bridge`.
`GatewayServer::shutdown` stops listening, ends every session (each signs its record, ended
`deleted`; an RTMP publisher as below), and returns once every task the gateway ran has ended:
every port it listened on is closed by then (HTTP, RTMP, RTMPS, SRT, the injector's, and each
RIST session's, waiting or running), and every connection it accepted. A gateway can start again
on the same ports at once, in the same process. A session that starts once the shutdown has
begun ends at once, `gateway_stopped`; `Gateway::spawn` runs a task the shutdown waits for.
So does one still dialing the relay when the shutdown begins (a dial can last 10 s, past the
5 s the shutdown waits for its tasks): it signs its record, `gateway_stopped`, with nothing
published. A task still running 5 s on is ended; a RIST listener it held is closed all the same
(`Gateway::hold_rist`: whatever binds one for a task of the gateway's hands it there as it
binds it; `contrib::serve_rist` serves a RIST session at its listener so).
`GatewayServer::start` binds every port and makes every check (the RTMPS identity, the
injector's key and grant) before it spawns a task to serve one. A start refused part way
returns its error with every port it bound free and nothing of its running, so the next start
can bind them at once.
## Resources
| resource | methods |
|---|---|
| `/whip` | `POST` an `application/sdp` offer: `201`, the answer, `Location`, `ETag`. `OPTIONS` (CORS, `Accept-Post`), `GET`, `HEAD` |
| `/whip/s/<id>` | `PATCH` an `application/trickle-ice-sdpfrag`: trickle ICE (`204`) or an ICE restart (`200`, a fragment, a new `ETag`); `DELETE` (`200`) ends the session and its tracks |
| `/whep/<room>/<owner>[?layer=<n>]` | `POST` an offer for the owner's tracks: `201`, or `409` with `Retry-After` while the owner is not live. `HEAD` answers with `Link: <urn:ietf:params:whep>; rel="profile"` (WHEP §4.2) |
| `/whep/s/<id>` | `PATCH`, `DELETE`, as for WHIP |
| `/rtmp` | `POST`: `201`, `{"expires_in", "stream_key", "url"}`: a one-time stream key for an Enhanced RTMP publish under the grant |
| `/metrics` | `GET`, `HEAD`: the gateway's counters in the text exposition format ([`wai/wai-metrics`](../../../../wai/wai-metrics)), for the operator at the gateway's own address; it carries no grant |
`/metrics` holds the requests by resource, method and status (`wai_ingest_requests_total`), the
sessions running by protocol (`wai_ingest_sessions`: WHIP, Enhanced RTMP and contribution publish
in, an RTMP connection counting once its publish is admitted; WHEP sessions are the gateway's
fan-out), the WHIP and WHEP sessions opened, the requests refused, the session records signed,
and `wai_ingest_energy_unmetered`, 1: the gateway's own energy in every record it signs is the
unmetered marker, since no meter backs it. No room, principal, grant, session or address is a
label.
Every request but `OPTIONS` and the operator's `/metrics` carries `Authorization: Bearer <token>`.
**The token is a room grant**: a WAI `WRG1` record (`live-rooms.md` §4), base64url without padding.
A grant is public, so every request that carries one also carries a proof of possession of its key,
`WAI-Room-Proof` (`live-rooms.md` §10.5.3), over a single-use challenge the gateway gave in
`WAI-Room-Challenge`: on a `401` and on each response to a request with a grant. Refusals are
RFC 9457 problem details whose `reason` member is `live-ingest.md` §14's code.
## Admission
The gateway holds each room's signed descriptor (`WRD1`) and checks a token against the
room it names, as the relay checks a token against its grants, here by the room's own
signed records: the grant reads and its signature verifies under the room's authority
(`live-rooms.md` §4.3, its field rules, its fourteen-day bound, its role and the room's
ceiling), it is good now, it is not among the room's revocations, its origins include the
gateway's, a single-use grant has opened no session here, and the request's proof passes
`live-rooms.md` §10.5.4 over a challenge held for its endpoint or session
(`joule_moq::grant_proof::check_proof`, through `gateway::check_http_proof`). What fails is `401` with
`WWW-Authenticate: Bearer error="invalid_token"`. A good grant without the power the
request needs is **`403`** with `error="insufficient_scope"` (RFC 6750 §3.1): the publish
power for each media kind a WHIP offer carries, the subscribe power up to the layer a WHEP
request asks for. A session's resource answers only the token that opened it.
`room.rs` reads descriptors and grants from the extension's text; `tests/room_corpus.rs`
holds it to WAI's corpus `wai/live-conformance/rooms/`: every room id and grant digest,
the registered profiles' records, and every refusal of a descriptor or a grant there, but
an exchanged grant's link and the key log, which a bearer token does not bring. It also
runs the corpus's six HTTP proofs through `check_http_proof`, each with its stated
outcome, and presents each accepted proof again as a replay.
## WHIP into the room
- **Tracks.** Per media kind of the offer, `(room, owner, audio|video)` / `0` and its
receipts track `0_receipts`; and the owner's catalog, `(room, owner)` / `catalog`, with
`catalog_receipts`. All are published (PUBLISH) before the answer goes back. Audio and
the catalog at publisher priority 0, video at 1.
- **Frames.** str0m depacketises RTP into frames: H.264 (RFC 6184), VP8 (RFC 7741), VP9
(RFC 9628), AV1 (its RTP payload format v1.0.0) and Opus (RFC 7587). Each frame is one
object, one `WAI1` envelope:
| codec | capability | payload |
|---|---|---|
| H.264 | `wai.video.avc` | one access unit, Annex B |
| VP8 | `wai.video.vp8` | one frame in IVF (`VP80`), its size from the keyframe header |
| VP9 | `wai.video.vp9` | one frame in IVF (`VP90`), its size from the uncompressed header |
| AV1 | `wai.video.av1` | one temporal unit in SPEC §5's AV1 framing, the temporal delimiter restored, the size from the sequence header |
| Opus | `wai.audio.opus.packet` | one packet |
- **Groups.** Video: one group per keyframe (a PLI goes to the publisher before the first
one, and when a group passes `max_video_group_ms`). Audio: one group per fixed window of
`audio_group_ms` on the room clock. One subgroup stream per group, written as the frames
arrive. A track's first group ID is its first frame's room-clock time in microseconds.
- **Time.** The manifest's `live` member carries the frame's room-clock time `t`, its RTP
time and clock rate, whether it is a keyframe, and on a group's first object the grant
reference (`live-rooms.md` §4.5). The mapping is anchored at a track's first frame's
arrival, `t = A + (R − R0) · 10^6 / rate`, and re-anchored at any frame it would place
more than `max_skew_ms` from its arrival: so **every frame's `t` lies within
`max_skew_ms` of its arrival at the gateway**.
- **Receipts.** When a group ends, the gateway signs its group receipt over every object
and publishes it with the object receipts on the receipts track, chained to the group
before. Every figure is the unmetered marker: no meter backs the gateway. The chain's
alias is derived per session, so a publisher that reconnects starts a new chain.
- **The catalog** is MSF JSON with WAI's `wai_*` fields, published once every track has
begun (or a second after the first did) and again whenever a codec or picture size
changes, and signed like the media, so a relay that checks every track of the room at its
door forwards it.
- **The end.** DELETE (or ICE lost, idle, a bound passed) ends each open group, publishes
its receipts, waits for the relay to acknowledge every stream, and sends PUBLISH_DONE
`TRACK_ENDED` on every track: the relay's subscribers see the tracks end.
## WHEP out of the room
The gateway reads the owner's latest catalog once its receipt verifies, answers the
player's offer with the codecs the tracks carry, and subscribes to each accepted track and
its receipts track from the start of the latest group (the live part from the Next Object,
the rest of that group by a fill), so the player starts at a keyframe. Before a frame
reaches the player, its group's receipts must name its MOQT group and Object ID and the
track's chain, verify under a trusted first hop's key (`whep.trust`; this gateway's own
key by default), chain to the group before, and prove the object's inclusion; the object
must open as an envelope of the track's codec. What fails is dropped and counted in the
session's record; a group whose receipts never come is dropped after `hold_ms`.
**What a WHEP player cannot see:** the receipts. WebRTC carries media only. The gateway
verifies on the player's behalf and records what it dropped and why; a WAI-aware player
subscribes to the same tracks and their receipts tracks over MoQ, through the relay's
WebTransport or WebSocket paths, and verifies every object itself.
A verified group is released when its receipts arrive, at the group's end, so a WHEP
player is up to a video group behind the publisher.
**What the player's sink dispatches first** (live-ingest §9.5). A WAI-aware player writes in
its offer, as SDP capability attributes (RFC 5939, RFC 6871), the WAI capabilities its sink
dispatches, in its order. The gateway reads them with WAI's reference reader (`sdp_caps`,
wai-rs's `src/sdp_caps.rs` compiled here from its source) and forwards, per kind, the owner's
rendition the first of them names at or below the grant's `max_layer`: it answers that
configuration, its payload type alone in the section with `a=acfg`. Where nothing advertised is
published, the floor, the layer the resource names. An offer with none is answered as before,
byte for byte; so is one asking for what this revision does not negotiate
(`caps_unsupported`), with `a=csup`; every other reason of §9.5 is `422`. The session's record
keeps the choice under `detail.caps`.
## Enhanced RTMP into the room
- **Admission.** An encoder has a server URL and a stream key, and cannot answer a
challenge, so the publisher first POSTs to `/rtmp` with its grant and a proof of
possession, admitted as a WHIP request is, and gets a stream key: 128 random bits, good for
one `publish` within `stream_key_ttl_s` (60 s). The `publish` command's stream name is the
key; the grant is admitted again when it is used. A refusal is `onStatus`
`NetStream.Publish.BadName` with the reason as its description.
- **The connection** (`rtmp::chunk`, `rtmp::amf0`, `rtmp::session`): the simple handshake,
the chunk stream with its extended timestamps, Set Chunk Size, Abort, acknowledgements and
pings, and the commands. The `connect` result declares `capsEx` 0x0F and the FourCCs it
carries.
- **Tracks** (`rtmp::tracks`, `rtmp::flv`): legacy AVC and AAC, and `avc1`, `hvc1`, `av01`,
`vp09`, `vp08`, `Opus`, `fLaC` and `mp4a`, through ModEx and multitrack bodies and aggregate
messages. Each RTMP track is bound to a room track at its first packet: under a grant with a
layered video profile, the layer its `onMetaData` entry's size and frame rate match; audio
track 0 only. Each frame is one object: Annex B with the record's parameter sets before a
key access unit that carries none, the AV1 framing with the sequence header, IVF for VP8 and
VP9, ADTS for AAC, a FLAC stream. `live.rtp` is the decode time in µs (`rate` 1 000 000) and
`live.cto` the composition offset; §7's mapping gives `t`.
- **RTMPS** on `rtmp.tls_listen` with a PEM chain and PKCS #8 key (tokio-rustls on the
workspace's rustls and ring).
- **Shutdown.** A publisher that declared `Reconnect` is sent
`NetConnection.Connect.ReconnectRequest` (with `rtmp.reconnect_url` as its `tcUrl`, if set),
its key is good once more, and the session ends when it leaves or after
`reconnect_grace_ms`.
- **Records.** Each publish ends with a session record (`protocol` `ertmp`) whose `detail`
counts what was read and not carried, by reason.
## SCTE 104 into the room's cues
`scte` listens on `injector.listen` (SCTE 104's port is 5167) for one automation system at a
time: `init_request`, `alive_request`, and `multiple_operation_message()`s of splice,
time-signal and splice-null requests with avail, DTMF, segmentation, time, tier and private
descriptor requests. Each is answered (`inject_response`, then `inject_complete_response`
once its cues are signed) with SCTE 104's result codes. `scte::scte104::translate` builds each
SCTE 35 section (`scte::scte35`) on the room's 90 kHz clock and the cues it yields;
`scte::Engine` holds each cue until its splice instant, applies cancels and early returns,
drops what live-ads' break rules would refuse, and signs the cue as a `cue` event of the
injector's principal after its injection record (`WIJ1`, a `meta` event carrying the message,
its arrival and the translation's parameters). The events go out on the principal's events
track at the relay, published from the injector's start. The injector's grant must name its
key and permit `cue` and `meta`.
A relay whose door checks receipts names the room's media kinds in its rules
(`[room, "*", "audio"]`, `[room, "*", "video"]`): an events track carries no receipts.
## Bounds
Each is a field of `[limits]`, `[groups]`, `[timeline]` or `[whep]` in the configuration:
| field | default | past it |
|---|---|---|
| `max_whip_sessions`, `max_whep_sessions` | 64, 256 | `503` with `Retry-After` |
| `max_tracks_per_session` | 2 | `422`; and one track of each kind at most |
| `max_bitrate_kbps` over `bitrate_window_ms` | 8 000 over 2 000 ms | stated per section as `b=AS` in the answer (a grant's publish profile lowers it); a session past it is ended |
| `max_frame_bytes` | 4 MiB | the frame is dropped and counted |
| `max_queue_bytes` | 32 MiB | the session is ended (`relay_backpressure`) |
| `audio_group_ms`, `max_video_group_ms` | 200, 2 000 | — |
| `max_skew_ms` | 250 | the track is re-anchored |
| `whep.hold_ms` | 5 000 | the group is dropped (`receipt_missing`) |
| `rtmp.max_sessions` | 64 | one more connection is closed at accept |
| `rtmp.max_message_bytes` | 8 MiB | the connection ends (`message_too_large`) |
| `rtmp.max_bitrate_kbps` over `bitrate_window_ms` | 40 000 over 2 000 ms | the publish ends (`bitrate_exceeded`) |
| `rtmp.connect_timeout_ms`, `rtmp.idle_timeout_ms` | 15 000, 20 000 | the connection ends |
| `rtmp.stream_key_ttl_s` | 60 | the key is refused (`stream_key_expired`) |
| `injector.max_pending` | 256 | a message is answered 124 |
`gateway.example.toml` shows every field. It holds no secret: the relay token and the
signing key are files, and a room is its public, signed descriptor.
```text
wai-ingest --config gateway.toml # JOULE_MOQ_INGEST_LOG sets the log level
```
Every listener is loopback by default: put a TLS front before the HTTP resources (RFC 9725
§5) and set `media.bind_ip` to an address clients reach. `/metrics` is the operator's: a request
that came through a front, one carrying `X-Forwarded-For` or another of the fields a front sets, is
answered as a path the gateway does not have, and a front that sets none of them is configured not
to pass the path on. Read at the gateway's own address, it carries no CORS field.
## The WebRTC stack
[str0m](https://crates.io/crates/str0m) 0.25.0, pinned, MIT OR Apache-2.0, with
`default-features = false` and its `rust-crypto` backend: DTLS by str0m's own `dimpl`
0.9, SRTP and the rest on RustCrypto, whose elliptic curves (p256 and p384 0.14) run on
crypto-bigint 0.7. The stack builds one C library, aws-lc: each session's self-signed DTLS
certificate is made by `rcgen` on `aws-lc-rs`, which `dimpl`'s certificate feature takes. It
is sans-IO: each session here owns a UDP socket and drives its `Rtc` from a tokio task,
feeding it datagrams and the time and draining its output after every change. It
depacketises and packetises H.264, VP8, VP9, AV1 and Opus at the frame level (AV1 is a
hidden codec in its public API; this crate's tests take AV1 through it end to end), supports
ICE lite and full ICE, ICE restarts, NACK and keyframe requests. Every crate new to the
lockfile is MIT, Apache-2.0 or ISC.
## Uncompressed essence (SMPTE ST 2110)
WAI `extensions/essence-contribution.md`. The readers (`st2110`: the description, RTP and its
sequence, video rows into frames, PCM into blocks, ancillary data into records and the SCTE
104 messages in it) are in every build, with their corpus (`wai/live-conformance/st2110/`,
`joule-moq-ingest-st2110-vectors --out|--check`). The session is built with the feature
`st2110`, which brings the encode stage and the one encoder the crate links (`rav1e`,
BSD-2-Clause, without its assembly: the fallback); the real-time encoders are loaded at run
time and are no build dependency:
| resource | what it does |
|---|---|
| `/st2110` | `POST` with the grant and its proof and `{"sdp": "<the description>", "source": "<a capture's digital source type, optional>"}`: `201`, the session and, for each stream, the UDP `port` to send it to, its clock class and its `TSMODE`. Refusals: a description's own reason (`sdp_*`, 422), `essence_streams_unsupported`, `essence_format_not_carried`, `essence_outside_floor`, `multicast_not_permitted`, `session_conflict` |
| `/st2110/s/<id>` | `DELETE` ends the session |
A whole frame goes onto the video floor as `wai.video.av1`; a frame with a sample missing,
or one that found the encode stage's queue full, is counted and not published. Sound goes
out as `wai.audio.flac`, every sample as it arrived. Each object carries its capture instant;
SCTE 104 messages in the ancillary data go to the injector; every other ancillary data
packet is counted as not carried. The session's record (protocol `st2110`) holds each
stream's statement and every count, the encoder and its stage's counts, and an origin claim
a track where a `source` was stated. The gateway holds no reference clock and claims no
lock. `[st2110]` in `gateway.example.toml` lists the settings; a multicast group is joined
only with `multicast = true`.
### The encode stage
`st2110::encode`: one stage ([`Stage`]) and one trait ([`Backend`]) with three encoders
behind it, chosen by `[st2110] encoder` and probed when the gateway starts.
| `encoder` | what encodes | where it comes from |
|---|---|---|
| `va` | the GPU's AV1 encode engine through VA-API (`libva.so.2`, loaded at run time; MIT), on the render node `va_device` | the host's driver; Linux |
| `svt-av1` | SVT-AV1 in its real-time mode (`libSvtAv1Enc.so.4`, or the path `svt_av1_library`, loaded at run time; BSD-3-Clause-Clear with the AOM Patent License 1.0). The adapter holds the layouts of 4.2 and refuses another version | the operator's build of it |
| `rav1e` | the pure-Rust encoder this crate links, at its fastest preset, with tiles cut for the host's threads | every build: the fallback |
| `auto` | the first of those, in that order, that passes its probe | |
- **Asked for and not here is a refusal.** With `encoder = "va"` or `"svt-av1"` on a host
without it, the gateway does not start and says why. With `encoder_fallback = true` it
starts on the next encoder and says so in its log, in each session's answer (`encoder`:
`backend`, `asked`, `fallback`) and in each session's record. No encoder runs under
another's name.
- **The probe.** Each candidate encodes a few computed pictures at 8 and at 10 bits; its
first temporal unit must hold a sequence header within the floor (Main profile, Main
tier, level 4.1 or below) and state the picture's size. One that does not is refused
with what was found. `POST /st2110` does the same at the stream's own size and depth
before it answers (`essence_encoder_refused`), and every keyframe of every session is
held to the same check: a session whose encoder leaves the floor ends
`encoder_outside_floor`, that unit unpublished.
- **The picture's size.** A hardware engine may code frames larger than the picture, to
its own block alignment: the one measured codes 1920 × 1082 for a picture of
1920 × 1080. The VA-API adapter states the picture's size as the render size in every
frame header (`encode::obu` writes the sequence header and the start of each frame
header, which that interface leaves to the application, and reads both back from what
the driver made); the stage refuses a stream whose keyframes do not state the picture;
the record's `encoder.stage` has `picture` and `coded_frame`.
- **What the gateway says at start**: the encoder, its version and where it was loaded
from, every probe's outcome, and how long computed pictures of 1280 × 720 and
1920 × 1080 took at its fastest setting on this host (`calibrate_ms`; a start-up
estimate, not a measurement of a stream).
- **The real-time budget.** The stage runs on its own thread behind `encode_queue` frames.
It converts 4:2:2 to 4:2:0, asks for a keyframe every `key_interval_ms` of the stream's
timestamps, times every frame against the frame interval, and moves the encoder along
its rungs: faster when a frame takes more than 85% of the interval or two frames wait,
straight to the fastest when a frame was dropped, and toward the better picture only
after a hold with the queue empty and under 45% of the interval used. SVT-AV1's rungs
are its presets from `svt_av1_preset` to 13, changed mid-stream with no keyframe (with
`svt_av1_preset = 0` the first is chosen for the stream: 9 up to 1280 × 720 at 30 frames
a second, 11 up to 1920 × 1080 at 30, 12 above); `rav1e`'s are quantizers, each a new
sequence; a hardware engine has one. `encode_queue = 0` is the frames of two fifteenths
of a second, at least 4: a keyframe takes several frames' time, and the queue absorbs it.
- **The drop policy.** A frame that arrives while the queue is full is not encoded.
Quality is lowered first. Every such frame is counted
(`not_published.frames_encoder_behind_not_published`, and the stage's
`frames_not_encoded_queue_full`), and no encoder here drops a frame of its own.
- **The loops.** `st2110::convert`: 4:2:2 pgroups at 8 and 10 bits into planes, a
word-at-a-time reader for every other pgroup, and chroma at half height by the rounded
mean of two rows; each held bit for bit to a reference that says the same a sample at a
time, at every shape.
- **The receive buffer.** Each stream's socket is asked for `receive_buffer_octets`,
halving the ask while the host refuses it; the host's limit bounds what it gets
(`net.core.rmem_max` on Linux, `kern.ipc.maxsockbuf` on macOS), and the session's answer
and record state it, the record in time at the stream's rate too. A 1920 × 1080 stream at
30 frames a second is about 109 000 datagrams a second, at 60 about 218 000: a default
buffer holds under a millisecond of them.
- **The way in** (`st2110::rx`). Each stream is read by a thread of its own that does
nothing else, into batches through the session's one bounded queue. On Linux one
`recvmmsg` reads up to 64 buffers, `UDP_GRO` lets the kernel hand over a run of datagrams
of one size as one buffer (cut back here at the size it states), `SO_TIMESTAMPNS` stamps
each and `SO_RXQ_OVFL` counts the buffers a full one dropped. Elsewhere one `recvmsg` a
datagram with `SO_TIMESTAMP`; on macOS the thread asks to be favoured
(`QOS_CLASS_USER_INTERACTIVE`). The readers, and so the sequence, loss and completeness
rules, are unchanged. The system calls are in `st2110::sock`, each with its SAFETY note.
- **Arrivals against the sender's type** (`st2110::timing`). Where a video stream's
description names an ST 2110-21 type (`TP`), its arrivals are read against that type's
Network Compatibility Model and the record counts the frames outside it; it is a reading
at the gateway's socket, not the sender's compliance (`sender_type_checked` stays `"no"`).
- **The test sender** (`st2110::pace`): ST 2110-21's linear schedule, four packets at an
instant and never sooner than the model drains them, by `sendmmsg` with `UDP_SEGMENT` on
Linux and one `send` a datagram elsewhere. `tests/essence_rx.rs` holds the way in to
1280 × 720 at 30 and 1920 × 1080 at 30 and 60 frames a second without loss, on request.
`examples/st2110_profile.rs` prints where a frame's time goes on the host it is run on
(reader, conversion, encode, packaging). `tests/essence_rt.rs` holds a real session to its
frame rate: 1280 × 720 and 1920 × 1080 at 30 frames a second for 20 seconds with each
real-time encoder the host has, failing on one frame dropped behind the encoder, and
refusing to measure on a loaded host. Every temporal unit is decoded by the host's libdav1d
(`libdav1d.so.7`, loaded at run time; BSD-2-Clause) and held to the picture sent, and the
first frames a second time by `wai-av1`, this repository's own decoder, sample for sample
against the first decode. What each encoder held on which host, dated, is in the
extension's §12.
## The telephony bridge (SIP and RTP)
WAI `extensions/telephony-bridge.md`. The readers and the user agent's answers (`telephony`:
SIP messages, digest authentication with MD5 written from RFC 1321, offers and answers, RTP,
G.711, telephone events, the order and form of every answer) are in every build, with their
corpus (`wai/live-conformance/telephony/`, `joule-moq-ingest-telephony-vectors --out|--check`).
The bridge is built with the feature `telephony` and started by `wai-ingest` where
`[telephony] enabled = true`; it links nothing the crate did not already have.
- **One peer.** A SIP trunk or a PBX at `peer`, over UDP, TCP or TLS (`sip_udp`, `sip_tcp`,
`sip_tls`; port 0 takes one the host gives, and the bridge logs it). Requests from any other
address are dropped and counted. The bridge's own requests go to the peer over UDP or TCP. A
dialog's `Contact` is the listener of the transport it was made over (`;transport=tcp` over
TCP, a `sips:` URI over TLS), so the peer's requests in it come back the same way.
- **A call into the room.** A number maps to grants (`[[telephony.number]] grants`), or to PIN
entry (`pin = true`, then `[[telephony.pin]]`, the PIN read from a file). The call publishes
one audio track of the grant's principal: G.711 as `wai.audio.flac` (every character's
sample, 20 ms an object, 100 ms groups), Opus as `wai.audio.opus.packet` as it came, each
object stamped with its capture instant, every group receipted as the first hop.
- **The room back.** Each room's mix subscribes to the tracks its calls hear (the other calls,
and `hear_owners`), verifies every object against its receipt, decodes FLAC at 8 kHz, and
every 20 ms sends each call the sum less its own, in the call's law.
- **Placing a call and registering.** `Bridge::place_call` dials the peer; with
`[telephony.outbound] register = true` the bridge registers and keeps its registration.
- **The record** of each call (protocol `telephony`): its statement, its RTP counts by reason,
the jitter buffer's, what it heard of each track, its origin claim where `source` is set. No
record says who called unless `caller_in_record = true`; none holds a PIN or a password.
`tests/telephony_bridge.rs` runs on loopback with the tests' own SIP peer and RTP callers
(`tests/common/telephony.rs`): two calls hearing each other through the room, sample for
sample, their tracks verified at a relay subscriber; credentials, a replayed digest and a PIN;
the planted faults over the wire, among them a call from an unconfigured peer, each with its
reason and the call going on after it; Opus carried as it came; calls over TCP and TLS; a call
the bridge places, challenged, and its registration. `tests/telephony_rt.rs` (ignored unless
asked for) holds one call to real time for 30 seconds both ways and carries 64 calls at once,
the default `max_calls`.
`tests/telephony_interop.rs` (ignored unless asked for) holds the bridge to an outside SIP user
agent: baresip 4.13.0 with libre 4.13.0 (BSD-3-Clause), built from their pinned sources and
named by `TELEPHONY_BARESIP` (WAI decisions D58 has the hashes and the build). It refuses to run
unless loopback is its only network interface: run it in a namespace of its own, as its header
shows. Three calls: the softphone calls in over UDP and over TCP (a PIN entered as its own RFC
4733 events), answering a SHA-256 digest challenge, and is called by the bridge; each time its
sound is its track, verified sample for sample, the room's other caller hears it exactly, and
it hears the room.
## SRT and RIST
| resource | methods |
|---|---|
| `/srt` | `POST` with the grant and its proof: `201`, a one-time session as JSON: its `stream_id`, `passphrase`, `pbkeylen`, the listener's `latency_ms`, `host` and `port`. With `{"call": "ip:port", "passphrase": …}`, the gateway calls the publisher's SRT listener, where `[contrib.srt] call_allow` lists it |
| `/rist` | `POST` with the grant and its proof, `{"profile": "simple"|"main"}`: `201`, the session's `port` (Simple: an even port and the one above), its `cname`, its `buffer_ms`, and for the Main Profile the PSK `passphrase` and its `aes` |
| `/srt/s/<id>`, `/rist/s/<id>` | `DELETE` ends the session, or one not yet connected; `GET`, `HEAD` |
The SRT listener (`[contrib.srt] listen`) checks each caller's stream ID
(`#!::r=<room>/<owner>,m=publish,s=<session>`) against the sessions it gave, and refuses with
the codes of live-ingest §13 (1401, 1402, 1403, 1404, 1405, 1409) and the draft's (1010 for a
wrong passphrase, 1011 for none). A RIST session's port takes the first flow whose RTCP CNAME
is the session's. The link's transport stream is demultiplexed: the first H.264 or HEVC
stream and the first AAC stream go to the video and audio tracks as WHIP's frames do, one
access unit (one ADTS frame) an object, the PTS on the room clock; a PES missing a packet is
dropped whole. The catalog declares the owner's `link` track.
**The link record** (`linkrec`, `WLK1`): every `[contrib] record_interval_ms` and when the
session ends, the gateway signs a part: the sequence numbers passed, each delivered at the
first try, recovered, dropped or outstanding, the loss runs, requests, retransmissions, round
trips and recovery times, chained to the part before and naming each media track's receipt
chain head. Each part goes out on the `link` track with its receipts, is kept
(`Gateway::link_records`) and is appended to `[summary] link_out_path`. The corpus
`wai/live-conformance/links/` is written by `joule-moq-ingest-link-vectors`.
RIST is built only with the feature `rist` (`joule-contrib`'s README, "Patents").
## The session record, and live metering
Each session ends with a record of what it carried, signed by the gateway's key
(`record.rs`, format `joule-moq-ingest/session/1`): per track and direction the objects and
bytes, the drops by reason, subscriber sessions and their time by layer class, the grant
with its ceilings and the bytes under it, and the gateway's energy as the unmetered marker.
`Gateway::records` keeps them and `summary.out_path` appends them as JSON lines.
These are the counts of the per-hop session record `WMH1` of WAI's live metering, kept in
its shape, role 3 (ingest gateway). Once that record lands, a `record::HopSink` that writes
`WMH1` turns each record into its track entries, subscriber time and ledger entry and signs
it with the same key, which the room's key log lists under `live/ingest`; nothing else in
the gateway changes.
## Tests
All on loopback, every wait bounded, the relay checking every receipt at its door:
| test | what it shows |
|---|---|
| `whip_publish` | A WHIP publisher (str0m as the client) sends three seconds of H.264 and Opus. A subscriber at the relay receives every group of both tracks, verifies every receipt under the gateway's key and every chain link, and gets the frames' bytes as sent; one group per keyframe, every audio group within its window; DELETE ends the tracks with PUBLISH_DONE; the session's record verifies. |
| `whep_play` | Before the publisher is live a WHEP POST is `409` with `Retry-After`. Then a WHEP player (str0m) gets the publisher's frames, byte for byte, from the keyframe of the group it joined to the last group closed; the WHEP session ends with the tracks, nothing refused. |
| `whep_caps` | An Enhanced RTMP publisher sends AV1 at layer 0 and H.264 at layer 1 (the `camera-720` profile) with Opus; WHEP players state their sinks' capabilities. H.264 first: the answer is its configuration, `a=acfg` and one payload type, and the player gets layer 1's frames byte for byte from a keyframe. AV1 first: layer 0 in AV1. No attributes, and a required option the gateway does not support: the floor, no `a=acfg` (the second with `a=csup`). Attributes that do not hold together: `422` with the reason. Each record keeps the choice. |
| `auth_and_ice` | No token `401`; a token that is not a grant `401 invalid_token`; a viewer's grant `403 insufficient_scope`; a grant past its window, one of another room; `415`; `422`; WHEP without subscribe or above `max_layer` `403`; discovery and CORS. `PATCH` without `If-Match` `428`, stale `412`, trickle `204`, another principal's grant, its own key proved, `403`; a `PATCH` or `DELETE` presenting the grant without a proof `401 pop_missing`; an ICE restart (`If-Match: *`) `200` with new credentials and a new `ETag`, ICE connecting again, and every frame before and after it at the relay. |
| `codecs` | VP8, VP9 and AV1 each through WHIP: every frame whole, one group per keyframe, the size each framing states. |
| `room_corpus` | The room reader against `wai/live-conformance/rooms/`, and its six HTTP proofs of possession through the gateway's proof step, each accepted proof also refused as a replay. |
| `ertmp` | A publisher of our own over RTMP and RTMPS: legacy AVC and AAC; enhanced HEVC with composition offsets and Opus; AV1 with FLAC and VP9 with AAC, a ModEx nanosecond offset; multitrack layers bound by the grant's profile; every refusal of a stream key; the reconnect request at shutdown. Every frame reaches a relay subscriber in its payload framing, at its mapped time, every receipt verified. |
| `scte104_injection` | An automation system of our own over TCP: init, alive, a second connection refused 110, splices with and without pre-roll, a cancel, time signals, refusals with their result codes; the chain verifies and every cue is its message's translation; a relay subscriber reads the chain from the events track. |
| `conformance_corpus` | `wai/live-conformance/ertmp/` and `wai/live-conformance/injection/` regenerate byte for byte; the texts' reason tables list the codes reported. |
| `ertmp_ffmpeg` (ignored; `--ignored` where a local encoder is present) | A local encoder as the test client: AVC with B-frames and AAC, HEVC and AVC on the platform's media engine, HEVC with Opus, AV1 with FLAC, VP9 with Opus; every frame decoded from what the relay carries. |
| `contrib_srt` | An SRT publisher, through a path that drops datagrams each way, into the gateway: a relay subscriber gets every H.264 access unit and AAC frame, every receipt verified; the link record's parts verify as a series, show the losses recovered and name each track's receipt-chain head as the subscriber found it, and are on the link track. Every listener refusal with its code; the gateway calling an encoder's listener with HEVC; 20 Mbit/s with groups of megabytes through the relay's door. |
| `contrib_rist` (feature `rist`) | A RIST Simple Profile flow and a Main Profile tunnel under PSK into the gateway, received whole; another CNAME opens no session. |
| `shutdown_ports` | From a task on a runtime of one worker, the gateway shut down and started again on the same ports many times: when the shutdown returns, its HTTP, RTMP and SRT ports and a RIST session's ports (waiting or running, feature `rist`) bind at once, the HTTP connection it kept alive and an RTMP connection mid-handshake are closed, and the SRT and RIST sessions that ran have signed their records. A RIST session whose relay does not answer its dial signs its record, `gateway_stopped`, and its ports bind at once; so do a RIST listener's held by a task the shutdown ends past its bound. |
| `start_ports` | From a task on a runtime of one worker, a start on fixed ports refused part way (the RTMP, RTMPS, SRT or SCTE 104 port held, or the RTMPS identity spoiled): when it returns, every other port binds at once, and a start with nothing held comes up on all of them. |
| `link_corpus` | Every vector of `wai/live-conformance/links/` with its stated outcome. |
| `contrib_interop` (ignored) | The reference SRT caller and RIST sender into the gateway, with an encoder's H.264, HEVC and AAC through lossy paths (tools named by `WAI_SRT_LIVE_TRANSMIT`, `WAI_RISTSENDER`, `WAI_FFMPEG`, `WAI_FFPROBE`). |
| `contrib_bench` (ignored) | The gateway's throughput and energy on an SRT link, the sender in a process of its own. |
## Not implemented
`vvc1` over Enhanced RTMP; a stream key another gateway accepts after a reconnect request;
SCTE 104 in a video signal's ancillary data (`cue-injection.md` §10); SRT and RIST egress; a
WHEP counter-offer;
a player's keyframe request carried upstream; publishing into a slot; simulcast within one
WHIP session; STUN and TURN in `Link` headers; reading the key log itself (revocations are
the list a deployment configures).
## Licence
Apache-2.0.