diff --git a/quest/m1/README.md b/quest/m1/README.md index 85795295ea9..a36d2de4a6b 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -48,6 +48,8 @@ moves what remains of them once their claims merge. - [Stream credit by priority](/quest/m1/stream-credit/README.md) - every stream opens through one priority entry point, and a higher-priority group takes over or resets a lower one when credit runs out - [Audio jitter target](/quest/m1/audio-jitter-target/README.md) - the playout target is default in both languages; time-stretched browser playback without steady-state underruns and a native trace replay remain - [Buffered playback subscribes in ascending order](/quest/m1/buffered-ascending.md) - a buffered `` asks for the oldest group first, so a voice reply's opening arrives first, and audio's head hold shrinks to the floor +- [One assets() for every hosted file](/quest/m1/hosted-assets.md) - `@moq/hang` owns `assets()`, which also points the libav polyfill at the hosted wasm +- [Watch sources default to the decoder's probe](/quest/m1/watch-supported-default.md) - a source built without `in.supported` plays instead of selecting nothing - [Push-to-talk's press-to-audible latency fits a budget on the defaults](/quest/m1/push-to-talk-latency.md) - press to audible on the browser defaults, excluding device output, stays under a kixtop-measured budget that `just test push-to-talk` enforces - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [MoQ on rind](/quest/m1/rind/README.md) - every MoQ crate takes its crypto from rind, a pure-Rust graviola fork, so no build compiles C crypto diff --git a/quest/m1/explicit-state/ffi-state.md b/quest/m1/explicit-state/ffi-state.md index 05f9282b283..2993048db67 100644 --- a/quest/m1/explicit-state/ffi-state.md +++ b/quest/m1/explicit-state/ffi-state.md @@ -51,6 +51,14 @@ Also sweep: - `MoqServer` (`server.rs`): its lifecycle is spread over four places. - `MoqTrackProducer.keep: None`, which means both "finished" and "has no family". +- `MoqConnectionState` (`session.rs`) is a flat snapshot (`revision`, + `phase`, `connects`, `failure`, `version`), so `failure` and `version` are + readable in phases where they mean nothing. It carries each phase's data + with that phase, mirroring whatever shape Rust's connection state takes, + and moq-c's `moq_connection_state` follows (renaming its `protocol` to + `version`). It breaks every binding, so under the epic's milestone rule + this item splits into its own m2 quest when this quest starts (added + 2026-11-07, maintainer, /quest-plan from the API shape audit). - moq-c's global id counter, whose wrap lets a stale route id name a newer broadcast. Use generation-tagged ids or clear ids on close. diff --git a/quest/m1/explicit-state/publish-capture.md b/quest/m1/explicit-state/publish-capture.md index bdb07356c98..2bee77f4bee 100644 --- a/quest/m1/explicit-state/publish-capture.md +++ b/quest/m1/explicit-state/publish-capture.md @@ -45,6 +45,10 @@ Also sweep: - The preview `Transcode.#active`. - The `` and `` enable latches (`#enabled`, `#connected`, `#invalid`, `#resume`). Derive enabled as connected with no - invalid attributes. + invalid attributes. ``'s `announce` getter returns + `AnnounceMode`, and an invalid `announce` attribute suspends announcing + through `#invalid` like every other attribute, instead of the getter + returning `undefined` (added 2026-11-07, maintainer, /quest-plan from the + API shape audit). - `js/moq-boy/src/game.ts`: a command sent while disconnected replays on reconnect. diff --git a/quest/m1/hosted-assets.md b/quest/m1/hosted-assets.md new file mode 100644 index 00000000000..7973ba7c5d4 --- /dev/null +++ b/quest/m1/hosted-assets.md @@ -0,0 +1,21 @@ +# [S] One assets() for every hosted file + +## Goal + +A page that serves the player's files itself (for a strict +`Content-Security-Policy` with no `blob:` workers) calls one `assets(url)`, +and that call also points the libav AAC polyfill at the hosted wasm. Today +`@moq/watch` and `@moq/publish` each carry an identical copy of `assets()` +(`js/watch/src/assets.ts`, `js/publish/src/assets.ts`), and an app that +needs the polyfill must also import `@moq/libav` and set its mutable +`LibAV.base`, a transitive dependency our API never mentions. + +## Plan + +Found by the 2026-11-07 API shape audit (browser media). Move `assets` and +its resolver into `@moq/hang`, the shared dependency whose +`Util.Libav.polyfill()` (`js/hang/src/util/libav.ts`) loads libav, and have +the polyfill take its base from it. `@moq/watch` and `@moq/publish` +re-export it, so their existing `assets()` callers keep working and nothing +breaks. `@moq/libav` stays an implementation dependency. Future hosted files +(the watch worker) join the same call. Test it on the CSP demo pages. diff --git a/quest/m1/stream-credit/README.md b/quest/m1/stream-credit/README.md index deebfe36d1d..fcda393f586 100644 --- a/quest/m1/stream-credit/README.md +++ b/quest/m1/stream-credit/README.md @@ -36,6 +36,12 @@ which JS cannot raise; Chrome stalled at 103, Firefox at 100): browser test stays this README's own work. Not chosen: holding the whole quest until #7670 merges. +- Decided 2026-11-07 (maintainer, /quest-plan from the API shape audit): + ✅ delete Rust's `StreamCredit` hook too, with no replacement hook + ([no-credit-hook](/quest/m1/stream-credit/no-credit-hook.md)). #7894's + reversal removed the JS half; the Rust trait kept it because the open line + reads it. Not chosen: keeping the hook and recording why. + This epic owns the end-to-end test: a browser reader and a Rust relay over WebTransport, 300 live tracks of mixed priority, the highest-priority ones receiving every group, and nothing hanging. @@ -43,3 +49,4 @@ receiving every group, and nothing hanging. ## Required - [Takeover](/quest/m1/stream-credit/takeover.md) - on lite-07 a higher-priority group takes over an acknowledged stream, and a group that lost its stream resumes +- [Transports report no stream credit](/quest/m1/stream-credit/no-credit-hook.md) - `StreamCredit` and the `poll::Session::stream_credit` hook are gone, and the open line infers a credit wait from its own state diff --git a/quest/m1/stream-credit/no-credit-hook.md b/quest/m1/stream-credit/no-credit-hook.md new file mode 100644 index 00000000000..301831fb95d --- /dev/null +++ b/quest/m1/stream-credit/no-credit-hook.md @@ -0,0 +1,29 @@ +# [S] Transports report no stream credit + +## Goal + +`moq_net::transport::StreamCredit`, `Dir`, and the `poll::Session::stream_credit` +hook are gone, so no transport implements a credit API. The open line +(`rs/moq-net/src/transport/open.rs`) decides when a waiting open preempts a +lower-ranked stream from its own state, as `js/net` already does, and an +open waiting on something other than credit still never triggers a reset. + +## Plan + +Decided 2026-11-07 (maintainer, /quest-plan from the API shape audit): +✅ "Delete, no hooks" (rejected: keeping the hook and recording why). This +follows the [moq-dev/moq#7894](https://github.com/moq-dev/moq/pull/7894) +reversal recorded in this epic ("the signal is the open line's waiting +state, with no transport credit or blocked API") and overrides +[moq-dev/moq#8088](https://github.com/moq-dev/moq/pull/8088)'s refutation, +which kept the hook because the open line reads it. + +The hook exists today so an open waiting on a WebTransport stream header is +not mistaken for a credit wait (`make_room`, open.rs). Remove that case at +its source, for example by having a transport's open complete only once +credit is granted and write the header after, so a pending head always means +credit. Implementers of `poll::Session` (moq-tokio, moq-uring, qmux, the +memory transport) drop their implementations. + +Breaking for transport implementers; the Impact section says what replaces +the hook. diff --git a/quest/m1/watch-supported-default.md b/quest/m1/watch-supported-default.md new file mode 100644 index 00000000000..75b08dae4df --- /dev/null +++ b/quest/m1/watch-supported-default.md @@ -0,0 +1,16 @@ +# [XS] Watch sources default to the decoder's probe + +## Goal + +A composer that builds `@moq/watch`'s `Audio.Source` or `Video.Source` +without `in.supported` gets playback. Today the input defaults to +`undefined`, and "nothing is selected until one is provided" +(`js/watch/src/audio/source.ts`), so forgetting it is silent non-playback. +When `in.supported` is absent, the source uses its decoder's `supported` +probe. + +## Plan + +Found by the 2026-11-07 API shape audit (browser media). Additive: callers +that pass `supported`, such as the `` element, are unchanged. Add a test that a source built without it selects a +rendition. diff --git a/quest/m2/README.md b/quest/m2/README.md index 8c9d864ec75..ab4cbc6096b 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -163,6 +163,13 @@ Added 2026-11-04 (maintainer): the relay reports the native sessions its drain deadline force-closes while still open, apart from those only finishing their end report, so an embedder's drain alert pages on real cuts. +Added 2026-11-07 (maintainer, /quest-plan from an API shape audit of +`release` against `main` and the open quests): the breaks it found that the +cut would otherwise publish. A route's role becomes its type; JS durations +and instants become objects; native playout sync decouples its tracks like +the browser's; sentinels, moq-video's environment reads, and per-gateway +ingest setters go; and an epic batches the small fixes per area. + Decided 2026-11-05 (maintainer, /quest-audit): blockers rank above the quests that require them. @@ -178,12 +185,19 @@ quests that require them. - [A smaller, safer API than release](/quest/m1/api-audit/README.md) - every surface added since release has a consumer or is gone, overlapping operations are collapsed, names mirror across Rust, JS, and the bindings, and every break says how to migrate (held in m1 by an open claim) - [Cluster routing](/quest/m2/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters - [A job's exact route continues its claim's readers](/quest/m2/announce-takeover.md) - a job's route inherits its claim's instance, replacing the ANNOUNCE_TAKEOVER predecessor design +- [A route's role is its type](/quest/m2/route-roles.md) - no route method panics, throws, or errors because the other constructor made the route; prototypes of a single prefix route and an exact/prefix split decide the shape - [A group a subscriber gives up on is always reported](/quest/m2/group-drops.md) - the wire names every abandoned group and the drop signal is public, so readers count losses instead of stalling - [Stream credit by priority](/quest/m1/stream-credit/README.md) - every stream opens through one priority entry point, and a higher-priority group takes over or resets a lower one when credit runs out (held in m1 by an open claim) - [Finalize moq-lite-07](/quest/m2/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it - [Unbounded groups](/quest/m2/unbounded-groups/README.md) - groups and frames lose their size caps; a group keeps a 32 MB tail, and readers behind it replay through paced flights or refresh - [moq-quic drops inherited surfaces with no consumer](/quest/m2/moq-quic-unused-surface.md) - unused accept, connection, and stream-id surfaces leave moq-quic's public API before the cut - [Publish reports rendition stats as hang's type](/quest/m2/rendition-stats-type.md) - `Rendition.stats` and `Report.Writer` take the rendition stats schema's input type, so encoders write plain numbers +- [No sentinel values in public API](/quest/m2/no-sentinels.md) - receive windows, cache durations, reconnect timeouts, and the FFI test clock use `Option` instead of a huge value or zero +- [moq-video takes devices, not environment variables](/quest/m2/video-devices.md) - VA-API and V4L2 device paths are config fields, and `moq_video::ENV` never ships +- [Gateways share one ingest config and one rate cap](/quest/m2/gateway-ingest.md) - `moq_mux::Ingest` replaces 18 per-gateway setters, and the live rate cap moves to `moq_net::bandwidth` +- [Native playout sync keeps tracks decoupled](/quest/m2/mux-sync.md) - `moq_mux::timeline` becomes `moq_mux::sync`, and one slow track no longer stalls or moves another +- [JS time values are types, not numbers](/quest/m2/js-time-types.md) - every public JS duration and instant is a `Clock.Duration` or `Clock.Instant` object, so a unit mistake fails to compile +- [Small API fixes before the cut](/quest/m2/api-shape/README.md) - one path and one spelling per item, names by role, and no dead or wrong-role public surface across net, transport, media, native, apps, JS, and bindings - [MoQ on rind](/quest/m1/rind/README.md) - every MoQ crate takes its crypto from rind, a pure-Rust graviola fork, so no build compiles C crypto (held in m1 by an open claim) - [A recreated or restarted track cannot reuse group sequences](/quest/m2/sequence-reuse.md) - recreating a track or restarting an instance cannot give new content an old group identity unless the app seeds past it, and datagrams are documented as exempt - [Subscriptions name the oldest group without a u64::MAX sentinel](/quest/m2/anchor-oldest.md) - `Anchor` gets an explicit oldest variant, decoded the same way on every wire diff --git a/quest/m2/api-shape/README.md b/quest/m2/api-shape/README.md new file mode 100644 index 00000000000..1936e5df2c4 --- /dev/null +++ b/quest/m2/api-shape/README.md @@ -0,0 +1,53 @@ +# [L] Small API fixes before the cut + +## Goal + +The surfaces `main` adds since `release` follow the repository's API rules +before the cut publishes them: one path and one spelling per item, names by +role, no dead or unreachable public items, no setter that a caller can apply +to the wrong role, and Rust, JS, and the bindings mirrored. Each child is a +batch of small, mostly breaking fixes to one area, sized so one PR reviews in +one sitting. + +## Plan + +Planned 2026-11-07 (maintainer, /quest-plan) from an audit of `release` +(a005c7ad9a) against `main` (a9d7925123) plus the open quests. Nine +read-only reviewers (net model, net session, transport, media formats, +native media, apps, JS core, browser media, bindings) scored 127 clusters +for performance, usability, testability, and future fit, using +`just api-diff` and consumer searches in this repository and its +downstream applications. The maintainer approved the plan as shown. The larger findings +became their own m1, m2, m3, and m4 quests; this epic holds the rest. + +This audit ran while [moq-dev/moq#8088](https://github.com/moq-dev/moq/pull/8088), +the previous API audit's final pass, was open, and none of its children +repeats an item #8088 changes. A child that starts after #8088 merges +rechecks each item against `main` and drops what is already done. + +Rules every child follows: + +- Removal beats addition. A public item with no caller in this repository, + a known downstream application goes private, unless an m1 or m2 quest plans a + caller. +- No serde aliases, refusals, or shims for a spelling `release` never + shipped. +- Each child lists its public API and wire impact in its PR, and its Impact + section says how to migrate. + +Rejected in planning: + +- One audit PR for every area: each child touches a different owner's code, + and #8088 showed that a 100-file audit PR spends weeks rebasing. +- Folding these items into the larger quests: those wait on decisions or + prototypes, and these do not. + +## Required + +- [moq-net's small surface fixes](/quest/m2/api-shape/net.md) - one spelling per session, admission, compression, and priority surface, and no public wire codes +- [Transports take shared types](/quest/m2/api-shape/transport.md) - moq-uring takes a congestion factory and rustls configs, and the tokio server request stops exposing its QUIC internals +- [Media crates' small surface fixes](/quest/m2/api-shape/media.md) - exporters, the container consumer, catalog color range, rate grant, moq-stats, JSON, and MSF each have one path and one name +- [Native media's small surface fixes](/quest/m2/api-shape/native.md) - audio encode internals go private, `on_demand` has one meaning, and video and render setters fold into their constructors +- [Apps' small surface fixes](/quest/m2/api-shape/apps.md) - the relay, gateways, moq-hls, moq-cli, and moq-boy drop dead or unreachable surface and gain role names +- [JS packages' small surface fixes](/quest/m2/api-shape/js.md) - track info, connection, qmux, watch, publish, ui, video, boy, and auth exports each have one path and one name +- [Bindings' small surface fixes](/quest/m2/api-shape/bindings.md) - Go's read cancel, JSON producer construction, and the Python wheel's test module diff --git a/quest/m2/api-shape/apps.md b/quest/m2/api-shape/apps.md new file mode 100644 index 00000000000..81600d3356e --- /dev/null +++ b/quest/m2/api-shape/apps.md @@ -0,0 +1,35 @@ +# [M] Apps' small surface fixes + +## Goal + +- moq-relay: + - `Internal::with_uring` becomes crate-private; it takes a type nobody + outside can build since `uring` went private. + - The hidden refusals for config keys and flags `release` never shipped + (`[session]`, `[peer]`, `--peer-*`, `--limits-*-requests`, + `auth.verifications`) are deleted. + - `cluster::Options.session` becomes `client`, matching its type and the + `[limits.client]` key. + - `Relay::{rtmp, webrtc}` become take-once `&mut self -> Option<_>`, like + `admissions()`. + - `session::View` and `shutdown::{Tally, Drained}` get + `#[non_exhaustive]`. +- moq-webrtc drops the dead `Error::{SessionNotFound, SessionClosed}` and + hides `p2p` (`#[doc(hidden)]`, as on release) until the + [m4 p2p work](/quest/m4/p2p/README.md) has a caller. +- moq-srt's `dial` module becomes private; `Client` keeps one path. +- moq-hls gains `Error::is_denied()`, so embedders stop matching nested + errors. +- `moq export hls`'s listener flags become `--hls-listen` and + `--hls-tls-{cert,key,generate,root}`, matching the `MOQ_HLS_*` + environment, with named refusals for the old flags, which shipped. +- moq-boy's Rust crate stops exporting its internals (the JS half is in + [the JS child](/quest/m2/api-shape/js.md)). + +## Plan + +Found by the 2026-11-07 API shape audit (apps); see the +[epic](/quest/m2/api-shape/README.md). Decided 2026-11-07 (maintainer): +✅ hide `p2p` until m4 (rejected: keep it public; delete it). Gateway ingest +is [gateway-ingest](/quest/m2/gateway-ingest.md). Downstream code that +matches the deleted webrtc variants loses only dead arms. diff --git a/quest/m2/api-shape/bindings.md b/quest/m2/api-shape/bindings.md new file mode 100644 index 00000000000..f192962f155 --- /dev/null +++ b/quest/m2/api-shape/bindings.md @@ -0,0 +1,23 @@ +# [S] Bindings' small surface fixes + +## Goal + +- Go: a cancelled context on `Next`, `ReadFrame`, or `RecvGroup` cancels + only that read, as in Python, Swift, Kotlin, Dart, and C++. Today + `go/wrapper/subscribe.go` passes the consumer's `Cancel`, so a caller + cannot bound one read and continue. Range-loop iterators still cancel the + consumer when the loop exits. Check first that the Go generator fork + supports a per-call cancel. +- JSON tracks have one constructor shape in moq-ffi and every wrapper: a + producer the catalog advertises comes from the catalog producer, not from + a free constructor that the caller must then register. +- Python: `moq.testing` leaves the published wheel; tests import it from the + source tree. + +## Plan + +Found by the 2026-11-07 API shape audit (bindings); see the +[epic](/quest/m2/api-shape/README.md). The generated check that each +hand-written wrapper exports every FFI item is +[binding-export-coverage](/quest/m4/binding-export-coverage.md). moq-c's +renames are in [moq-dev/moq#8060](https://github.com/moq-dev/moq/pull/8060). diff --git a/quest/m2/api-shape/js.md b/quest/m2/api-shape/js.md new file mode 100644 index 00000000000..a6b4a3f68de --- /dev/null +++ b/quest/m2/api-shape/js.md @@ -0,0 +1,35 @@ +# [M] JS packages' small surface fixes + +## Goal + +- `@moq/net`: + - A resolved `Track.Info` uses `null` for "none" (`maxAge`, `timescale`); + `Track.infoDefaults` and the `Track.rescale` alias go. + - `Connection.Established` becomes `Session`, and the orphan + `Connection.Extensions` type goes. + - `readBool` goes; it has no caller and no Rust mirror. + - `RouteOptions.instance` takes `{ id, epoch }`, like Rust's + `with_instance(instance, epoch)`. +- `@moq/qmux` exports `Session` by name instead of an anonymous default. +- `@moq/watch`: the decoder and source plumbing outputs are `@internal`, the + renderer's output is a named `Video.RendererOutput`, and ``'s + `player` property goes private, so each component has one path. +- `@moq/publish`: `Rendition.onDemand` becomes an enum, `Broadcast.join` + becomes a `Getter`, and the five internal source helpers are unexported. +- `@moq/ui` and `@moq/video` mark their helpers `@internal`; only `Support`, + `Report`, `Show`, `Level`, and `Status` stay public in `@moq/ui`, and + `rotateVideoDimensions`, `Dimensions`, `Presentation`, and + `Preview.Transcode` leave `@moq/video`'s public API. +- `@moq/boy` stops exporting `Game`, `GameConfig`, `KEY_MAP`, and its + `Moq` and `Watch` namespace re-exports, keeping the element and status + schemas. +- `@moq/auth`'s `Key.sign` takes one spelling for caps. + +## Plan + +Found by the 2026-11-07 API shape audit (JS core and browser media); see +the [epic](/quest/m2/api-shape/README.md). Durations are +[js-time-types](/quest/m2/js-time-types.md); hosted assets and the +`supported` default are m1 quests. Each change updates its package's +`api/*.api.md`, and moq.dev's npm consumer is checked for each removed +export. diff --git a/quest/m2/api-shape/media.md b/quest/m2/api-shape/media.md new file mode 100644 index 00000000000..eb4080ab6b7 --- /dev/null +++ b/quest/m2/api-shape/media.md @@ -0,0 +1,28 @@ +# [M] Media crates' small surface fixes + +## Goal + +Each item below has one path and one name: + +- moq-mux exporters (fmp4, mkv, flv, mp4): the fmp4 and catalog internals + no caller uses become private, flv returns a typed error instead of a + string, and every exporter names its wait the same way (the name moq-mux's + TS exporter uses since [moq-dev/moq#7981](https://github.com/moq-dev/moq/pull/7981)). +- The container `Consumer` has one constructor, through + `container::Playout`; `with_delay` goes. +- hang's `catalog::Range` becomes `ColorRange`, and the MoQ epoch constant + lives only in `kio::clock::Wall`; hang and moq-mux re-export nothing. +- `moq_mux::rate::Follower` becomes `rate::Grant`, so it no longer collides + with `ts::Follower`. +- moq-stats drops its ten `moq_net::stats` re-exports. +- `moq_mux::json::Options` folds into `json::Config`. +- `msf::Track.init_data` returns to `Option`. + +## Plan + +Found by the 2026-11-07 API shape audit (media formats); see the +[epic](/quest/m2/api-shape/README.md). The playout clock is +[mux-sync](/quest/m2/mux-sync.md), and rendition stats are +[rendition-stats-type](/quest/m2/rendition-stats-type.md). Check the +`ts::Follower` and `ts::Release` collision too, and rename whichever name +the PR shows is ambiguous. diff --git a/quest/m2/api-shape/native.md b/quest/m2/api-shape/native.md new file mode 100644 index 00000000000..991ab6b31ed --- /dev/null +++ b/quest/m2/api-shape/native.md @@ -0,0 +1,27 @@ +# [M] Native media's small surface fixes + +## Goal + +- moq-audio's `encode::{Control, State, Status}` become private, and + `publish_capture` stays the public path. +- `on_demand` means one thing across crates. moq-video's closes the camera + between viewers and defaults to `false`; moq-audio's keeps the device open, + defers encoding, and replays retained PCM, and defaults to `true`. Audio's + gets a name for its role (for example `retain`), and `@moq/publish`'s + audio `capture` mode is checked for the same name. +- moq-video's encode producer folds `with_track` into `new`. +- `encode::Applied` reports `cut`, so callers stop matching + `CutUnsupported` to learn it. +- moq-audio's playback `Sink::{set_volume, volume, peak}` go, since + `Control` has them, and the audio decode `Consumer::max_delay()` duplicate + goes. +- The render `Exporter::new` returns the settled `encode::Config` instead + of rewriting the caller's `&mut` config. + +## Plan + +Found by the 2026-11-07 API shape audit (native media); see the +[epic](/quest/m2/api-shape/README.md). moq-ffi, moq-c, and the wrappers +follow each rename. The device paths are in +[video-devices](/quest/m2/video-devices.md). Check downstream audio +callers of `on_demand` before picking the new name. diff --git a/quest/m2/api-shape/net.md b/quest/m2/api-shape/net.md new file mode 100644 index 00000000000..f8f19cc3011 --- /dev/null +++ b/quest/m2/api-shape/net.md @@ -0,0 +1,51 @@ +# [M] moq-net's small surface fixes + +## Goal + +Each item below lands in moq-net (and moq-auth for the mesh constants) with +its JS and binding mirrors: + +- Remove `Client::with_origin` (`rs/moq-net/src/client.rs`). It is sugar + for `with_publisher(&o).with_subscriber(o)`, release's + `Server::with_origin` is already gone, and its six callers (moq-boy, + moq-stats, the relay's cluster and stats, a moq-ffi test, and moq-tokio's + forwarder) switch to the two calls. +- Make `Version::{code, from_code}` `pub(crate)`. The relay compares + `version.code() >= 0xff0dad05` (`rs/moq-relay/src/cluster.rs`) and moq-cli + scans `0xff0dad00..=0xff0dadff`; each asks a named question of `Version` + instead (add the predicate the relay needs, and let moq-cli list + `Version`'s supported set). +- Make `group::Producer::demand` and `group::Demand` `pub(crate)`. Nothing + outside moq-net calls them, and flights stop through + `Fill::poll_unwanted`. Update `rs/moq-net/MIGRATE.md`'s mention. +- Replace `compress: bool` on track info with a `Compression` enum that + matches the wire's values, in Rust, `@moq/net`, moq-ffi, and moq-c. +- Rename `server::{Activity, Active}` to `server::{Admission, Admitted}`. +- Rename `RefusalKind::Lan` and `Refusals.lan` to `Membership` and + `membership` ("a membership proof was missing or invalid"), so moq-net's + counters name no relay flag. No serde alias: the field is new since + release. moq-stats and stats dashboards follow. +- Add `reconnect::Attempt::client(&self, &Client) -> Client`, which sets the + URL, places the token where the client's own versions put it, and attaches + the token handle. It replaces `Attempt::token()`, `Reconnect::with_tokens`'s + `versions` argument, and the caller's separate `with_auth` call. +- Make `moq_auth::mesh::{PROOF_CONTEXT, CHALLENGE_LEN, proof_message}` + private. +- Rename the datagram `Producer::poll_priority(waiter, last)` to + `poll_priority_changed(&mut self, waiter)`, keeping its own last value like + `poll_wanted_changed`. +- `Producer::route` and `Request::route` refuse an instance that + `is_minted()`, so a caller cannot claim a random instance as stable. +- After [moq-dev/moq#8063](https://github.com/moq-dev/moq/pull/8063) removes + `RouteEvent::Takeover`, un-hide `origin::Consumer::routes`, `Routes`, and + `RouteEvent`, documented as the relay-level stream that moq-relay and + downstream relays already consume, and fix `request`'s doc to say what #8063 made it + do. + +## Plan + +Found by the 2026-11-07 API shape audit (net model and net session); see +the [epic](/quest/m2/api-shape/README.md). Each item is XS except +`Attempt::client` and `Compression` (S each). `RefusalKind` is a stats-wire +field, so say in the PR which deployed dashboards read it. If #8063 has not +merged when this starts, leave the un-hide for a follow-up and say so. diff --git a/quest/m2/api-shape/transport.md b/quest/m2/api-shape/transport.md new file mode 100644 index 00000000000..d7372270b15 --- /dev/null +++ b/quest/m2/api-shape/transport.md @@ -0,0 +1,23 @@ +# [S] Transports take shared types + +## Goal + +- moq-uring stops defining its own congestion and TLS types. Its QUIC + configs take a `moq_congestion` factory (BBR3 by default, matching + moq-tokio; today `moq_uring::quic::Congestion` defaults to `Loss`) and an + `Arc` or `Arc`, as moq-tokio's + do. `moq_uring::quic::Congestion`, `Identity`, `ClientAuth`, and the client + TLS fields are gone. +- moq-tokio's `server::Request::quic()` is removed; nothing outside the + crate calls it, and it exposes the QUIC backend through the session API. + +## Plan + +Found by the 2026-11-07 API shape audit (transport); see the +[epic](/quest/m2/api-shape/README.md). The relay's io_uring path builds its +configs the way the tokio path does, so it loses its conversion code. The +moq-quic config knobs and re-export fan-out are in +[moq-quic-unused-surface](/quest/m2/moq-quic-unused-surface.md), and the +shared sans-IO driver in [quic-sansio-driver](/quest/m3/quic-sansio-driver.md). +Run the uring benches before and after, since the default controller +changes. diff --git a/quest/m2/cluster-routing/routes/session.md b/quest/m2/cluster-routing/routes/session.md index 9afc6653e1c..905d2b61296 100644 --- a/quest/m2/cluster-routing/routes/session.md +++ b/quest/m2/cluster-routing/routes/session.md @@ -32,6 +32,16 @@ node table, and the public API drops what the route layer replaced. Remove them, or hide them behind those wires, when the rollout window closes (see Waiting). +Added 2026-11-07 (maintainer, /quest-plan from the API shape audit), for +the PR that lands this ([moq-dev/moq#8023](https://github.com/moq-dev/moq/pull/8023)): + +- An origin has one identity on both wires, seeded from `Config.hop`, not a + hop ID beside a separate random route identity. +- `Cost::{MAX, DRAIN}` become `pub(crate)`. A drained route is the lowest + preference, never a saturated metric ("drain is a preference state", in + the [epic](/quest/m2/cluster-routing/README.md)), so sessions stop + stamping `Cost::DRAIN` on GOAWAY, including on moq-transport. + ## Waiting - Maintainer: decide when `Hops`, `RouteInfo::hops`, and the saturating diff --git a/quest/m2/gateway-ingest.md b/quest/m2/gateway-ingest.md new file mode 100644 index 00000000000..7e8c30772fe --- /dev/null +++ b/quest/m2/gateway-ingest.md @@ -0,0 +1,30 @@ +# [M] Gateways share one ingest config and one rate cap + +## Goal + +Every protocol gateway configures ingest the same way, and no setter applies +to a direction it cannot affect: + +- `moq_mux::Ingest { max_age, bandwidth, linger }` (`#[non_exhaustive]`, + `Default`) replaces the 18 `with_max_age`/`with_bandwidth`/`with_linger` + setters and fields on moq-rtmp's and moq-srt's `Client` and `Publish`, and + moq-webrtc's `server::Config` and `client::Config`. It is taken by + `rtmp::Publish::accept`, `rtmp::Client::pull`, `srt::Publish::accept`, + `srt::Client::pull`, `moq_webrtc::whip::Request`, and + `moq_rtsp::Client::pull`. Egress keeps its own setters on `publish`. +- `moq_webrtc::rate::{Cap, Limit}` move to `moq_net::bandwidth` beside + `Rate`, so WHIP, WHEP, SRT, and the relay's grant caps share one live cap + handle. + +## Plan + +Found by the 2026-11-07 API shape audit (apps). Today a bidirectional client +documents its ingest setters as "pull direction only", so setting +`with_linger` and then calling `publish` is silently ignored. moq-rtsp +publishes with fixed defaults and gains the same knobs. Land before the cut +so moq-webrtc never ships its own `Cap`/`Limit` and moq-srt never copies +them: [srt-rate-handle](/quest/m3/srt-rate-handle.md) and +[rtc-pacing](/quest/m3/rtc-pacing.md) take `bandwidth::Limit`. moq-cli's +`import --max-age`/`--linger` build one `Ingest`. Whether +`moq_quic::limits::Rate`'s `Unlimited` variant can become `Option` is +out of scope. diff --git a/quest/m2/js-time-types.md b/quest/m2/js-time-types.md new file mode 100644 index 00000000000..58f4bf51e6e --- /dev/null +++ b/quest/m2/js-time-types.md @@ -0,0 +1,49 @@ +# [L] JS time values are types, not numbers + +## Goal + +Every public duration and instant in the published JS packages is a +`Clock.Duration` or `Clock.Instant` object, so a unit mistake fails to +compile and cannot be stripped by `as number` or arithmetic. Today +`@moq/clock` and `@moq/signals` take `Nano`, `@moq/net` takes `Milli` +(`ConnectProps.timeout`, `Backoff`, `Delay`, `Info.maxAge`, fetch `maxDelay`, +`Goaway.handover`, `Drain.timeout`, `Probe.rtt`), and `@moq/qmux` takes raw +unbranded milliseconds (`Config.idleTimeout`, `handshakeTimeout`). That +costs 54 non-test unit conversions in `js/` alone. Covers +`@moq/clock`, `@moq/signals`, `@moq/net`, `@moq/qmux`, `@moq/hang`, +`@moq/json`, `@moq/room`, `@moq/watch`, and `@moq/publish`. `@moq/net` +re-exports `Clock` as it re-exports `Signals`. + +## Plan + +Decided 2026-11-07 (maintainer, /quest-plan from the API shape audit): +"Can we make a proper new type instead of an alias? Converting to number is +easy but dangerous." ✅ `Duration` and `Instant` classes, mirroring +`kio::clock` (rejected: a `Duration` class with `Instant` kept a branded +number; branded `Nano` everywhere, which keeps the `as number` escape; +moving everything to `Milli`, which reverses #5706 and loses timer +precision). + +- `Clock.Duration` is immutable, with unit constructors (`Duration.millis`, + `secs`, `micros`, `nanos`), unit accessors, `add`/`sub`/`mul`, comparison, + and no implicit number conversion (`valueOf` refuses). `Clock.Instant` + supports `add(Duration)`, `since(Instant) -> Duration`, and comparison; + `Wall` follows the same rule. +- The wire still carries whole milliseconds; rounding stays inside the + encoders, as it does today. +- `Manual`, `sleep`, `timeout`, `interval`, and the signals timers take the + new types. +- Per-frame paths now allocate (watch `Sync`, the audio jitter ring, hang's + container reader, publish encoders). Run their benches (`js/*/bench`) + against main and report the deltas in the PR; avoid allocations where a + hot loop can keep an internal number without exposing it. +- Decide here whether `Time.Timestamp.value` becomes a `bigint` or keeps a + `number`, together with [time addressing's JS half](/quest/m2/time-addressing/js.md), + which makes group starts `bigint` timestamps. + +Breaking in every listed package; the Impact section gives the migration +(`x as Milli` becomes `Clock.Duration.millis(x)`). + +## Related + +- [Time addressing in JS](/quest/m2/time-addressing/js.md) - rekeys groups by timestamp in the same packages diff --git a/quest/m2/moq-quic-unused-surface.md b/quest/m2/moq-quic-unused-surface.md index 2c610dd793a..8018c28ae94 100644 --- a/quest/m2/moq-quic-unused-surface.md +++ b/quest/m2/moq-quic-unused-surface.md @@ -6,7 +6,17 @@ moq-quic's public API loses the inherited surfaces nothing calls: `endpoint::{Accepting, Accepted}`, `Endpoint::{start_accept, finish_accept}`, `Connection::{side, is_handshaking}`, and `stream::Id::{new, dir, initiator}`. Each is removed, not just made private, unless a caller inside moq-quic needs -it, in which case it becomes private. The PR's Impact section says how to +it, in which case it becomes private. + +The config knobs nothing outside moq-quic sets become private at their +current defaults: `packet_threshold`, `time_threshold`, +`persistent_congestion_threshold`, `allow_spin`, `crypto_buffer_size`, +`pad_to_mtu`, `send_fairness`, `min_mtu`, `min_reset_interval`, +`retry_token_lifetime`, `incoming_buffer_size_total`, and `initial_rtt`. +moq-tokio, moq-uring, and qmux each replace their lists of moq-quic names +(20 in moq-tokio's `quic::driver`) with one `pub use moq_quic;`, so each +item has one path and a moq-quic bump changes no list. qmux keeps `Version`, +its own wire concept. The PR's Impact section says how to migrate; the release cut copies that into `MIGRATE.md`. ## Plan @@ -17,5 +27,8 @@ before the cut, since removing public API after it would be another break. Not chosen: folding the list into the post-cut [dead-code audit](/quest/m3/audit-dead-code.md). +Added 2026-11-07 (maintainer, /quest-plan from the API shape audit): the +config knobs and the re-export fan-out, which grows the quest from [XS]. + Confirm each has no caller across the workspace, bindings, and vendored crates before removing it. diff --git a/quest/m2/mux-sync.md b/quest/m2/mux-sync.md new file mode 100644 index 00000000000..92f6a3296db --- /dev/null +++ b/quest/m2/mux-sync.md @@ -0,0 +1,38 @@ +# [M] Native playout sync keeps tracks decoupled + +## Goal + +`moq_mux::timeline` becomes `moq_mux::sync` and follows the browser +player's model: lip sync in the steady state, but one slow track never stalls +or moves another. A wall-clock reference holds the broadcast's delay, each +track converges on it (audio by time-stretching toward it), and audio no +longer owns the anchor. `moq play` (`rs/moq-cli/src/play/`) moves to it, and +the module's duplicate `Kind` gives way to `container::Kind`. + +## Plan + +Decided 2026-11-07 (maintainer, /quest-plan from the API shape audit): +✅ decouple and rename (rejected: rename only, keeping the audio anchor; +keeping it as is). The 2026-11-02 decision behind +[moq-dev/moq#7759](https://github.com/moq-dev/moq/pull/7759) ("while audio +plays, only audio moves the anchor") is superseded by the 2026-11-05 ruling +on [moq-dev/moq#7941](https://github.com/moq-dev/moq/pull/7941) for the JS +player: "I kind of don't want one slow track to impact everything. We want +lip sync in the steady state but desync during the congestion state." + +- ✅ Name: `moq_mux::sync`, matching `@moq/watch`'s `Sync` by role + (rejected: `presentation`, "presentation sucks"; `playhead`; `schedule`). + `container::Playout` keeps its name. +- Port the JS semantics from `js/watch/src/sync.ts` (per-track timelines on + one reference, rework in #7941) rather than inventing a third model. + `Producer::{audio_restarted, audio_stopped}` and `Consumer::target` likely + go. +- `moq play`'s timeline tests move with the module and change where audio + stalls used to hold video; add a case where stalled audio no longer holds + video. +- moq-dev/studio's moq-scene playout consumes this module through a path + dependency and adapts on its next pin; this PR names that migration in its + Impact section. + +Breaking: the module is new on main, but release had an unrelated +`moq_mux::timeline`, so the cut's MIGRATE entry says both are gone. diff --git a/quest/m2/no-sentinels.md b/quest/m2/no-sentinels.md new file mode 100644 index 00000000000..02a256feb6d --- /dev/null +++ b/quest/m2/no-sentinels.md @@ -0,0 +1,29 @@ +# [S] No sentinel values in public API + +## Goal + +No public type uses a huge value or zero as a marker for "none" or +"unbounded"; each uses `Option` (or `NonZero*` where zero is invalid): + +- `moq_net::transport::ReceiveWindow.connection` is documented as + `u64::MAX` "when streams share none"; it becomes `Option`, + matching `transport::Limits.receive_window`. +- `cache_duration` defaults to `Duration::MAX` on `origin::Config` and + `broadcast::Info` in moq-net, and `moq_relay::cache::Cache.duration` does + the same; each becomes `Option`, `None` meaning no ceiling. +- `moq_net::reconnect`'s `Config.timeout`, `Backoff.timeout`, and + `Backoff.max` use zero for unbounded; they become `Option`, as + `Server::with_timeout` already is. The JS mirror (`Connection.Backoff`) + follows, and the Rust (10 s) and JS (unlimited) defaults for the backoff + timeout agree. +- moq-ffi's `moq_ffi_test_clock_now` returns `u64::MAX` when no clock runs; + it reports absence without a sentinel. + +## Plan + +Found by the 2026-11-07 API shape audit (net-model, net-session, apps, +bindings). AGENTS.md: "Never use a huge value like `u64::MAX` as a marker." +Each struct already breaks in this release, so no extra break is taken. +`Range::oldest()`'s `Relative(u64::MAX)` is owned by +[anchor-oldest](https://github.com/moq-dev/moq/pull/8149), not here. Pick +the aligned backoff default in the PR and say why. diff --git a/quest/m2/rendition-stats-type.md b/quest/m2/rendition-stats-type.md index ca0c15418bf..2157accdc23 100644 --- a/quest/m2/rendition-stats-type.md +++ b/quest/m2/rendition-stats-type.md @@ -6,7 +6,9 @@ type (`z.input` of the schema behind `Stats.Rendition`), so publish no longer keeps its own `RenditionStats` shape (`js/publish/src/rendition-internal.ts`) beside the schema it writes. The encoders keep writing plain numbers into it -(`effect.proxy` in `js/publish/src/{video,audio}/encoder.ts`). +(`effect.proxy` in `js/publish/src/{video,audio}/encoder.ts`). The same +applies to `@moq/publish`'s `Video.Stats` and `Audio.Stats`, which also +restate the schema's fields by hand. ## Plan @@ -19,4 +21,7 @@ same input type, since the encoders write it directly, not through `Report.Writer`. Not chosen: dropping the `u53` brand from the stats schemas, which would lose the 53-bit integer check on read. +Added 2026-11-07 (maintainer, /quest-plan from the API shape audit): +`Video.Stats` and `Audio.Stats`, a third and fourth copy of the shape. + It changes a published TypeScript type, so it lands before the cut. diff --git a/quest/m2/route-roles.md b/quest/m2/route-roles.md new file mode 100644 index 00000000000..b856f5b4562 --- /dev/null +++ b/quest/m2/route-roles.md @@ -0,0 +1,43 @@ +# [L] A route's role is its type + +## Goal + +No route method panics, throws, or returns an error because the route was +made by the other constructor. Today one `Route` type serves both exact +routes (`origin.route(path)`) and prefix routes (`route_prefix`): +`Route::broadcast()` panics on a prefix route in Rust +(`rs/moq-net/src/model/origin.rs`, "# Panics"), throws in JS +(`js/net/src/origin.ts`), and returns `MoqError::Internal` in moq-ffi +(`rs/moq-ffi/src/origin.rs`), while `requested()` ends at once on an exact +route. The fix lands in Rust, JS, moq-ffi, the hand-written C ABI, and every +binding wrapper, and `RouteInfo`'s public `Default` (whose `instance()` +panics) goes private. + +## Plan + +Decided 2026-11-07 (maintainer, /quest-plan from the API shape audit): +replace [moq-dev/moq#7806](https://github.com/moq-dev/moq/pull/7806)'s +single handle with a shape where the misuse is unrepresentable. ✅ Prototype +two shapes and pick from the prototypes (rejected: an `Option`-returning +`broadcast()`, which keeps a runtime check in every caller; keeping #7806 as +is). "Every route is a prefix on the wire" (maintainer), so: + +- **Single prefix Route**: one `Route` always claims a prefix. + `route.broadcast(suffix)` publishes a broadcast under it, an exact route is + the empty suffix, and `requested()` yields requests for any suffix the + route covers. +- **Split**: `route()` returns `Route` (with `broadcast()`), `route_prefix()` + returns a `Claim` (with `requested()`), and the shared methods (`announce`, + `withdraw`, `set_cost`, `set_auth`, `instance`) are on both. + +Build both in Rust first, port each to the in-repo callers (moq-mux, hang, +moq-cli, moq-relay, moq-archive, moq-ffi, moq-c) and sketch the diff for +downstream callers (`.route(` and `route_prefix` sites), then +put the comparison in the PR under `## Waiting` with a recommendation. After +the maintainer picks, land the winner in JS, moq-ffi, moq-c, and the +wrappers in the same PR. The route instance accessor's five spellings across +bindings stay with [the exporter](/quest/m3/ffi-exporter.md). + +## Required + +- [A job's exact route continues its claim's readers](/quest/m2/announce-takeover.md) - changes how `Request::route` lends a claim's instance, which both shapes must keep diff --git a/quest/m2/time-addressing/model.md b/quest/m2/time-addressing/model.md index a1ecaab2f5a..e8c7495e797 100644 --- a/quest/m2/time-addressing/model.md +++ b/quest/m2/time-addressing/model.md @@ -128,6 +128,12 @@ recorded only sequencing. stamps the catalog from the broadcast clock and refuses to list a track off that clock's timescale, or a scale-less track beside timed ones. moq-net tracks stay independent. +- **No local clock in the model.** Added 2026-11-07 (maintainer, + /quest-plan from the API shape audit): `Timestamp::now()`, the only model + API that reads the local clock, is removed, and its caller (moq-boy) + stamps from its own clock. `Timed

