live-contribution: SRT and RIST contribution ingest, with a signed link record #483

Closed
dcharlot wants to merge 0 commits from wai/ingest-srt-rist into main
Owner

What is in it

SRT and RIST contribution into a room's tracks. Each access unit becomes a WAI object with
receipts, and the gateway signs a record of the link: loss, recovery and latency for every
sequence number.

  • joule-contrib is a new crate in joule-code (Apache-2.0, pure Rust, no C library).
    • SRT (draft-sharabayko-srt-01): a listener that serves many callers on one port, and a
      caller. It has the HSv5 caller-listener handshake with the SYN cookie, the stream ID, HSREQ
      and KMREQ, and the rejection codes. In live mode it has TSBPD over the negotiated latency,
      too-late drop, full and light ACK, ACKACK with the RTT estimate, immediate and periodic
      NAKs, and retransmission. Payloads are encrypted with AES-CTR, and the key is wrapped
      (RFC 3394) under a key from PBKDF2-HMAC-SHA1. SHUTDOWN is sent three times.
    • RIST (VSF TR-06-1:2020 Simple Profile, VSF TR-06-2:2024 Main Profile), behind the
      default-off feature rist. The Simple Profile carries RTP on P and RTCP on P+1, with
      SR/RR/SDES. It requests what is missing with range or bitmask requests, measures the RTT
      with the RTT echo, and delivers each packet a buffer after it was expected. The Main
      Profile runs GRE over UDP in reduced overhead mode, with the VSF EtherType or the legacy
      one and with keep-alives. Its PSK key comes from PBKDF2-HMAC-SHA256 over the GRE nonce,
      with AES-CTR on the GRE sequence number.
    • The rest of the crate: an MPEG-TS demultiplexer (PAT and PMT CRCs, PES, the PTS
      extended past 33 bits, a PES with a missing packet dropped whole) and a multiplexer and
      pacer; stats::Account, which records what became of every sequence number; a seeded
      lossy UDP proxy for tests; and the tools joule-contrib-send, -recv and -bench.
  • joule-moq-ingest:
    • POST /srt and POST /rist open a one-time contribution session under the same bearer
      token and proof as WHIP. The SRT listener admits callers by their stream ID, the gateway
      can call an encoder's SRT listener when its address is in call_allow, and each RIST
      session gets its own port.
    • The transport stream feeds the existing packager. H.264 and HEVC become one access unit
      per object, and AAC one ADTS frame per object, with receipts as in live-ingest.
    • The link record WCL1 (linkrec): binary and Ed25519-signed, in parts chained by
      parent digest. Each part states the account of its sequence numbers, the loss runs (512 at
      most, with a truncated flag) and each track's receipt-chain head. Parts go on the session's
      link track and, optionally, to a file.
    • The bin joule-moq-ingest-link-vectors writes and checks the corpus.
  • joule-moq-relay: two new limits (see the design decisions).
    • limits.quic_stream_window_bytes, 16 MiB by default.
    • limits.quic_connection_window_bytes, 64 MiB by default and never below the stream
      window. It bounds what one session can leave the relay holding unread.
  • WAI:
    • extensions/live-contribution.md (wai.live.contribution, registered): admission, the
      transports as profiled, the stream onto the tracks, the link record and its reading,
      refusals, bounds, status, patents, and security.
    • The corpus live-conformance/links/.
    • An independent verifier, live_contribution_verify.py. It is written from the text alone
      and uses the standard library only.
    • Pointers from live-ingest §13 and §17, SPEC §5 and the CHANGELOG.
  • CI:
    • joule-code-conformance runs joule-contrib with and without rist and
      joule-moq-ingest with rist, and checks that the corpus regenerates byte for byte.
    • wai-conformance runs the verifier, and the extension's text is in its paths.

