Skip to content
Merged
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Releases are cut separately; bump only when asked. Each package's version lives

- **Rust**: release-plz owns crate versions and Rust dependency requirements.
- **JavaScript**: `js/*/package.json` packages with a `scripts.release` entry (skip private ones like `@moq/clock`, `@moq/wasm`), plus the matching workspace version in `bun.lock`.
- **Python**: `py/moq-rs/pyproject.toml` only; `py/moq-ffi` follows the `moq-ffi-v*` tag and Rust crate.
- **Python**: `py/moq-rs/pyproject.toml`, plus the matching `moq-rs` entry in the root `uv.lock`; `py/moq-ffi` follows the `moq-ffi-v*` tag and Rust crate.
- **Swift**: `swift/VERSION`. **Kotlin**: `moq.version` in `kt/gradle.properties`. **Dart**: `version` in `dart/moq/pubspec.yaml`. Their FFI counterparts track the Rust crate.
- **Go**: `go/wrapper/VERSION` holds a human-owned `MAJOR.MINOR` line; CI derives the patch, so only edit it for a breaking API. Leave the placeholder FFI version in `go.mod` alone.
- **OBS**: the plugin takes the `libmoq` crate's version (`cpp/obs/CMakeLists.txt`), so release-plz owns it.
- **OBS**: release builds take their version from the `libmoq-v*` tag that release-plz cuts for the `libmoq` crate (`cpp/obs/build.sh --libmoq-release`), not from any manifest, so there is nothing to bump.
2 changes: 1 addition & 1 deletion doc/lib/py/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ asyncio.run(main())

For already-encoded live output, call `audio.flush(timestamp_us)` after each `audio.write_frame` with the same broadcast-clock PTS. It samples the transport handoff for catalog jitter. File, pipe, and network imports should omit `flush`; raw-pixel and PCM encoders inside the binding measure their own output.

Call `audio.discontinuity()` when the source seeks, pauses, or changes its time base. It publishes a timeline marker and restarts handoff measurement without lowering advertised jitter. Resume with timestamps that continue forward on the broadcast media clock; this does not permit timestamp rewinds. On a video track, resume with a keyframe: a delta frame before it fails.
Call `audio.discontinuity()` when the source seeks, pauses, or changes its time base. It publishes a timeline marker and restarts handoff measurement without lowering advertised jitter. Resume with timestamps that continue forward on the broadcast media clock; this does not permit timestamp rewinds. On a track from `publish_video` or `publish_video_on_track`, resume with a keyframe: a delta frame before it fails.

The three advertising operations, as the other bindings spell them:
`client.create_broadcast(path)` (or `OriginProducer.create_broadcast`) returns
Expand Down
2 changes: 1 addition & 1 deletion doc/lib/swift/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ session.shutdown()

For already-encoded live output, call `audio.flush(timestampUs:)` after `writeFrame` with the same broadcast-clock PTS. It measures catalog jitter at the transport handoff. File, pipe, and network imports should omit `flush`; built-in encoders observe their own output.

Call `audio.discontinuity()` when the source seeks, pauses, or changes its time base. It publishes a timeline marker and restarts handoff measurement without lowering advertised jitter. Resume with timestamps that continue forward on the broadcast media clock; this does not permit timestamp rewinds. On a video track, resume with a keyframe: a delta frame before it fails.
Call `audio.discontinuity()` when the source seeks, pauses, or changes its time base. It publishes a timeline marker and restarts handoff measurement without lowering advertised jitter. Resume with timestamps that continue forward on the broadcast media clock; this does not permit timestamp rewinds. On a track from `publishVideo`, resume with a keyframe: a delta frame before it fails.

The three advertising operations: `session.publish.createBroadcast(path:)`
returns an unannounced producer, invisible to everyone; `broadcast.announce(route:)` /
Expand Down
14 changes: 14 additions & 0 deletions js/net/src/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,20 @@ test("Reader decode rejects a stream that ends inside a message", async () => {
await expect(reader.decode(sized)).rejects.toThrow("unexpected end of stream");
});

test("Reader refuses an oversized value even when it is already buffered", async () => {
const size = 64 * 1024 * 1024 + 1;
const buffer = new Uint8Array(4 + size);
buffer.set([0x84, 0x00, 0x00, 0x01]); // the 4-byte varint for size
await expect(new Reader(undefined, buffer).string()).rejects.toThrow("exceeds max size");
await expect(new Reader(undefined, buffer.subarray(4)).read(size)).rejects.toThrow("exceeds max size");
});