` moves from moq-net's root to + moq-json, whose producers and readers are its only users, and its `at` + becomes required under the rule above. - **Open, for whoever starts this, with the maintainer:** how lite-05/06, which carry both a sequence and frame timestamps, map on the way out. @@ -174,7 +180,8 @@ The PR's Impact section says how to migrate. Public API: breaking. `group::Info`, `create_group`, `append_group`, `Position`, `Range` ends, `fetch_group`, `Subscriber::floor`, and group consumers address by start timestamp, every producer write takes a required -timestamp, and untimed tracks become scale-less. +timestamp, untimed tracks become scale-less, `Timestamp::now` is gone, and +`Timed` is moq-json's. Wire: no format change; legacy and lite-07-wip group IDs become start ticks. ## Required diff --git a/quest/m2/video-devices.md b/quest/m2/video-devices.md new file mode 100644 index 00000000000..d6cb225691f --- /dev/null +++ b/quest/m2/video-devices.md @@ -0,0 +1,22 @@ +# [S] moq-video takes devices, not environment variables + +## Goal + +moq-video reads no environment. The VA-API render node and the V4L2 encoder +and decoder device paths are Linux-only `Option` fields on +`encode::Config` and `decode::Config`, next to `encode::Config::input`. +`moq_video::ENV` and the reads in `rs/moq-video/src/frame/vaapi.rs` and +`rs/moq-video/src/v4l2.rs` are gone. moq-cli declares `MOQ_VAAPI_DEVICE`, +`MOQ_V4L2_ENCODER`, and `MOQ_V4L2_DECODER` as its own settings and passes +them in, so moq-ffi, studio, and other embedders can pick a device in code. + +## Plan + +Found by the 2026-11-07 API shape audit (native-media, scored 3/10). +[env-knobs](/quest/m4/env-knobs.md) decided on 2026-10-25 that "a library +reads no environment", but it lands after the cut while `ENV` is new public +surface since release. This quest takes moq-video's half before the cut so +`ENV` never ships; env-knobs keeps the test and bench knobs. Tests stop +mutating process environment (`frame/vaapi.rs`). Decide in the PR whether +moq-ffi and moq-c expose the device fields now or when a binding consumer +asks; the audit found none. diff --git a/quest/m3/quic-sansio-driver.md b/quest/m3/quic-sansio-driver.md index 617c5dc1ba6..58102c8bec0 100644 --- a/quest/m3/quic-sansio-driver.md +++ b/quest/m3/quic-sansio-driver.md @@ -40,7 +40,12 @@ Open, for whoever starts it: - How it builds its TLS configs (identity, roots, fingerprint pinning) without moq-tokio's `tls` module. -Mirror the two drivers' names and signatures where they overlap. Test it against +Decided 2026-11-07 (maintainer, /quest-plan from the API shape audit): +this driver is the one shared core, not a third copy. moq-tokio's +`quic::driver` and moq-uring's `quic` become I/O loops around it, so the +endpoint, connection table, and timer logic exist once. Not chosen: a third +driver that mirrors the other two. Mirror the two drivers' names and +signatures where they still differ. Test it against moq-tokio's server over loopback on simulated time. Bound a hostile peer's cost as the other drivers do. diff --git a/quest/m3/release/README.md b/quest/m3/release/README.md index 6c97681a750..0accc575b42 100644 --- a/quest/m3/release/README.md +++ b/quest/m3/release/README.md @@ -41,6 +41,7 @@ production stability proven merely because upstream artifacts published. - [Retire libmoq](/quest/m3/release/libmoq-retire.md) - cleanup only after that publication - [Retire hand-written C](/quest/m3/release/c-retire.md) - delete the hand-written crate once generated C publishes - [Retire the web-transport JS qmux](/quest/m3/release/qmux-js-retire.md) - once `@moq/qmux` publishes from `js/qmux`, deprecate `@moq/web-socket-stream` and stop the second publisher +- [Retire the web-transport Rust qmux](/quest/m3/release/qmux-rs-retire.md) - once `qmux` publishes from `rs/qmux`, moq-dev/web-transport stops publishing the crate - [moq.sh deployment](/quest/m3/release/moq-sh-deploy.md) - verify its release-triggered publication - [Runner client bump](/quest/m3/release/runner-client-bump.md) - the upstream interop runner's Rust client moves to the published moq-tokio, so it stops stamping every object - [Smoke clients use request and follow](/quest/m3/release/smoke-bindings-rename.md) - condition: a binding release ships the renames; then the native smoke clients move to them diff --git a/quest/m3/release/api-review.md b/quest/m3/release/api-review.md index 6ab00ec24d2..9104e5c0d67 100644 --- a/quest/m3/release/api-review.md +++ b/quest/m3/release/api-review.md @@ -28,6 +28,22 @@ each approved change as a quest in m2, which holds the cut until it lands. Model items only the built-in sessions use stay private ([moq-dev/moq#6747](https://github.com/moq-dev/moq/pull/6747)); this review judges the shape of what ships. +This run's first pass, ahead of m2 (2026-11-07, maintainer, /quest-plan): +an audit of `release` (a005c7ad9a) against `main` (a9d7925123) plus the open +quests. `just api-diff` gave the mechanical inventory; nine read-only +reviewers (net model, net session, transport, media formats, native media, +apps, JS core, browser media, bindings) scored 127 clusters out of 10 for +performance, usability, testability, and future fit, and searched for +consumers in this repository and its downstream applications. Mean 6.9; three +clusters were restrictive (the native playout clock, JS duration units, and +moq-video's environment knobs). Its approved changes are m2 quests +(route-roles, js-time-types, mux-sync, no-sentinels, video-devices, +gateway-ingest, and the [api-shape epic](/quest/m2/api-shape/README.md)), +two m1 quests, and plan edits. Check first, next run: surfaces `just +api-diff` cannot see (GStreamer properties, CLI flags and env, MIGRATE drift) +and publish versions of packages exempt from the cut's version. +This review still runs after m2 lands, over what changed since. + ## Required - [Breaking release readiness](/quest/m2/README.md) - the release's code is final before it is reviewed diff --git a/quest/m3/release/cut.md b/quest/m3/release/cut.md index 45f6d150f14..09c2a6f0dcd 100644 --- a/quest/m3/release/cut.md +++ b/quest/m3/release/cut.md @@ -12,7 +12,14 @@ then main into release using merge commits. Confirm the maintainer's cut instruction before executing it. Right after merging release into main, set every eligible package with `just version 0.22.0` for this cut, then one minor up per cut, following [Versions](/CONTRIBUTING.md#versions), including -packages that did not change. Rust +packages that did not change. `@moq/qmux` and the Rust `qmux` crate are +exempt from that version (`version.json`), so check each against its +registry by hand: npm already has `@moq/qmux` 0.3.3 and 0.3.4, published +from moq-dev/web-transport, while `js/qmux` is at 0.3.3. Publishing 0.3.3 +fails, and `workspace:^` would resolve `@moq/net`'s dependency to +web-transport's 0.3.4, a different API. `js/qmux` must publish above 0.3.4; +the maintainer picks the version (found 2026-11-07 by the API shape audit). +Rust publication remains owned by release-plz. This supersedes keeping `moq-rs` at its unpublished 0.6.0 (decided 2026-11-01). Run candidate compatibility, draft, package, and all-language interop checks, and verify diff --git a/quest/m3/release/migrate-audit.md b/quest/m3/release/migrate-audit.md index 65eebaf2d2d..45a62e30e0e 100644 --- a/quest/m3/release/migrate-audit.md +++ b/quest/m3/release/migrate-audit.md @@ -69,6 +69,26 @@ m1 quest on 2026-10-22. Write it into `rs/moq-relay/MIGRATE.md`'s transition section (its `## Unreleased` section until the cut names the version). +- Known drift (added 2026-11-07, from the API shape audit), to fix in the + guide each names: + - moq-webrtc: `rs/moq-webrtc/MIGRATE.md` is still titled "Migrating + moq-rtc" and documents `wire::Source`, which + [moq-dev/moq#6454](https://github.com/moq-dev/moq/pull/6454) removed. + - moq-boy: `rs/moq-boy/MIGRATE.md` documents `.socket/{name}` and + `.socket/viewer-*` paths and grants; the code serves `.server/{name}` + and `.client/{id}`. + - moq-cli: `rtmp` is listed under `--max-delay` but its flag is `--delay`; + the `--connect-once` removal and the `MOQ_LISTEN_TLS_*` to + `MOQ_HLS_TLS_*` rename are missing; the `export archive` entry reads as a + break of something release never had. + - moq-relay: the line saying no transport enforces bitrate caps is stale + (QUIC enforces them through the pacer); the two `receive_window` floor + entries (64 MiB, then 1 MiB) collapse into the net change; and the + removed `moq_relay_stale_datagrams_total` metric has no note. + - moq-gst: the request pad's `encoder` property, shipped in + `moq-gst-v0.4.24`, was removed with no entry, so a `gst-launch` line + setting it now fails at runtime. + ## Required - [Breaking release readiness](/quest/m2/README.md) - all breaking work and the MIGRATE.md convention have landed diff --git a/quest/m3/release/qmux-rs-retire.md b/quest/m3/release/qmux-rs-retire.md new file mode 100644 index 00000000000..023603d24ea --- /dev/null +++ b/quest/m3/release/qmux-rs-retire.md @@ -0,0 +1,19 @@ +# [XS] Retire the web-transport Rust qmux + +## Goal + +After the first `qmux` crate built from `rs/qmux` in this repository is on +crates.io, moq-dev/web-transport no longer publishes `qmux`: its `rs/qmux` +is frozen or removed, its release-plz config drops the crate, and its README +points at moq-dev/moq, so one repository owns the crate name. + +Check with `cargo info qmux` (its repository names moq-dev/moq) and the +web-transport release-plz config. + +## Plan + +Found by the 2026-11-07 API shape audit (transport); mirrors +[qmux-js-retire](/quest/m3/release/qmux-js-retire.md) for the Rust crate. +web-transport still carries `qmux` 0.6.6 and live quests that change it; +move or close them there. Waiting on the crate's publication and a +maintainer with rights in moq-dev/web-transport. diff --git a/quest/m3/rtc-pacing.md b/quest/m3/rtc-pacing.md index 1cecb22d650..33c1707fd88 100644 --- a/quest/m3/rtc-pacing.md +++ b/quest/m3/rtc-pacing.md @@ -58,8 +58,11 @@ WHEP enforcement needs it and nobody owned it. Public API: a rate handle on moq-webrtc's WHEP server. Decided 2026-10-30 (maintainer): `whep::accept` takes the same shape as `whip::accept`, a -`whep::Request` struct with `Request::with_rate(rate::Limit)`, replacing -its argument list rather than adding a fifth argument. Wire: none. +`whep::Request` struct with `Request::with_rate(bandwidth::Limit)`, +replacing its argument list rather than adding a fifth argument. The handle +is `moq_net::bandwidth::Limit`, which replaces moq-webrtc's +`rate::{Cap, Limit}` in [gateway-ingest](/quest/m2/gateway-ingest.md) +(2026-11-07, maintainer, /quest-plan from the API shape audit). Wire: none. ## Related diff --git a/quest/m3/srt-rate-handle.md b/quest/m3/srt-rate-handle.md index b359a861551..c28d78420d5 100644 --- a/quest/m3/srt-rate-handle.md +++ b/quest/m3/srt-rate-handle.md @@ -52,7 +52,10 @@ enforcement needs it and nobody owned it. included, stays under the cap is untouched; under the cap nothing changes. Public API: a rate handle and a rate-limit close reason on moq-srt's server. -Wire: none. +The handle is `moq_net::bandwidth::Limit`, shared with WHIP, WHEP, and the +relay's grant caps since [gateway-ingest](/quest/m2/gateway-ingest.md) +(2026-11-07, maintainer, /quest-plan from the API shape audit), not a +moq-srt copy. Wire: none. ## Required diff --git a/quest/m4/README.md b/quest/m4/README.md index 217bbd487ae..35d8c12062c 100644 --- a/quest/m4/README.md +++ b/quest/m4/README.md @@ -66,6 +66,8 @@ deleted. - [Drop kio's epoch fence](/quest/m4/crossbeam-pin-fence.md) - waiting on crossbeam#1352, then the `Read::drop` fence goes - [A cancelled miss stops waiting](/quest/m4/fill-wait-cancel-test.md) - a test proves a relay miss waiting on its own fill ends when no reader wants it - [No MOQ_* knobs outside the binaries](/quest/m4/env-knobs.md) - test, bench, and library knobs leave the `MOQ_*` prefix +- [The API diff reports GStreamer element properties](/quest/m4/api-diff-gst.md) - `just api-diff` lists added, removed, and changed moq-gst properties +- [Hand-written wrappers reach every FFI item](/quest/m4/binding-export-coverage.md) - Python, Go, and Swift fail a test when an FFI item has no wrapper path, as Kotlin does - [The HLS server follows each path once](/quest/m4/hls-follow-once.md) - `Following` reports synced absence, so moq-hls resolves with one follow - [moq-bench pins the instances it measures](/quest/m4/bench-epoch-pin.md) - pooled requests pin their announced epoch, so a takeover is not counted as the same broadcast - [moq-bench reports sub-millisecond latency](/quest/m4/bench-latency-resolution.md) - steady-window latency resolves finer than 1 ms diff --git a/quest/m4/api-diff-gst.md b/quest/m4/api-diff-gst.md new file mode 100644 index 00000000000..4a7b1a51f13 --- /dev/null +++ b/quest/m4/api-diff-gst.md @@ -0,0 +1,18 @@ +# [S] The API diff reports GStreamer element properties + +## Goal + +`just api-diff` lists added, removed, and changed properties of `moqsink`, +`moqsrc`, and their pads (name, type, flags, default) between two refs, so +a removed property shows up in the cut's API review. Today +`cargo-semver-checks` sees only Rust items and reports moq-gst unchanged, +which is how the `encoder` pad property left with no MIGRATE entry. + +## Plan + +Found by the 2026-11-07 API shape audit (bindings). Dump the properties for +each ref with `gst-inspect-1.0` on a built plugin (or a small Rust test that +walks the elements' `ParamSpec`s, which avoids a GStreamer install +difference between refs) and diff the dumps in `sh/api-diff/`. The missing +`encoder` MIGRATE entry itself is in +[migrate-audit](/quest/m3/release/migrate-audit.md). diff --git a/quest/m4/binding-export-coverage.md b/quest/m4/binding-export-coverage.md new file mode 100644 index 00000000000..2246a155127 --- /dev/null +++ b/quest/m4/binding-export-coverage.md @@ -0,0 +1,17 @@ +# [S] Hand-written wrappers reach every FFI item + +## Goal + +The Python, Go, and Swift wrappers each fail a test when a moq-ffi record, +enum, or handle has no wrapper path, as Kotlin's smoke test already does +(`SmokeTest.kt`). Today these three hand-wrap 3,000 to 5,000 lines each, +and parity depends on review. + +## Plan + +Found by the 2026-11-07 API shape audit (bindings). Generate the list of +FFI items from UniFFI's metadata once, and have each wrapper's test check +it against the wrapper's public types, with an explicit allowlist for items +a wrapper deliberately hides. Kotlin keeps its own check or moves to the +shared list. When [the exporter](/quest/m3/ffi-exporter.md) replaces UniFFI, +the list comes from it. diff --git a/quest/m4/env-knobs.md b/quest/m4/env-knobs.md index f98bb0c918b..9d56a97722b 100644 --- a/quest/m4/env-knobs.md +++ b/quest/m4/env-knobs.md @@ -11,7 +11,9 @@ from [moq-dev/moq#6633](https://github.com/moq-dev/moq/pull/6633). - Rename test and bench knobs out of `MOQ_*` (for example `MOQ_SHAPER_SEED`, `MOQ_QUIC_SEED`, `MOQ_JSON_SEED`, `MOQ_AUDIO_DEVICE`, `MOQ_BENCH_*`, `MOQ_X11_BENCH_LOG`), updating CI and docs that set them. -- moq-video reads `MOQ_VAAPI_DEVICE` and `MOQ_V4L2_*` from the environment as - a library. Decided 2026-10-25 (recommendation, maintainer delegated): a - library reads no environment; the binaries that link moq-video declare these - as settings and pass them in. +- Decided 2026-10-25 (recommendation, maintainer delegated): a library reads + no environment; the binaries that link it declare their settings and pass + them in. moq-video's half (`MOQ_VAAPI_DEVICE`, `MOQ_V4L2_*`, and the public + `moq_video::ENV`) moved to [video-devices](/quest/m2/video-devices.md) on + 2026-11-07 (maintainer, /quest-plan from the API shape audit), because + `ENV` is new public API the cut would otherwise ship.