Design decisions

  • RIST is behind a default-off feature. TR-06-1 and TR-06-2 each carry an intellectual
    property notice: a patent is claimed to read on them, with an assurance of licensing to
    implementers. Whether a distributed build carries RIST is a licensing decision, so the
    default build carries none of it (spec §10, crate README). Owner decision.
  • The record states what happened to every sequence number: delivered at the first try,
    recovered within the window, dropped, or outstanding. Those four sum to the span. Loss runs
    and recovery times are stated beside them, so a reader can check the totals against each
    other. A verifier recomputes every identity.
  • The relay's stream window. A door holds each object until its group's receipts arrive,
    and does not read the stream while it holds. The receipts follow the group's last object.
    A group larger than QUIC's default stream window of about 1.25 MB therefore stops its own
    stream, and the door refuses it after its hold. Contribution at 20 Mbit/s with a 1 s GOP
    sends groups of about 2.6 MB. The window is now configurable, 16 MiB by default. An
    earlier commit on this branch reverted the change because of a falsification that was
    wrong: the build host runs a snapshot commit whose parent is the branch head, so "HEAD~1"
    there still had the change. The next commit restores it, and the falsification below was
    redone with git apply -R.
  • A session's connection window. The relay set each stream's receive window but not
    the connection's, and QUIC's default connection window is unbounded. A session could
    therefore leave max_uni_streams (1024) streams each a stream window full: about
    1.25 GiB with QUIC's default stream window, 16 GiB with 16 MiB. A publisher that opens
    streams and finishes no group could push the relay past max_resident_mb, and then the
    relay exits for every room on it. limits.quic_connection_window_bytes now bounds the
    session over all its streams. Its default, 64 MiB, is the budget websocket_buffer_bytes
    gives a WebSocket session and four stream windows: a group a door holds whole, the next
    group's start, its receipts and the session's other tracks. The relay raises a setting
    below the stream window to the stream window.
  • RIST's rate is bounded by its sequence space. RTP sequence numbers are 16 bits, so a
    RIST buffer must hold fewer than 32 768 packets. The gateway bounds a RIST session at
    32 768 × 1 316 × 8 bits per buffer (344 981 kbit/s at 1 000 ms) and ends it with
    bitrate_exceeded above that (§8). The bench's unbounded RIST runs show why (below).
  • One frame per object. Each object carries one access unit or one ADTS frame, so
    receipts and the door work as they do for WHIP ingest.

Checks

On the Framework box (192.168.4.47, Linux x86_64, 32 threads), via
[email protected] ~/kani-logs/remote/rb <worktree> -- '<cmd>', at head
27a666cff (main merged in), and again at b54dc94c1 for the relay suite, the connection window and the tests that pass the door at 20 Mbit/s (joule-moq-ingest with rist: 46 passed), with commands run in joule-code/joule-code-rs unless noted:

check command result
joule-contrib cargo test --locked -p joule-contrib --all-targets 30 passed, 0 failed
joule-contrib with RIST … --features rist 43 passed, 0 failed
joule-moq-ingest cargo test --locked -p joule-moq-ingest --all-targets 44 passed, 0 failed
joule-moq-ingest with RIST … --features rist 46 passed, 0 failed
joule-moq-relay cargo test --locked -p joule-moq-relay --all-targets 95 passed, 0 failed (at b54dc94c1)
a session's connection window cargo test --locked -p joule-moq-relay --test connection_window 3 of 3 pass. Window 4 MiB, stream window 1 MiB, 32 streams, each a group held at the door and never finished: the publisher stalls having sent 4 190 208 bytes, and only the first 4 groups reach the door
the door at 20 Mbit/s, with the connection window cargo test --locked -p joule-moq-ingest --test contrib_srt twenty_megabits 2 of 2 pass in each of 2 rounds; the gateway bench at 54.62 Mbit/s refused nothing at the door
link corpus regenerates cargo run --locked -q -p joule-moq-ingest --bin joule-moq-ingest-link-vectors -- --check ../../wai/live-conformance/links the corpus regenerates byte for byte
clippy cargo clippy --locked --no-deps -p joule-contrib -p joule-moq-ingest --all-targets [--features joule-moq-ingest/rist,joule-contrib/rist] -- -D warnings clean, with and without the features
independent verifier python3 -I live-conformance/live_contribution_verify.py live-conformance/links (in wai) PASS: every vector as stated, every mutated copy refused, every required dropped check caught
registry cargo run -q --no-default-features --bin wai -- registry export --check; cargo test --no-default-features --lib codecs::registry (in wai/wai-rs) up to date; 14 passed

On the build host (Studio, macOS arm64), via plain rb, with the reference tools built
locally (test peers only, never shipped):

check command result
transport interop cargo test --locked -p joule-contrib --features rist --test interop -- --ignored --test-threads=1 5 passed: their SRT caller into this listener and this caller into their listener (in the clear and with 128-, 192- and 256-bit keys between them, through a lossy path), their caller with a wrong passphrase refused, their RIST senders of both profiles into this receiver and this sender into their receiver, byte for byte
gateway interop cargo test --locked -p joule-moq-ingest --features rist --test contrib_interop -- --ignored --test-threads=1 2 passed in each of 4 rounds at head: their SRT caller with H.264 and with HEVC, and their RIST Main sender under PSK, into the gateway through 2 % loss each way. Every frame arrived whole with its receipts verified, and in every round the link's account read 0 dropped, 24 to 33 of about 1 140 packets recovered, and every byte of the input delivered. One earlier run, on the tree before main was merged in, failed the SRT case at the frame comparison. Its cause is not known: it predates the account diagnostic, and the host's load average was 13 to 47 then