test("Reader refuses a buffered decode whose fields together exceed the max size", async () => {
const half = 32 * 1024 * 1024;
const reader = new Reader(undefined, new Uint8Array(2 * half + 1));
await expect(reader.decode((c) => [c.read(half), c.read(half + 1)])).rejects.toThrow("exceeds max size");
});

/** A stream reset as a transport delivers one: the peer's code, and nothing else useful. */
class Reset extends Error {
readonly source = "stream" as const;
Expand Down
3 changes: 3 additions & 0 deletions js/net/src/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ export class Cursor {

#ensure(size: number) {
const need = this.#offset + size;
// Checked here too, and on the whole decode like the fill, since bytes that are already
// buffered never reach the fill.
if (need > MAX_READ_SIZE) throw new Error(`read size ${need} exceeds max size ${MAX_READ_SIZE}`);
if (need > this.#buffer.byteLength) throw new Short(need);
}

Expand Down
6 changes: 4 additions & 2 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [Remove finish](/quest/m1/broadcast-remove.md) - on dev, the deprecated broadcast end APIs are gone and `closed()` carries no cause
- [CLI inspection](/quest/m1/cli-inspect/README.md) - `moq ls` lists what is live and `moq fetch` reads a group over MoQ, and a guide shows how to inspect a relay
- [Session close](/quest/m1/session-close.md) - a graceful session end withdraws announces and waits one second for the ack
- [Drain before close](/quest/m1/drain-before-close.md) - a closing client delivers its queued stream finishes, so `moq import` ends the catalog cleanly over a real relay
- [Close codes](/quest/m1/close-codes.md) - a client sees the peer's application close code over WebSocket and raw QUIC, like WebTransport
- [Raw stream codes](/quest/m1/raw-stream-codes.md) - raw QUIC stream resets and stops carry the application's code, not an HTTP/3-mapped one
- [JS caught up](/quest/m1/js-announce-caught-up.md) - @moq/net's announce consumer says when the initial set has landed, like Rust
Expand All @@ -48,13 +49,14 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [moq play decode schedule](/quest/m1/play-decode-schedule.md) - `moq play` video keeps valid pictures across rewinds, reordering deeper than 100 ms, and decoder batches larger than three
- [moqsrc stop](/quest/m1/moqsrc-stop.md) - moqsrc's stop blocks until its session ends, without deadlocking on a blocked pad push
- [More tests under load](/quest/m1/test-flakes-2.md) - the second round of load-only failures, fixed at the cause
- [Interop contention](/quest/m1/interop-contention.md) - two `just test interop --all` matrices pass side by side, and `just test harness` runs from a clean checkout
- [UnknownSession log flood](/quest/m1/unknown-session-logs.md) - streams reset before their WebTransport header stop being reported as UnknownSession at WARN
- [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main
- [Wire compatibility](/quest/m1/wire-compat.md) - a nightly run tests this checkout against the last published release for tokens, session wire, and catalog/container
- [Accept-side flags](/quest/m1/cli-given-flags.md) - dial-only and local verbs refuse every `--listen-*` flag instead of ignoring it
- [JS catalog path](/quest/m1/js-catalog-path.md) - `@moq/net` broadcast consumers expose their path and `Catalog.watch` rejects escaping references, like Rust
- [Full codec string](/quest/m1/publish-codec-string.md) - browser-published video carries the encoder's full RFC 6381 codec string, so native players decode it
- [TS export jitter](/quest/m1/ts-export-jitter.md) - the video reorder bound follows later catalogs and observed reordering, so a late B-frame never reorders TS output
- [TS export jitter](/quest/m1/ts-export-jitter.md) - the video reorder bound follows later catalogs and the declared reorder depth, so a late B-frame never reorders TS output; an undeclared stream can reorder once per new maximum depth
- [TS import shared shift](/quest/m1/ts-import-shared-shift.md) - unflagged loop wraps move audio and video by one shift, so A/V sync holds across wraps
- [PipeWire duplicate cameras](/quest/m1/pipewire-dup-cameras.md) - a webcam lists once with PipeWire enabled
- [Catalog wall clock](/quest/m1/catalog-wall-clock.md) - `Clock::wall_clock` keeps the catalog's full precision instead of truncating to milliseconds
Expand Down Expand Up @@ -104,7 +106,6 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [Own the QUIC stack](/quest/m1/quic/README.md) - the moq-noq fork carries
ACK progress, reliable reset, hierarchical scheduling, deadlines, probing,
keep-alive, peer limits, careful resume, ECN, and qmux
- [BBR idle burst](/quest/m1/bbr-idle-burst.md) - a BBRv3 burst after a long idle paces near the learned bandwidth, proven by a fork regression
- [P2P](/quest/m1/p2p/README.md) - opted-in clients serve each other over data channels and iroh while the relay stays the rendezvous and the fallback, under application policy
- [One port](/quest/m1/one-port/README.md) - a relay speaks QUIC, STUN, WebRTC media, and SRT on one UDP port and HTTP, RTMP, and RTMPS on one TCP port
- [Signed priority](/quest/m1/signed-priority.md) - on dev, every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it
Expand All @@ -120,6 +121,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics.
- [#3126](/quest/m1/3126-moq-bench-every-readme-example-fails-to-parse-and.md) - moq-bench reports per-interval latency percentiles so the ramp leaves the steady state
- [Relay session bench](/quest/m1/bench-relay.md) - the same scenario through moq-relay's own connection handling
- [Bench coverage](/quest/m1/bench-coverage.md) - Criterion targets for moq-mux containers, the hang catalog, moq-auth verification, and moq-pattern matching
- [Stats producer bench](/quest/m1/stats-producer-bench.md) - the stats drain and encode cost per tick, swept over held paths and tiers and run nightly
- [Relay profiling](/quest/m1/performance-profiles.md) - reproducible CPU and allocation captures under the existing workloads
- [Browser benchmarks](/quest/m1/browser-benchmarks.md) - measure JS transport, container, decode, and render costs in an identified browser
- [Generated @moq/net](/quest/m1/rs2ts/README.md) - the browser runs moq-net as TypeScript generated from the Rust source, retiring js/net's hand-written protocol and model code
Expand Down
31 changes: 0 additions & 31 deletions quest/m1/bbr-idle-burst.md

This file was deleted.

13 changes: 10 additions & 3 deletions quest/m1/data-capture-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
A moq-ffi publisher, and every wrapper over it (Python, Swift, Kotlin, Go,
Dart), can pass a capture time with a JSON or binary snapshot `update` or
stream `append`, so its data tracks advertise `delay` and `jitter` like a Rust
publisher's. Leaving it out keeps today's behaviour. `moq-json`'s `window`
publisher's. Leaving it out keeps today's behaviour. The Go and Python
wrappers gain the binary snapshot and stream producers they lack. `moq-json`'s `window`
producer takes a capture time too. Settled scope: moq-ffi and its wrappers,
not libmoq.

Expand All @@ -30,13 +31,19 @@ not libmoq.
stream producers do. Nothing in `moq-mux` publishes window mode, so there is
no estimator to feed.
- Wrappers follow per the cross-package sync table, each with a test that a
past capture time is accepted and a future one refused. Update
past capture time is accepted and a future one refused.
- Go (`go/wrapper/moq`) and Python (`py/moq-rs`) wrap only the JSON
producers; [#4137](https://github.com/moq-dev/moq/pull/4137) added
`publish_binary_snapshot` / `publish_binary_stream` to moq-ffi without
them. Add hand-written binary wrappers there, capture time included, so
the capture tests cover binary too. The maintainer asked for this. Update
Comment thread
coderabbitai[bot] marked this conversation as resolved.
`doc/lib/{py,swift,kt,go,dart}`.

Public API: breaking, so it lands on `dev`. A new parameter on the generated
`update` and `append` breaks every published binding caller (Go, for one, has
no optional arguments), and a `_with_x` twin is ruled out. The broadcast clock
`now()` is additive; `window::Producer::push` accepts `Timed`, source-compatible.
`now()` and the Go and Python binary producers are additive;
`window::Producer::push` accepts `Timed`, source-compatible.
Wire: none.

## Related
Expand Down
40 changes: 40 additions & 0 deletions quest/m1/drain-before-close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# [M] Deliver queued stream data before a client closes

## Goal

A process that finishes its tracks and then closes its `moq_tokio::Client`
delivers what it already queued, including each stream's FIN, before the
connection closes, bounded by a deadline. `moq import` at stdin EOF is the
consumer: a subscriber over a real relay sees the catalog finish instead of
`Error::Dropped`.

## Plan

- [#4303](https://github.com/moq-dev/moq/pull/4303) made `moq import` finish
the catalog at EOF, but only in-process: over a real session the process
exits and the close discards the queued finish.
[#4287](https://github.com/moq-dev/moq/pull/4287) added `Client::close`,
which sends the CONNECTION_CLOSE but does not wait for stream data.
- A QUIC close discards unacknowledged stream data, so the session has to
wait until its open send streams are written, finished, and acknowledged,
not only handed to the transport. The pending group and control writes live
in moq-net's session tasks; the transport wait lives in moq-tokio.
- Bound the wait so a stalled peer cannot hang an exiting process. Share the
deadline and the graceful path with
[Session close](/quest/m1/session-close.md), which waits for announce
acknowledgements the same way; `abort` and drop stay immediate.
- Cover every backend `Client::close` covers, and say which do not drain
(WebSocket and iroh end on drop today).
- Regression tests: a moq-tokio test, shaped like
`noq_client_close_reaches_server`, where the client finishes a track and
closes on a runtime dropped right after, and the server's subscriber reads
the finish rather than a drop. Then a CLI test piping a file through
`moq import` to a relay.

Public API: likely additive (`Client::close` gains the drain, or a graceful
close sits beside `abort`). Wire: none.

## Related

- [Session close](/quest/m1/session-close.md) - the graceful end that withdraws announces
- [Graceful relay drains](/quest/m1/drain/README.md) - the server-side drain over GOAWAY
2 changes: 1 addition & 1 deletion quest/m1/go-mirror-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
A recommendation, with a prototype, for delivering the Go binding's
staticlibs without committing them to git. `moq-dev/moq-go-ffi` commits
them straight into git today, at 60.2 MiB for linux, 52.7 for windows, and
39.1 for darwin. That puts the largest file at 60% of GitHub's 100 MB push
39.1 for darwin. That puts the largest file at 60% of GitHub's 100 MiB per-file
limit, and each release adds about 210 MiB of history.

## Plan
Expand Down
29 changes: 29 additions & 0 deletions quest/m1/interop-contention.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# [S] Interop matrices run side by side

## Goal

Two `just test interop --all` matrices can run at once on one machine, even
from one checkout, and both pass. `just test harness` runs from a clean
checkout.

## Plan

[#4228](https://github.com/moq-dev/moq/pull/4228) fixed the port
reservations and the canvas-blocked pause click, and stages Go per run. What
remains:

- Under two concurrent matrices, `python -> js` and `go -> js` sometimes stall
the browser subscriber: the Pause control never appears within 30 s. It
was not seen in a single run. Find the cause (CPU starvation of headless
Chromium, a shared resource, or a real stall) before touching timeouts.
- `prepare_python` runs `just py build` in the workspace, so concurrent runs
rewrite the same `.venv` and maturin output (Codex on #4228). Build into the
run directory, as Go now does, or serialize the build.
- `just test harness` runs `harness.browser.ts` without installing workspace
dependencies or Playwright Chromium, so it only passes where CI's earlier
step prepared them. Provision them in the recipe, or reuse the path
`interop.sh` already takes.
- Prove it by running two `--all` matrices at once, several times, from one
checkout.

Public API: none. Wire: none.
2 changes: 1 addition & 1 deletion quest/m1/quic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ This is a transport API change, not a MoQ wire change.

- [Preserve QUIC packet identity in BBR](/quest/m1/quic/bbr-packet-identity.md) - ACKs and losses identify the right packet across QUIC spaces
- [Finish each BBR ACK sample before using it](/quest/m1/quic/bbr-ack-sampling.md) - current delivery samples reach the model once with consistent metadata
- [Mark application starvation before the next BBR send](/quest/m1/quic/bbr-app-limited.md) - resumed bursts retain correct sample labels
- [BBR idle burst](/quest/m1/quic/bbr-app-limited.md) - a fork regression proves a burst after a long idle is paced at the learned bandwidth, closing #4219
- [Finish BBR bandwidth-probe feedback once](/quest/m1/quic/bbr-probe-feedback.md) - cruise rounds neither age probe history repeatedly nor retain probe-loss classification
- [Recalibrate BBR startup pacing from measured RTT](/quest/m1/quic/bbr-startup-pacing.md) - measured RTT replaces the nominal startup rate for media senders
- [Protect bandwidth samples during BBR ProbeRTT](/quest/m1/quic/bbr-probe-rtt.md) - intentionally reduced sending cannot masquerade as reduced capacity
Expand Down
Loading
Loading