joule-moq-web: the browser MoQ client over WebTransport and the WebSocket binding, with receipts verified, the room composed, and an end-to-end test against the relay #324

Closed
dcharlot wants to merge 0 commits from joule-moq/web into main
Owner

L5c-1: the browser MoQ client, with a local end-to-end test against the relay

A page publishes a camera and a microphone to joule-moq-relay with WebCodecs, over
WebTransport or, when WebTransport is unavailable or does not open, the relay's WebSocket
binding, and subscribes to the other participants of a room. It verifies every object
against its receipt, shows the room's events and captions, and composes the participants
with live-compose's integer layout and mixer, compiled to WebAssembly. Nothing is
deployed: every test runs against a relay on loopback.

What is in it

joule-moq: a feature split, additive. The endpoint traits, the in-memory mock and
the QUIC transport now sit behind a default feature, runtime (quic implies it).
Without it the crate is the draft-22 codec, the carriage of objects and receipts, the
receipt builder and verifiers and the session's setup rules, with no tokio and no native
dependency, and it builds for wasm32-unknown-unknown. Every dependent takes the default
features and builds what it built before (cargo test -p joule-moq, lockstep-quic and
examples/transport-moqt checked).

joule-code-rs/crates/joule-moq-web: a new workspace member.

  • src/: the wasm build of joule-moq and a small wasm-bindgen surface.
    • The client's SETUP, with the authorization token and the receipt extension.
    • Every request and answer a client sends, from a JSON description.
    • Framed control messages, and subgroup and fetch streams, read as their bytes arrive.
    • Subgroup streams written object by object.
    • TrackSigner: each group is one object, signed into a group receipt chained to the
      group before. It is written as the receipts-track object and as the media object with
      its WAI_RECEIPT property.
    • TrackVerifier: receipts are admitted to the joined chain under the publisher's key,
      each object is verified against the receipt of its position, and an object that did
      not arrive is accounted for by inclusion.
    • Signatures: Ed25519 in wasm, through joule-moq's ReceiptVerifier and
      ReceiptChains, the code the relay's door runs. WebCrypto's Ed25519 is not used: a
      receipt also needs BLAKE3 and the Merkle walk, which WebCrypto does not have, and one
      implementation keeps the page's verdicts the relay's.
  • web/moq/: the transport and the session.
    • The binding's capsules.
    • WebTransport (protocols: ["moqt-22"], serverCertificateHashes) and the WebSocket
      binding (subprotocol moqt-22, stream credit by WT_MAX_STREAMS) behind one
      interface. The binding is chosen when WebTransport is missing or fails, or forced by
      transport=websocket.
    • A MOQT session:
      • SETUP;
      • PUBLISH;
      • SUBSCRIBE with subscriber priorities, Location Filters, FILL_PARAMETERS and
        RENDEZVOUS_TIMEOUT;
      • FETCH;
      • every data stream read by a task of its own as it arrives;
      • GOAWAY.
    • The payload framings SPEC §5 pins: VP9 in IVF, AV1 in its five-field framing, Opus
      as an Ogg Opus stream.
  • web/room/: the room client.
    • Publishing. getUserMedia (the profile's audio processing as constraints) feeds
      VideoEncoder and AudioEncoder. The codec is the first of the room profile's
      codecs the encoder supports. Each frame becomes a WAI1 envelope, one object per
      group (moq-streaming-format §3, §4.1), with its receipts and an MSF catalog
      (wai_capabilities, wai_receipt_alias, wai_receipts_track).
    • Subscribing. For each participant: its events track, its catalog, then each media
      track's receipts track (no delivery timeout) and the media track. Audio gets a higher
      subscriber priority than video. Objects are verified before they are decoded with
      WebCodecs, and the page counts objects dropped whose receipts arrived. A receipts
      object the subscription did not deliver is fetched (moq-streaming-format §4.1). A late
      joiner FETCHes the earlier groups of every track and its receipts track from group 0.
    • Composition. The clock's lifecycle is replayed by wai-live-web into the
      participant set. The WCD1 descriptor gives the tiles, drawn on a canvas and re-laid
      out on each lifecycle event. Each participant's own tile shows its own camera. The
      voices are mixed by the integer mixer in 20 ms blocks into an AudioWorklet.
    • Events. Every record is verified through the room verifier. caption events show
      with their origin label (human, machine, machine-reviewed), and the other kinds go to
      a timeline.
  • web/compose/: live-conformance/compose run in the page.
  • e2e/: the harness.
    • The relay runs on loopback under a test configuration: grants required, the door on
      every media namespace, summaries written.
    • A static page server binds 127.0.0.1.
    • The browser is the host's installed one, driven by the automation package of
      e2e/package.json, with a fake camera and microphone. Each browser runs under the
      host's memguard.
    • Every wait is bounded.

wai/wai-live-web: a new crate, its own workspace like wai-web. It is wai-rs with
live_compose and timed_text only, for the browser:

  • the layout function and the mixer;
  • the WCD1 descriptor and the composition of a room at an instant;
  • WEV1 events signed and read, with the MSF event-timeline binding;
  • the room descriptor and grants;
  • a Room that holds a page's records chain by chain;
  • the WAI1 envelope.

The same functions build natively. A helper binary makes the test room (descriptor,
grants, composition descriptor, and the relay's token rules and door rules). It also
computes on the host the composition a page computes in the browser, and gives every case
of the corpus its outcome through this crate.

CI: .github/workflows/joule-moq-web.yml, gated on this change's own diff. It runs
e2e/run.sh on the macos runner, the host with the browser installed. The page's codec
build, joule-moq without runtime, is checked there too. joule-moq-web's native tests
also run in the existing joule-code conformance job (cargo test --workspace).

Results, on the build host through rb

The host was shared and loaded (load average about 100 to 270 during the runs). Once three defects were fixed, every end-to-end run passed: 8 of 8 (three, three and two). The first two runs, before the fixes, failed on defects since fixed: a participant's own tile had no video, fetched groups waited on the live decoder's key frame, and one receipts object missed its subscription.

Command Exit
cargo test -p joule-moq-web 0: 6 passed
cargo build -p joule-moq-web --release --target wasm32-unknown-unknown 0
cargo check -p joule-moq --no-default-features; cargo check -p joule-moq-relay --tests 0
cargo test -p joule-moq; cargo check -p lockstep-quic; cargo check in examples/transport-moqt 0: every test passed
cargo clippy -p joule-moq-web --all-targets, and for wasm32 0, no warning from the new crate
cd wai/wai-live-web && cargo test 0: 3 passed
cargo build --release --lib --target wasm32-unknown-unknown (wai-live-web) 0
wai_live_web cases ../live-conformance/compose/vectors.json (native) 0: 102 cases through the crate, 0 differ; wai-rs's corpus check holds
cargo clippy --all-targets (wai-live-web) 0, no warning from the new crate
node --test web/moq/capsule.test.mjs web/moq/media.test.mjs 0: 9 of 9
e2e/run.sh --runs 3 (every scenario, three times) 0: 3 of 3 runs passed, 198 of 198 checks
e2e/run.sh --runs 2 on the final head, after merging main 0: 2 of 2 runs passed, 132 of 132 checks
scripts/check-lockfiles.sh; the orphan-source, tests-in-CI, workflow-concurrency, spec- and section-reference, vanity-metric, comparative-prose and public-name checks 0

Each run of the end-to-end tests:

  • compose. All 69 layout, 21 mix, 37 descriptor and 12 composition cases through the
    wasm build give the corpus's outcome (326 tiles, 0 differ). The native build reaches the
    same outcomes for those cases (wai_live_web cases), so the browser's rectangles and
    samples are the host's. wai-rs's own corpus check, run in the browser, holds.
  • webtransport. The room's clock and two participants connect over WebTransport. The
    negotiated protocol is moqt-22.
    • Each participant decodes the other's video (VP9, the profile's first codec the encoder
      took) and audio (Opus).
    • Every received object verifies against its receipt, the chain anchoring once and then
      linking. The relay's door refuses none.
    • The canvas's layout equals the host's for the same descriptor and set: the same tiles,
      order, voices and set digest. Each tile is drawn into its layout rectangles and shows
      video, not the background.
    • The mix reaches the AudioWorklet.
    • A machine caption shows labelled "Machine-generated", with its model digest. A human
      caption shows labelled "Human captioner". The timeline shows join, publish and
      caption.
    • A third participant joins late. Every earlier group of both participants' tracks
      comes by FETCH from group 0, each object verified and decoded. Every page then
      re-lays out for three, again equal to the host's layout and drawn.
  • websocket. The same over the binding, forced on three pages. The fourth is given a
    certificate hash that is not the relay's: its WebTransport session does not open, and
    the page falls back to the binding by itself.
  • goaway. The relay is stopped with GOAWAY naming a second relay. Every page reads the
    URI and moves (in about 10 to 20 ms): it publishes again, closes its old session itself,
    and subscribes again. Media verifies through the second relay. The first relay reports
    0 session(s) closed at the GOAWAY timeout.

What each path needs, and what the binding costs

Path Needs
WebTransport WebTransport with serverCertificateHashes (for a development certificate: ECDSA P-256, valid at most fourteen days) and protocols; a secure context (https: or loopback)
WebSocket binding WebSocket with a subprotocol
Publishing getUserMedia, MediaStreamTrackProcessor, VideoEncoder (VP9 or AV1 here), AudioEncoder (Opus, 48 kHz), OffscreenCanvas when the camera's size is not the profile's
Subscribing VideoDecoder, AudioDecoder, 2D canvas, AudioWorklet
Both WebAssembly with BigInt at the boundary

The binding costs:

  • Head-of-line blocking. Every stream rides one TCP connection, so a lost packet holds
    up every stream behind it.
  • Datagrams are not datagrams. A DATAGRAM capsule is reliable and ordered.
  • Framing. Stream data is framed again in capsules, a few bytes for each 16 KiB chunk
    and for each stream's open and FIN.
  • Stream credit is granted by capsule.
  • No data flow control below TCP's. A receiver that stops reading is bounded by the
    relay's buffer, and then closed.

The relay still applies delivery timeouts before each write. In these runs neither path
dropped an object.

Findings

  1. A null-parent group fetched into a join gap is refused. ReceiptChains refuses
    such a group as chain_break instead of holding it as history. A late joiner whose
    live receipts anchored its chain first, then FETCHed from group 0, saw groups 1 and up
    held and then admitted as history. Group 0, the chain's real start, was refused,
    although group 1 links to it. Its object still verified against its own signed group
    receipt, so the object was not lost: only the chain verdict for group 0 is wrong. This
    is in joule-moq's verifier and is not changed here.
  2. live-rooms §6 has no names for a publisher's catalog or receipts tracks, and the
    moqt scopes of §10.2 admit neither.
    Its namespaces hold media kinds and events,
    and its Track Names hold layers. This client puts the catalog at [room, principal]
    catalog and each receipts track at <layer>/receipts (the relay's default suffix).
    The relay's prefix rules admit both.
  3. A room grant is public, so it cannot be the bearer token alone. Every participant
    holds every grant, to verify the room. Each test token is the grant's record followed
    by a secret only its participant holds. The relay's rules name the token's SHA-256 and
    map the grant's rights onto prefixes: PUBLISH under [room, principal]; SUBSCRIBE and
    FETCH under [room]. Proof of possession by the principal's key (live-rooms §10.1,
    catdpop) is not modelled by the relay.
  4. The lifecycle is authored by a page holding the clock's key, driven by the test. A
    deployment needs it authored by the relay or by a service acting for the authority.
  5. Receipts can miss a subscription by a few milliseconds. A receipts-track object
    that reached the relay just before the receipts subscription did is not delivered on
    it. The page fetches that group of the receipts track (one group in one run), as
    moq-streaming-format §4.1 describes for a missing receipt.
  6. The per-object cost at a 20 ms audio frame.
    • A whole Ogg Opus stream and a WAI1 manifest per object make each audio object
      about five times its Opus packet, about 440 bytes.
    • Each group's receipts object is about 1,130 bytes, larger than the audio object it
      covers. The receipts payload is joule-moq's own JSON, not yet jwp-receipts
      §2.4.1's.
    • A longer frame duration (audioFrameMs) or a compact receipt encoding are the
      levers.
  7. Pre-skip is 0 in every Opus object. The Ogg Opus stream of each object says so,
    since the object continues a live stream whose decoder is already running.

What L5c-2 needs

  • The /room/ page on the WAI site, built from web/room/ with both wasm builds under
    their own --out-name.
  • A public relay endpoint:
    • UDP 443 reaching the relay for WebTransport, which the sites' fronts do not carry
      (a firewall and router change on the serving host);
    • a TCP route through the tunnel to the relay's WebSocket listener, as wss://, the
      front terminating TLS.
  • A certificate for the public WebTransport endpoint. serverCertificateHashes takes
    only short-lived (at most fourteen days) ECDSA P-256 certificates. A public endpoint
    needs a certificate a browser accepts by its chain, for the relay's host name, from an
    automated issuer. The relay reads its certificate files only at start, so a renewal
    needs a restart (with a GOAWAY to move sessions, which this client follows) or a reload
    added to the relay.
  • The room's lifecycle authored on the server side (finding 4).
  • Token issuance for real participants, and proof of possession (finding 3).
  • The production e2e against the live page and the public relay.

Base: main. Not merged here.

🤖 Generated with Claude Code


Closed: landed in main through #325 (joule-moq/ingest), by way of #327; both contain this branch's head 8c80ef34 unchanged. #325's relevant CI was green but two failures outside this work: a wai-quantum nondeterminism test and a jouleclaw GPU-energy test on the macOS runner.

## L5c-1: the browser MoQ client, with a local end-to-end test against the relay A page publishes a camera and a microphone to `joule-moq-relay` with WebCodecs, over WebTransport or, when WebTransport is unavailable or does not open, the relay's WebSocket binding, and subscribes to the other participants of a room. It verifies every object against its receipt, shows the room's events and captions, and composes the participants with live-compose's integer layout and mixer, compiled to WebAssembly. Nothing is deployed: every test runs against a relay on loopback. ### What is in it **`joule-moq`: a feature split, additive.** The endpoint traits, the in-memory mock and the QUIC transport now sit behind a default feature, `runtime` (`quic` implies it). Without it the crate is the draft-22 codec, the carriage of objects and receipts, the receipt builder and verifiers and the session's setup rules, with no tokio and no native dependency, and it builds for `wasm32-unknown-unknown`. Every dependent takes the default features and builds what it built before (`cargo test -p joule-moq`, `lockstep-quic` and `examples/transport-moqt` checked). **`joule-code-rs/crates/joule-moq-web`: a new workspace member.** - `src/`: the wasm build of `joule-moq` and a small wasm-bindgen surface. - The client's SETUP, with the authorization token and the receipt extension. - Every request and answer a client sends, from a JSON description. - Framed control messages, and subgroup and fetch streams, read as their bytes arrive. - Subgroup streams written object by object. - `TrackSigner`: each group is one object, signed into a group receipt chained to the group before. It is written as the receipts-track object and as the media object with its `WAI_RECEIPT` property. - `TrackVerifier`: receipts are admitted to the joined chain under the publisher's key, each object is verified against the receipt of its position, and an object that did not arrive is accounted for by inclusion. - **Signatures:** Ed25519 in wasm, through `joule-moq`'s `ReceiptVerifier` and `ReceiptChains`, the code the relay's door runs. WebCrypto's Ed25519 is not used: a receipt also needs BLAKE3 and the Merkle walk, which WebCrypto does not have, and one implementation keeps the page's verdicts the relay's. - `web/moq/`: the transport and the session. - The binding's capsules. - WebTransport (`protocols: ["moqt-22"]`, `serverCertificateHashes`) and the WebSocket binding (subprotocol `moqt-22`, stream credit by `WT_MAX_STREAMS`) behind one interface. The binding is chosen when WebTransport is missing or fails, or forced by `transport=websocket`. - A MOQT session: - SETUP; - PUBLISH; - SUBSCRIBE with subscriber priorities, Location Filters, `FILL_PARAMETERS` and `RENDEZVOUS_TIMEOUT`; - FETCH; - every data stream read by a task of its own as it arrives; - GOAWAY. - The payload framings SPEC §5 pins: VP9 in IVF, AV1 in its five-field framing, Opus as an Ogg Opus stream. - `web/room/`: the room client. - **Publishing.** `getUserMedia` (the profile's audio processing as constraints) feeds `VideoEncoder` and `AudioEncoder`. The codec is the first of the room profile's `codecs` the encoder supports. Each frame becomes a `WAI1` envelope, one object per group (moq-streaming-format §3, §4.1), with its receipts and an MSF catalog (`wai_capabilities`, `wai_receipt_alias`, `wai_receipts_track`). - **Subscribing.** For each participant: its events track, its catalog, then each media track's receipts track (no delivery timeout) and the media track. Audio gets a higher subscriber priority than video. Objects are verified before they are decoded with WebCodecs, and the page counts objects dropped whose receipts arrived. A receipts object the subscription did not deliver is fetched (moq-streaming-format §4.1). A late joiner FETCHes the earlier groups of every track and its receipts track from group 0. - **Composition.** The clock's lifecycle is replayed by `wai-live-web` into the participant set. The `WCD1` descriptor gives the tiles, drawn on a canvas and re-laid out on each lifecycle event. Each participant's own tile shows its own camera. The voices are mixed by the integer mixer in 20 ms blocks into an AudioWorklet. - **Events.** Every record is verified through the room verifier. `caption` events show with their origin label (human, machine, machine-reviewed), and the other kinds go to a timeline. - `web/compose/`: `live-conformance/compose` run in the page. - `e2e/`: the harness. - The relay runs on loopback under a test configuration: grants required, the door on every media namespace, summaries written. - A static page server binds 127.0.0.1. - The browser is the host's installed one, driven by the automation package of `e2e/package.json`, with a fake camera and microphone. Each browser runs under the host's `memguard`. - Every wait is bounded. **`wai/wai-live-web`: a new crate, its own workspace like `wai-web`.** It is `wai-rs` with `live_compose` and `timed_text` only, for the browser: - the layout function and the mixer; - the `WCD1` descriptor and the composition of a room at an instant; - `WEV1` events signed and read, with the MSF event-timeline binding; - the room descriptor and grants; - a `Room` that holds a page's records chain by chain; - the `WAI1` envelope. The same functions build natively. A helper binary makes the test room (descriptor, grants, composition descriptor, and the relay's token rules and door rules). It also computes on the host the composition a page computes in the browser, and gives every case of the corpus its outcome through this crate. **CI:** `.github/workflows/joule-moq-web.yml`, gated on this change's own diff. It runs `e2e/run.sh` on the `macos` runner, the host with the browser installed. The page's codec build, `joule-moq` without `runtime`, is checked there too. `joule-moq-web`'s native tests also run in the existing joule-code conformance job (`cargo test --workspace`). ### Results, on the build host through `rb` The host was shared and loaded (load average about 100 to 270 during the runs). Once three defects were fixed, every end-to-end run passed: 8 of 8 (three, three and two). The first two runs, before the fixes, failed on defects since fixed: a participant's own tile had no video, fetched groups waited on the live decoder's key frame, and one receipts object missed its subscription. | Command | Exit | |---|---| | `cargo test -p joule-moq-web` | 0: 6 passed | | `cargo build -p joule-moq-web --release --target wasm32-unknown-unknown` | 0 | | `cargo check -p joule-moq --no-default-features`; `cargo check -p joule-moq-relay --tests` | 0 | | `cargo test -p joule-moq`; `cargo check -p lockstep-quic`; `cargo check` in `examples/transport-moqt` | 0: every test passed | | `cargo clippy -p joule-moq-web --all-targets`, and for wasm32 | 0, no warning from the new crate | | `cd wai/wai-live-web && cargo test` | 0: 3 passed | | `cargo build --release --lib --target wasm32-unknown-unknown` (wai-live-web) | 0 | | `wai_live_web cases ../live-conformance/compose/vectors.json` (native) | 0: 102 cases through the crate, 0 differ; wai-rs's corpus check holds | | `cargo clippy --all-targets` (wai-live-web) | 0, no warning from the new crate | | `node --test web/moq/capsule.test.mjs web/moq/media.test.mjs` | 0: 9 of 9 | | `e2e/run.sh --runs 3` (every scenario, three times) | 0: **3 of 3 runs passed, 198 of 198 checks** | | `e2e/run.sh --runs 2` on the final head, after merging main | 0: 2 of 2 runs passed, 132 of 132 checks | | `scripts/check-lockfiles.sh`; the orphan-source, tests-in-CI, workflow-concurrency, spec- and section-reference, vanity-metric, comparative-prose and public-name checks | 0 | Each run of the end-to-end tests: - **compose.** All 69 layout, 21 mix, 37 descriptor and 12 composition cases through the wasm build give the corpus's outcome (326 tiles, 0 differ). The native build reaches the same outcomes for those cases (`wai_live_web cases`), so the browser's rectangles and samples are the host's. `wai-rs`'s own corpus check, run in the browser, holds. - **webtransport.** The room's clock and two participants connect over WebTransport. The negotiated protocol is `moqt-22`. - Each participant decodes the other's video (VP9, the profile's first codec the encoder took) and audio (Opus). - Every received object verifies against its receipt, the chain anchoring once and then linking. The relay's door refuses none. - The canvas's layout equals the host's for the same descriptor and set: the same tiles, order, voices and set digest. Each tile is drawn into its layout rectangles and shows video, not the background. - The mix reaches the AudioWorklet. - A machine caption shows labelled "Machine-generated", with its model digest. A human caption shows labelled "Human captioner". The timeline shows `join`, `publish` and `caption`. - A third participant joins late. Every earlier group of both participants' tracks comes by FETCH from group 0, each object verified and decoded. Every page then re-lays out for three, again equal to the host's layout and drawn. - **websocket.** The same over the binding, forced on three pages. The fourth is given a certificate hash that is not the relay's: its WebTransport session does not open, and the page falls back to the binding by itself. - **goaway.** The relay is stopped with GOAWAY naming a second relay. Every page reads the URI and moves (in about 10 to 20 ms): it publishes again, closes its old session itself, and subscribes again. Media verifies through the second relay. The first relay reports `0 session(s) closed at the GOAWAY timeout`. ### What each path needs, and what the binding costs | Path | Needs | |---|---| | WebTransport | `WebTransport` with `serverCertificateHashes` (for a development certificate: ECDSA P-256, valid at most fourteen days) and `protocols`; a secure context (`https:` or loopback) | | WebSocket binding | `WebSocket` with a subprotocol | | Publishing | `getUserMedia`, `MediaStreamTrackProcessor`, `VideoEncoder` (VP9 or AV1 here), `AudioEncoder` (Opus, 48 kHz), `OffscreenCanvas` when the camera's size is not the profile's | | Subscribing | `VideoDecoder`, `AudioDecoder`, 2D canvas, `AudioWorklet` | | Both | WebAssembly with BigInt at the boundary | The binding costs: - **Head-of-line blocking.** Every stream rides one TCP connection, so a lost packet holds up every stream behind it. - **Datagrams are not datagrams.** A `DATAGRAM` capsule is reliable and ordered. - **Framing.** Stream data is framed again in capsules, a few bytes for each 16 KiB chunk and for each stream's open and FIN. - **Stream credit** is granted by capsule. - **No data flow control below TCP's.** A receiver that stops reading is bounded by the relay's buffer, and then closed. The relay still applies delivery timeouts before each write. In these runs neither path dropped an object. ### Findings 1. **A null-parent group fetched into a join gap is refused.** `ReceiptChains` refuses such a group as `chain_break` instead of holding it as history. A late joiner whose live receipts anchored its chain first, then FETCHed from group 0, saw groups 1 and up held and then admitted as history. Group 0, the chain's real start, was refused, although group 1 links to it. Its object still verified against its own signed group receipt, so the object was not lost: only the chain verdict for group 0 is wrong. This is in `joule-moq`'s verifier and is not changed here. 2. **live-rooms §6 has no names for a publisher's catalog or receipts tracks, and the `moqt` scopes of §10.2 admit neither.** Its namespaces hold media kinds and `events`, and its Track Names hold layers. This client puts the catalog at `[room, principal]` `catalog` and each receipts track at `<layer>/receipts` (the relay's default suffix). The relay's prefix rules admit both. 3. **A room grant is public, so it cannot be the bearer token alone.** Every participant holds every grant, to verify the room. Each test token is the grant's record followed by a secret only its participant holds. The relay's rules name the token's SHA-256 and map the grant's rights onto prefixes: PUBLISH under `[room, principal]`; SUBSCRIBE and FETCH under `[room]`. Proof of possession by the principal's key (live-rooms §10.1, `catdpop`) is not modelled by the relay. 4. **The lifecycle is authored by a page holding the clock's key, driven by the test.** A deployment needs it authored by the relay or by a service acting for the authority. 5. **Receipts can miss a subscription by a few milliseconds.** A receipts-track object that reached the relay just before the receipts subscription did is not delivered on it. The page fetches that group of the receipts track (one group in one run), as moq-streaming-format §4.1 describes for a missing receipt. 6. **The per-object cost at a 20 ms audio frame.** - A whole Ogg Opus stream and a `WAI1` manifest per object make each audio object about five times its Opus packet, about 440 bytes. - Each group's receipts object is about 1,130 bytes, larger than the audio object it covers. The receipts payload is `joule-moq`'s own JSON, not yet jwp-receipts §2.4.1's. - A longer frame duration (`audioFrameMs`) or a compact receipt encoding are the levers. 7. **Pre-skip is 0 in every Opus object.** The Ogg Opus stream of each object says so, since the object continues a live stream whose decoder is already running. ### What L5c-2 needs - **The `/room/` page** on the WAI site, built from `web/room/` with both wasm builds under their own `--out-name`. - **A public relay endpoint:** - UDP 443 reaching the relay for WebTransport, which the sites' fronts do not carry (a firewall and router change on the serving host); - a TCP route through the tunnel to the relay's WebSocket listener, as `wss://`, the front terminating TLS. - **A certificate for the public WebTransport endpoint.** `serverCertificateHashes` takes only short-lived (at most fourteen days) ECDSA P-256 certificates. A public endpoint needs a certificate a browser accepts by its chain, for the relay's host name, from an automated issuer. The relay reads its certificate files only at start, so a renewal needs a restart (with a GOAWAY to move sessions, which this client follows) or a reload added to the relay. - **The room's lifecycle authored on the server side** (finding 4). - **Token issuance** for real participants, and proof of possession (finding 3). - **The production e2e** against the live page and the public relay. Base: main. Not merged here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Closed:** landed in main through #325 (joule-moq/ingest), by way of #327; both contain this branch's head 8c80ef34 unchanged. #325's relevant CI was green but two failures outside this work: a wai-quantum nondeterminism test and a jouleclaw GPU-energy test on the macOS runner.
The endpoint traits, the in-memory mock and the QUIC transport stay where they were,
under the default feature runtime (quic implies it). Turned off, the crate is the
draft-22 codec, the carriage of objects and receipts, the receipt builder and
verifiers and the session's setup rules, with no tokio and no native dependency, so a
browser crate can build the same code for wasm32-unknown-unknown. Additive: every
dependent that takes the default features builds what it built before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A wasm-bindgen surface, byte-oriented: the client's SETUP, every request and answer a
client sends from a JSON description, framed control messages and data streams read as
their bytes arrive, subgroup streams written object by object, a publisher's signer
(each group one object, signed into a group receipt chained to the one before, written
as the receipts-track object and the media object with its WAI_RECEIPT property) and a
subscriber's verifier (receipts admitted to the joined chain under the publisher's key,
each object verified against the receipt of its position, or accounted for by
inclusion). Native tests hold the pure-Rust half.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The live-compose layout function and mixer, the WCD1 descriptor and the composition of
a room at an instant (its lifecycle replayed, its participant set, its tiles and
voices), in the JSON view live-conformance/compose states its cases in; WEV1 events
signed and read with their payloads (captions with their origin) and the MSF timeline
binding; what a page reads of a room descriptor and its grants; a Room that holds the
records a page receives, chain by chain, and verifies and composes from them; and the
WAI1 envelope packed and read. The same functions build natively: a helper binary makes
a test room (descriptor, grants, composition descriptor, relay token rules) and computes
on the host the composition a page computes in the browser.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
web/moq: the WebSocket binding's capsules, WebTransport and the binding behind one
transport interface (WebTransport first, offering moqt-22 and the relay's certificate
hash; the binding as the fallback, or forced), a MOQT session (SETUP with the token,
requests, every data stream read concurrently, GOAWAY), and the payload framings a WAI
track carries. web/room: the room client (WebCodecs publishing with receipts, verified
subscriptions, a late joiner's FETCH, the composed canvas and the mixed AudioWorklet,
the events timeline and captions with their origin, the move after GOAWAY). web/compose:
the composition corpus in the page. e2e: the relay on loopback from a test
configuration, real Chrome through Playwright with a fake camera and microphone, each
browser under memguard.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An object whose receipts-track object reached the relay just before the receipts
subscription did never gets it live; after a second its group of the receipts track is
fetched (moq-streaming-format §4.1). e2e/package-lock.json pins the automation package
by integrity (playwright 1.60.0 and playwright-core, Apache-2.0; fsevents, MIT, optional,
its install script never run: npm ci --ignore-scripts).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-web e2e: the browser's executable comes from BROWSER_BIN alone
Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m3s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 15s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m4s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m29s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 5m51s
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Has been cancelled
8c80ef3434
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot closed this pull request 2026-10-19 05:55:21 -04:00
Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m3s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 15s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m4s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m29s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 5m51s
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (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!324
No description provided.