Local repo checks, all clean: check-public-names.sh (with the private patterns) over the
changed files, the commit messages and this body; check-tests-in-ci.py;
check-workflow-concurrency.sh; check-spec-refs.py; check-wai-section-refs.py;
check-no-vanity-metrics.sh; test-public-text-checks.sh; check-comparative-prose.sh;
check-orphan-sources.py; and the YAML parse of every workflow. wfsteps.py HEAD <merge-base>
reports only this branch's three workflow changes (one changed body, two new steps), with no
step missing run:/uses: and no duplicate names.

Falsifications

Each mutation was applied to the committed tree in the run's snapshot. Each test meant to
catch it failed, and the tree was then restored. The baseline passes first.

mutation test result
the relay's connection window removed (git apply -R of its two lines) connection_window fails: the publisher sends 33 554 432 bytes (each of the 32 streams a stream window full) and all 32 groups are held
the relay's stream window removed (git apply -R of the change) contrib_srt twenty_megabits 2 of 2 fail (no PUBLISH_DONE, no session record)
the link record's account rules not enforced link_corpus fails
a series part's parent digest not checked link_corpus fails
SRT's receiver sends no NAK srt_link losses_each_way… fails
RIST's range requests never sent rist_link …range_requests fails
RFC 3394 unwrap's integrity check skipped srt_link refusals_carry_their_codes fails
RIST's rate bound removed contrib::tests fails
SRT's payload IV shifted by one byte (Studio) srt_link (both ends ours) / interop srt (reference tools) loopback still passes (both ends ours agree); against the reference tools 2 of the 3 SRT tests fail (the third, a wrong passphrase refused, carries no payload)

The verifier also refuses every in-memory mutation of the accepted records and series. It
drops each of its own checks in turn and requires the corpus to catch the weakened verifier;
two checks (a small-order R point, and the first-try count alone) have no isolating vector,
and it says so.

The gateway bench was also falsified by length. With QUIC's default window it refused at
the door at 4, 6 and 10 s; with the window, it passed 2 of 2 at 10 s (Framework).

Throughput and energy

Energy uses EN-A's profiles. Its class is OnChipCounter. The scope is stated per host.
Every figure comes from three rounds.

Studio (macOS arm64), macos-task profile, scope = the bench process. In the link bench
that process holds both ends of the link (and the lossy proxy when there is loss). In the
gateway bench it holds the gateway and the relay, but not the sender. The host was shared
with CI, at a load average of 12 to 32.

link (joule-contrib-bench) payload rate J/GB (median, range)
SRT AES-128, 20 Mbit/s 20.0 Mbit/s 16.8 (15.5–21.0)
SRT AES-128, 20 Mbit/s, 1 % loss each way 20.0 Mbit/s, 163–171 recovered, 0 dropped 21.6 (19.4–26.9)
RIST Simple, clear, 20 Mbit/s 20.0 Mbit/s 10.1 (9.9–11.2)
RIST Main PSK-128, 20 Mbit/s, 1 % loss 20.0 Mbit/s, 166 recovered, 0 dropped 19.8 (19.2–24.3)
SRT clear, as fast as it goes 925–997 Mbit/s 40.5 (39.7–40.9)
SRT AES-128, as fast as it goes 587–600 Mbit/s 70.9 (70.2–71.2)
RIST Main PSK-128, as fast as it goes 541–548 Mbit/s 74.5 (71.2–75.0)
gateway path (contrib_bench: SRT AES-128 into the gateway, every object with receipts into a relay whose door checks them; 15 s) payload rate objects J/GB (median, range)
sender at 20 000 kbit/s 21.93 Mbit/s 1 152 27.3 (26.7–30.3)
sender at 50 000 kbit/s 54.62 Mbit/s 1 152 23.1 (22.3–24.8)

The door refused nothing in any round.

Framework (Linux x86_64), throughput. The linux-rapl package counter is shared by every
process on the box. Its bracket (the run less an idle period of the same length) did not
resolve: across the runs it ranged from −278 to +448 J, negative in several. So no energy
figure is given from this host.

link payload rate
SRT clear, as fast as it goes 4.60–5.04 Gbit/s, 0 dropped
SRT AES-128, as fast as it goes 4.28–4.42 Gbit/s, 0 dropped
RIST Main PSK-128, the sender unbounded 213–686 Mbit/s delivered, millions dropped: the sender ran far past the 16-bit sequence space within the buffer, which is the case the gateway's RIST rate bound now refuses

Hardware

  • The Studio (M3 Ultra, macOS arm64) ran the interop tests against the reference tools, the
    IV falsification, and every energy figure (macos-task).
  • The Framework box (Zen 5, Linux x86_64) ran every unit, integration and gateway test,
    clippy, the verifier, the registry export, the falsifications and the throughput figures.
  • Not applicable to this slice: NVENC, VA-API and the NPU (no encode here). jetson-hub was
    not used.

Not done

  • SRT egress (m=request) is refused 1405.
  • A RIST receiver as the tunnel's client; the Main Profile's multiplexing of several flows
    in one tunnel.
  • SRT key refresh as a sender (the receiver accepts refreshed keys).
  • DTLS, EAP-SRP, FEC, bonding and null-packet deletion are out of scope (spec §1).
  • An IPR check of the SRT draft. The IETF datatracker's IPR search could not be reached
    from here.
  • joule-moq-relay has two clippy lints on code this branch does not touch
    (server.rs:173, session.rs:359). jwp fails clippy's manual_checked_div when its
    lints are not capped. Both predate this branch.
  • joule-moq-relay's tests/age_rising.rs failed 1 run in 3 on the base commit, before this
    branch changed anything.

🤖 Generated with Claude Code


Closed: Landed on main through merge train E (#499, merge bf39ec9a); verified by content: this PR's head is in main

## What is in it SRT and RIST contribution into a room's tracks. Each access unit becomes a WAI object with receipts, and the gateway signs a record of the link: loss, recovery and latency for every sequence number. - **`joule-contrib`** is a new crate in joule-code (Apache-2.0, pure Rust, no C library). - **SRT** (`draft-sharabayko-srt-01`): a listener that serves many callers on one port, and a caller. It has the HSv5 caller-listener handshake with the SYN cookie, the stream ID, HSREQ and KMREQ, and the rejection codes. In live mode it has TSBPD over the negotiated latency, too-late drop, full and light ACK, ACKACK with the RTT estimate, immediate and periodic NAKs, and retransmission. Payloads are encrypted with AES-CTR, and the key is wrapped (RFC 3394) under a key from PBKDF2-HMAC-SHA1. SHUTDOWN is sent three times. - **RIST** (VSF TR-06-1:2020 Simple Profile, VSF TR-06-2:2024 Main Profile), behind the default-off feature `rist`. The Simple Profile carries RTP on P and RTCP on P+1, with SR/RR/SDES. It requests what is missing with range or bitmask requests, measures the RTT with the RTT echo, and delivers each packet a buffer after it was expected. The Main Profile runs GRE over UDP in reduced overhead mode, with the VSF EtherType or the legacy one and with keep-alives. Its PSK key comes from PBKDF2-HMAC-SHA256 over the GRE nonce, with AES-CTR on the GRE sequence number. - **The rest of the crate:** an MPEG-TS demultiplexer (PAT and PMT CRCs, PES, the PTS extended past 33 bits, a PES with a missing packet dropped whole) and a multiplexer and pacer; `stats::Account`, which records what became of every sequence number; a seeded lossy UDP proxy for tests; and the tools `joule-contrib-send`, `-recv` and `-bench`. - **`joule-moq-ingest`**: - `POST /srt` and `POST /rist` open a one-time contribution session under the same bearer token and proof as WHIP. The SRT listener admits callers by their stream ID, the gateway can call an encoder's SRT listener when its address is in `call_allow`, and each RIST session gets its own port. - The transport stream feeds the existing packager. H.264 and HEVC become one access unit per object, and AAC one ADTS frame per object, with receipts as in live-ingest. - **The link record `WCL1`** (`linkrec`): binary and Ed25519-signed, in parts chained by parent digest. Each part states the account of its sequence numbers, the loss runs (512 at most, with a truncated flag) and each track's receipt-chain head. Parts go on the session's link track and, optionally, to a file. - The bin `joule-moq-ingest-link-vectors` writes and checks the corpus. - **`joule-moq-relay`**: two new limits (see the design decisions). - `limits.quic_stream_window_bytes`, 16 MiB by default. - `limits.quic_connection_window_bytes`, 64 MiB by default and never below the stream window. It bounds what one session can leave the relay holding unread. - **WAI**: - `extensions/live-contribution.md` (`wai.live.contribution`, registered): admission, the transports as profiled, the stream onto the tracks, the link record and its reading, refusals, bounds, status, patents, and security. - The corpus `live-conformance/links/`. - An independent verifier, `live_contribution_verify.py`. It is written from the text alone and uses the standard library only. - Pointers from live-ingest §13 and §17, SPEC §5 and the CHANGELOG. - **CI**: - `joule-code-conformance` runs `joule-contrib` with and without `rist` and `joule-moq-ingest` with `rist`, and checks that the corpus regenerates byte for byte. - `wai-conformance` runs the verifier, and the extension's text is in its paths. ## Design decisions - **RIST is behind a default-off feature.** TR-06-1 and TR-06-2 each carry an intellectual property notice: a patent is claimed to read on them, with an assurance of licensing to implementers. Whether a distributed build carries RIST is a licensing decision, so the default build carries none of it (spec §10, crate README). **Owner decision.** - **The record states what happened to every sequence number:** delivered at the first try, recovered within the window, dropped, or outstanding. Those four sum to the span. Loss runs and recovery times are stated beside them, so a reader can check the totals against each other. A verifier recomputes every identity. - **The relay's stream window.** A door holds each object until its group's receipts arrive, and does not read the stream while it holds. The receipts follow the group's last object. A group larger than QUIC's default stream window of about 1.25 MB therefore stops its own stream, and the door refuses it after its hold. Contribution at 20 Mbit/s with a 1 s GOP sends groups of about 2.6 MB. The window is now configurable, 16 MiB by default. An earlier commit on this branch reverted the change because of a falsification that was wrong: the build host runs a snapshot commit whose parent is the branch head, so "HEAD~1" there still had the change. The next commit restores it, and the falsification below was redone with `git apply -R`. - **A session's connection window.** The relay set each stream's receive window but not the connection's, and QUIC's default connection window is unbounded. A session could therefore leave `max_uni_streams` (1024) streams each a stream window full: about 1.25 GiB with QUIC's default stream window, 16 GiB with 16 MiB. A publisher that opens streams and finishes no group could push the relay past `max_resident_mb`, and then the relay exits for every room on it. `limits.quic_connection_window_bytes` now bounds the session over all its streams. Its default, 64 MiB, is the budget `websocket_buffer_bytes` gives a WebSocket session and four stream windows: a group a door holds whole, the next group's start, its receipts and the session's other tracks. The relay raises a setting below the stream window to the stream window. - **RIST's rate is bounded by its sequence space.** RTP sequence numbers are 16 bits, so a RIST buffer must hold fewer than 32 768 packets. The gateway bounds a RIST session at 32 768 × 1 316 × 8 bits per buffer (344 981 kbit/s at 1 000 ms) and ends it with `bitrate_exceeded` above that (§8). The bench's unbounded RIST runs show why (below). - **One frame per object.** Each object carries one access unit or one ADTS frame, so receipts and the door work as they do for WHIP ingest. ## Checks On the Framework box (`192.168.4.47`, Linux x86_64, 32 threads), via `[email protected] ~/kani-logs/remote/rb <worktree> -- '<cmd>'`, at head `27a666cff` (main merged in), and again at `b54dc94c1` for the relay suite, the connection window and the tests that pass the door at 20 Mbit/s (joule-moq-ingest with `rist`: 46 passed), with commands run in `joule-code/joule-code-rs` unless noted: | check | command | result | |---|---|---| | joule-contrib | `cargo test --locked -p joule-contrib --all-targets` | 30 passed, 0 failed | | joule-contrib with RIST | `… --features rist` | 43 passed, 0 failed | | joule-moq-ingest | `cargo test --locked -p joule-moq-ingest --all-targets` | 44 passed, 0 failed | | joule-moq-ingest with RIST | `… --features rist` | 46 passed, 0 failed | | joule-moq-relay | `cargo test --locked -p joule-moq-relay --all-targets` | 95 passed, 0 failed (at `b54dc94c1`) | | a session's connection window | `cargo test --locked -p joule-moq-relay --test connection_window` | 3 of 3 pass. Window 4 MiB, stream window 1 MiB, 32 streams, each a group held at the door and never finished: the publisher stalls having sent 4 190 208 bytes, and only the first 4 groups reach the door | | the door at 20 Mbit/s, with the connection window | `cargo test --locked -p joule-moq-ingest --test contrib_srt twenty_megabits` | 2 of 2 pass in each of 2 rounds; the gateway bench at 54.62 Mbit/s refused nothing at the door | | link corpus regenerates | `cargo run --locked -q -p joule-moq-ingest --bin joule-moq-ingest-link-vectors -- --check ../../wai/live-conformance/links` | the corpus regenerates byte for byte | | clippy | `cargo clippy --locked --no-deps -p joule-contrib -p joule-moq-ingest --all-targets [--features joule-moq-ingest/rist,joule-contrib/rist] -- -D warnings` | clean, with and without the features | | independent verifier | `python3 -I live-conformance/live_contribution_verify.py live-conformance/links` (in `wai`) | PASS: every vector as stated, every mutated copy refused, every required dropped check caught | | registry | `cargo run -q --no-default-features --bin wai -- registry export --check`; `cargo test --no-default-features --lib codecs::registry` (in `wai/wai-rs`) | up to date; 14 passed | On the build host (Studio, macOS arm64), via plain `rb`, with the reference tools built locally (test peers only, never shipped): | check | command | result | |---|---|---| | transport interop | `cargo test --locked -p joule-contrib --features rist --test interop -- --ignored --test-threads=1` | 5 passed: their SRT caller into this listener and this caller into their listener (in the clear and with 128-, 192- and 256-bit keys between them, through a lossy path), their caller with a wrong passphrase refused, their RIST senders of both profiles into this receiver and this sender into their receiver, byte for byte | | gateway interop | `cargo test --locked -p joule-moq-ingest --features rist --test contrib_interop -- --ignored --test-threads=1` | 2 passed in each of 4 rounds at head: their SRT caller with H.264 and with HEVC, and their RIST Main sender under PSK, into the gateway through 2 % loss each way. Every frame arrived whole with its receipts verified, and in every round the link's account read 0 dropped, 24 to 33 of about 1 140 packets recovered, and every byte of the input delivered. One earlier run, on the tree before main was merged in, failed the SRT case at the frame comparison. Its cause is not known: it predates the account diagnostic, and the host's load average was 13 to 47 then | Local repo checks, all clean: `check-public-names.sh` (with the private patterns) over the changed files, the commit messages and this body; `check-tests-in-ci.py`; `check-workflow-concurrency.sh`; `check-spec-refs.py`; `check-wai-section-refs.py`; `check-no-vanity-metrics.sh`; `test-public-text-checks.sh`; `check-comparative-prose.sh`; `check-orphan-sources.py`; and the YAML parse of every workflow. `wfsteps.py HEAD <merge-base>` reports only this branch's three workflow changes (one changed body, two new steps), with no step missing `run:`/`uses:` and no duplicate names. ## Falsifications Each mutation was applied to the committed tree in the run's snapshot. Each test meant to catch it failed, and the tree was then restored. The baseline passes first. | mutation | test | result | |---|---|---| | the relay's connection window removed (`git apply -R` of its two lines) | `connection_window` | fails: the publisher sends 33 554 432 bytes (each of the 32 streams a stream window full) and all 32 groups are held | | the relay's stream window removed (`git apply -R` of the change) | `contrib_srt twenty_megabits` | 2 of 2 fail (no PUBLISH_DONE, no session record) | | the link record's account rules not enforced | `link_corpus` | fails | | a series part's parent digest not checked | `link_corpus` | fails | | SRT's receiver sends no NAK | `srt_link losses_each_way…` | fails | | RIST's range requests never sent | `rist_link …range_requests` | fails | | RFC 3394 unwrap's integrity check skipped | `srt_link refusals_carry_their_codes` | fails | | RIST's rate bound removed | `contrib::tests` | fails | | SRT's payload IV shifted by one byte (Studio) | `srt_link` (both ends ours) / `interop srt` (reference tools) | loopback still passes (both ends ours agree); against the reference tools 2 of the 3 SRT tests fail (the third, a wrong passphrase refused, carries no payload) | The verifier also refuses every in-memory mutation of the accepted records and series. It drops each of its own checks in turn and requires the corpus to catch the weakened verifier; two checks (a small-order R point, and the first-try count alone) have no isolating vector, and it says so. The gateway bench was also falsified by length. With QUIC's default window it refused at the door at 4, 6 and 10 s; with the window, it passed 2 of 2 at 10 s (Framework). ## Throughput and energy Energy uses EN-A's profiles. Its class is `OnChipCounter`. The scope is stated per host. Every figure comes from three rounds. **Studio (macOS arm64), `macos-task` profile, scope = the bench process.** In the link bench that process holds both ends of the link (and the lossy proxy when there is loss). In the gateway bench it holds the gateway and the relay, but not the sender. The host was shared with CI, at a load average of 12 to 32. | link (`joule-contrib-bench`) | payload rate | J/GB (median, range) | |---|---|---| | SRT AES-128, 20 Mbit/s | 20.0 Mbit/s | 16.8 (15.5–21.0) | | SRT AES-128, 20 Mbit/s, 1 % loss each way | 20.0 Mbit/s, 163–171 recovered, 0 dropped | 21.6 (19.4–26.9) | | RIST Simple, clear, 20 Mbit/s | 20.0 Mbit/s | 10.1 (9.9–11.2) | | RIST Main PSK-128, 20 Mbit/s, 1 % loss | 20.0 Mbit/s, 166 recovered, 0 dropped | 19.8 (19.2–24.3) | | SRT clear, as fast as it goes | 925–997 Mbit/s | 40.5 (39.7–40.9) | | SRT AES-128, as fast as it goes | 587–600 Mbit/s | 70.9 (70.2–71.2) | | RIST Main PSK-128, as fast as it goes | 541–548 Mbit/s | 74.5 (71.2–75.0) | | gateway path (`contrib_bench`: SRT AES-128 into the gateway, every object with receipts into a relay whose door checks them; 15 s) | payload rate | objects | J/GB (median, range) | |---|---|---|---| | sender at 20 000 kbit/s | 21.93 Mbit/s | 1 152 | 27.3 (26.7–30.3) | | sender at 50 000 kbit/s | 54.62 Mbit/s | 1 152 | 23.1 (22.3–24.8) | The door refused nothing in any round. **Framework (Linux x86_64), throughput.** The `linux-rapl` package counter is shared by every process on the box. Its bracket (the run less an idle period of the same length) did not resolve: across the runs it ranged from −278 to +448 J, negative in several. So no energy figure is given from this host. | link | payload rate | |---|---| | SRT clear, as fast as it goes | 4.60–5.04 Gbit/s, 0 dropped | | SRT AES-128, as fast as it goes | 4.28–4.42 Gbit/s, 0 dropped | | RIST Main PSK-128, the sender unbounded | 213–686 Mbit/s delivered, millions dropped: the sender ran far past the 16-bit sequence space within the buffer, which is the case the gateway's RIST rate bound now refuses | ## Hardware - The Studio (M3 Ultra, macOS arm64) ran the interop tests against the reference tools, the IV falsification, and every energy figure (`macos-task`). - The Framework box (Zen 5, Linux x86_64) ran every unit, integration and gateway test, clippy, the verifier, the registry export, the falsifications and the throughput figures. - Not applicable to this slice: NVENC, VA-API and the NPU (no encode here). jetson-hub was not used. ## Not done - SRT egress (`m=request`) is refused 1405. - A RIST receiver as the tunnel's client; the Main Profile's multiplexing of several flows in one tunnel. - SRT key refresh as a sender (the receiver accepts refreshed keys). - DTLS, EAP-SRP, FEC, bonding and null-packet deletion are out of scope (spec §1). - An IPR check of the SRT draft. The IETF datatracker's IPR search could not be reached from here. - `joule-moq-relay` has two clippy lints on code this branch does not touch (`server.rs:173`, `session.rs:359`). `jwp` fails clippy's `manual_checked_div` when its lints are not capped. Both predate this branch. - `joule-moq-relay`'s `tests/age_rising.rs` failed 1 run in 3 on the base commit, before this branch changed anything. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Closed:** Landed on main through merge train E (#499, merge bf39ec9a); verified by content: this PR's head is in main
SRT (draft-sharabayko-srt-01): a listener that serves many callers on one
UDP port by socket ID and a caller; the HSv5 caller-listener handshake with
its SYN cookie, HSREQ/HSRSP, the stream ID, key material; live mode with
TSBPD over the negotiated latency, too-late packet drop, ACK/ACKACK and the
RTT estimate, immediate and periodic NAKs, retransmission, and AES-CTR with
the SEK wrapped (RFC 3394) under a PBKDF2-HMAC-SHA1 key.

RIST (VSF TR-06-1:2020, TR-06-2:2024), feature rist: the Simple Profile's RTP
on P and RTCP on P+1, SR/RR/SDES CNAME, range and bitmask requests, SSRC
low-bit retransmissions, the RTT echo; the Main Profile's GRE-over-UDP tunnel
in reduced overhead mode with the VSF EtherType and the legacy one,
keep-alives, and PSK (PBKDF2-HMAC-SHA256 over the nonce, AES-CTR on the GRE
sequence number).

An MPEG-TS demultiplexer and multiplexer, a PCR pacer, an account of every
sequence number's delivery, recovery or drop, an impairing UDP proxy, and
send, receive and bench tools.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A publisher POSTs /srt or /rist under its room grant and proof and gets a
one-time session: for SRT the stream ID and passphrase the gateway's listener
admits (live-ingest §13's mapping, its rejection codes), or the gateway calls
the publisher's listener where call_allow lets it; for RIST a port of its own,
the CNAME its RTCP must give and, in the Main Profile, a PSK passphrase.

The link's transport stream is demultiplexed: the first H.264 or HEVC stream
and the first AAC stream go to the same packagers, groups, catalog and first-hop
receipts as a WHIP session's, one access unit or one ADTS frame an object, the
PTS mapped onto the room clock. WAI capabilities wai.video.hevc and
wai.audio.aac join the gateway's codecs; WHIP and WHEP keep WebRTC's.

Every record interval and at the end, the gateway signs a part of the link's
WCL1 record: each sequence number delivered at the first try, recovered,
dropped or outstanding, its loss runs, requests, retransmissions, RTT and
recovery times, chained part to part and naming the head of each media track's
receipt chain; published with receipts on the owner's link track.

The corpus wai/live-conformance/links/ comes from joule-moq-ingest-link-vectors,
from fixed seeds; tests/link_corpus.rs holds the reader to it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai/extensions/live-contribution.md profiles SRT and RIST onto a room's tracks
through the ingest gateway: the one-time session resource, the listener's
checks and codes, the gateway calling, RIST's port, CNAME and PSK; the
transports as the gateway runs them; the transport stream onto the tracks;
and the link record WCL1, its account, parts and chain, how a part and a
series are verified, the size bound, the refusals, and the RIST texts' patent
notices.

live-conformance/live_contribution_verify.py is written from that text alone,
standard library only, with mutation runs and dropped-check runs. The corpus
gains vectors that isolate each check the verifier found uncovered.

Interop with the reference tools, run locally as test peers (ignored unless
named by environment variables): their SRT caller and listener and RIST sender
and receiver of both profiles against joule-contrib, and their SRT caller and
RIST sender into the gateway with an encoder's H.264, HEVC and AAC, through
lossy paths. Two fixes they found: an SRT recovery margin is held to the
window, and no part is cut before the first sequence number resolves.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-contrib-bench measures one link on loopback, both ends in one process,
and contrib_bench (ignored) the gateway path with the sender in a process of
its own: the payload rate, CPU time, and energy by the counter's profile,
class and scope: macos-task (the process's own account), or linux-rapl as a
bracket, the run's package reading less an idle reading of the same length,
across a wrap of the counter. The transport tests send a tail past what they
count: a receiver cannot know of a packet lost after the last it receives.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A door holds each object of a group until the group's receipts arrive, and
it holds the object while reading its stream; the receipts follow the
group's last object. A group larger than the stream's receive window (QUIC's
default, about 1.25 MB) therefore stops its stream: the publisher cannot
finish the group, its receipts never come, and the door refuses it after its
hold. Contribution at 20 Mbit/s with a one-second GOP sends groups of 2.5 MB.
limits.quic_stream_window_bytes sets the window, 16 MiB by default (64 Mbit/s
for two seconds). contrib_srt's test of a 4.5 MB group shows it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The change to joule-moq-relay's stream window (650ed3365) is reverted: its
test passed with QUIC's default window as well, and the gateway bench's
refusals that prompted it came back neither with nor without it once the
shared host was quiet. contrib_srt keeps two regressions: four seconds at
20 Mbit/s with 2.6 MB video groups through the relay's door, with a
subscriber and with none, every group released.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
81273dfc5 reverted 650ed3365 on a falsification that did not remove the
change: the build host's run checks out a snapshot commit whose parent is
the branch's head, so "HEAD~1" there was 650ed3365 itself, and both runs
"with QUIC's default window" ran with the window in place. Run again with
the change taken out (git apply -R), contrib_srt's two tests at 20 Mbit/s
fail (no PUBLISH_DONE, no session record: the door refuses each 2.6 MB
group after its hold), and the gateway bench refuses at the door at 4, 6
and 10 seconds; with the window both pass.

The door holds each object while its stream goes unread, and a group's
receipts follow its last object, so the whole group must fit the stream's
window. limits.quic_stream_window_bytes, 16 MiB by default; the relay's
README says so beside the door.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- wai.live.contribution registered (registry.rs; REGISTRY.md, capabilities.json and
  v2.ttl regenerated); SPEC §5, CHANGELOG and live-ingest point to the extension.
- joule-code-conformance runs joule-contrib with and without rist, joule-moq-ingest
  with rist, and checks the link corpus regenerates; wai-conformance runs
  live_contribution_verify.py over live-conformance/links and fires on the extension.
- A RIST link's rate is bounded by its 16-bit sequence space: fewer than 32 768
  packets within the buffer (§8), refused as bitrate_exceeded above it.
- READMEs for joule-contrib and the link corpus.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
rate_bound_kbps gives a session's bound: the configured one, and for RIST at
most what keeps its buffer under 32 768 packets of 1 316 bytes
(live-contribution §8: 344 981 kbit/s with a buffer of 1 000 ms).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts in wai/CHANGELOG.md and wai/live-conformance/README.md, each
resolved by keeping both sides.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-ingest: interop states the link's account before comparing frames
Some checks failed
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
27a666cff5
A frame missing at the subscriber is either a packet the link dropped or a
loss outside the link (the datagrams into the reference tool). The check now
reads the link record's series first, prints its account (delivered, at the
first try, dropped, outstanding, bytes against the input's) and requires
nothing dropped, before it compares the frames.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-relay: bound a session's unread bytes by a connection window
Some checks failed
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
b54dc94c1d
transport_config set each stream's receive window but not the connection's,
and QUIC's default connection window is unbounded. A session could therefore
leave max_uni_streams (1024) streams each a stream window full unread: about
1.25 GiB with QUIC's default stream window, 16 GiB with 16 MiB. One publisher
that opens streams and finishes no group could push the relay past
max_resident_mb, and the relay exits for every room on it.

limits.quic_connection_window_bytes sets the session's window over all its
streams. The default is 64 MiB, the budget websocket_buffer_bytes gives a
WebSocket session and four stream windows: a group a door holds whole, the
next group's start, its receipts and the session's other tracks. The relay
never sets it below the stream window. The doc comment, the example and the
README say that this window bounds a session's unread bytes, not the
stream's.

tests/connection_window.rs: a publisher with a 1 MiB stream window and a
4 MiB connection window opens 32 streams, each a group whose first object
waits at the door and whose stream is then not read. It stalls having sent
4 190 208 bytes, and the streams it opens after that carry nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot closed this pull request 2026-10-26 20:59:06 -04:00
Some checks failed
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!483
No description provided.