diff --git a/quest/m1/README.md b/quest/m1/README.md index 59416deeeda..4f8e6065b0d 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -98,6 +98,8 @@ lite-07 stream type here. - [Publish channel count](/quest/m1/publish-audio-channel-count.md) - forcing a channel count on an Audio.Capture stops costing the subscriber gaps of silence, and the media fixture's cold-start audio-tone flake is fixed at its cause - [Container reads drop backfill](/quest/m1/read-backfill.md) - the JS container consumer drops groups below its read cursor like Rust - [qmux no-wait opens](/quest/m1/qmux-no-wait.md) - the in-tree @moq/qmux rejects an over-limit create when waitUntilAvailable is false, like Chrome +- [qmux local lifecycle](/quest/m1/qmux-local-lifecycle.md) - an abort blocked on send credit resets at once, and a close behind a backed-up socket still sends APPLICATION_CLOSE +- [JS release preflight](/quest/m1/js-publish-preflight.md) - `release.ts` refuses a package whose `@moq/*` dependency is not on npm, so no dependent publishes uninstallable - [Stream sockets disable Nagle](/quest/m1/tcp-nodelay.md) - every qmux TCP, TLS, and WebSocket socket, accepted or dialed, sets `TCP_NODELAY` - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [Resumed close](/quest/m1/resume-closed.md) - a resumed JS track keeps its declared end across copies, and its `closed` and `largest()` settle on a clean end @@ -106,6 +108,7 @@ lite-07 stream type here. - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS - [Interop finish](/quest/m1/interop-finish.md) - the interop matrix proves a clean broadcast finish across implementations - [Exporters follow a rendition to a new track](/quest/m1/export-rendition-rebind.md) - the fMP4, TS, MKV and FLV exporters rebuild a rendition whose resolved `track` or `broadcast` changes under the same ID +- [HLS cursor failure](/quest/m1/hls-segments-failure.md) - the export segment cursor returns a failed timeline as an error after draining, so a truncated recording never looks complete - [Track live signal](/quest/m1/track-live.md) - a reader can wait for its track's feed to be live, so a player skips old cached groups while a fresh upstream subscription starts - [moq-net compiles once](/quest/m1/net-erase-transport.md) - the session drivers stop monomorphizing per transport and per crate, saving several MiB of the relay, FFI library, `libmoq` and CLI - [Audio devices on ARM](/quest/m1/audio-devices-arm.md) - `just rs audio-devices` passes on the hosted ARM runner diff --git a/quest/m1/hls-segments-failure.md b/quest/m1/hls-segments-failure.md new file mode 100644 index 00000000000..3d8ae02a801 --- /dev/null +++ b/quest/m1/hls-segments-failure.md @@ -0,0 +1,33 @@ +# [S] The HLS segment cursor reports a failed timeline + +## Goal + +`moq-hls`'s export `segments::Consumer::next` drains the segments a failed +timeline still covers, then returns the failure as an error. `Ok(None)` +means only a clean end, so a recorder that mirrors a broadcast to storage +can't take a truncated recording for a complete one. + +## Plan + +Decided 2026-10-22 (maintainer, from the release audit): fix it before the +cut, since changing what `Ok(None)` means is a breaking change. +[moq-dev/moq#6153](https://github.com/moq-dev/moq/pull/6153) fixed the +playlist side and named this follow-up. + +In `rs/moq-hls/src/export/segments.rs`, `Consumer::next` returns `Ok(None)` +when `rendition.is_failed(&row)`, and its doc comment says so. A shared +reference failure ends the same way: `Producer::fail` records `failed` +without setting `ended`, the closed window maps to `Ready(None)`, and +`next_after` never reads `failed`. `Producer::failure()` holds the error +nobody reads. + +Return the window's or rendition's failure once the covered segments drain. +Fix the doc comment and add a `rs/moq-hls/MIGRATE.md` entry. Test the +cursor alongside the playlist for both failure sources: valid segments +first, then the error, never `Ok(None)`. + +The cursor has no consumer outside its tests yet. It stays public because +it is the documented entry point for mirroring to storage. + +Public API: `segments::Consumer::next` returns an error where it returned +`Ok(None)` (breaking). Wire: none. diff --git a/quest/m1/js-publish-preflight.md b/quest/m1/js-publish-preflight.md new file mode 100644 index 00000000000..bb57cfacc53 --- /dev/null +++ b/quest/m1/js-publish-preflight.md @@ -0,0 +1,35 @@ +# [S] JS release refuses a package whose @moq dependency is missing + +## Goal + +`js/common/release.ts` refuses to publish a package to npm when any +`@moq/*` dependency range in its built `package.json` resolves to no +published version, and names the missing ones. A failed dependency publish +can no longer leave a dependent on npm that fails to install. + +## Plan + +Decided 2026-10-22 (maintainer, from the release audit): requested as a +follow-up by [moq-dev/moq#6243](https://github.com/moq-dev/moq/pull/6243). + +`release-js.yml` runs `bun --filter '*' release`, which orders packages by +dependency but still runs dependents after a dependency's script fails. +`release.ts` skips a version already on npm and otherwise publishes; its only +`@moq/*` existence check gates the JSR dry run. `js/common/package.ts` +rewrites `workspace:^` to `^` without checking npm. + +Before `npm publish`, read the dependencies and peer dependencies of +`dist/package.json`, run `npm view @ version` for each `@moq/*` +entry, and exit non-zero listing every range with no match. Reuse the +existing JSR dry-run lookup instead of adding a second one. Confirm that +`deno publish` already refuses an unresolvable `jsr:` import; extend the +check to JSR only if it does not. + +Prove it on a dry run that depends on an unpublished `@moq/*` version: the +run fails, naming the package and range. + +Public API: none. Wire: none. + +## Related + +- [First npm publish](/quest/m2/release/npm-first-publish.md) - the one-time hand publish of the new names this check would otherwise refuse diff --git a/quest/m1/qmux-local-lifecycle.md b/quest/m1/qmux-local-lifecycle.md new file mode 100644 index 00000000000..caaeb4e8c42 --- /dev/null +++ b/quest/m1/qmux-local-lifecycle.md @@ -0,0 +1,47 @@ +# [S] qmux aborts and closes without the peer's help + +## Goal + +In the in-tree `@moq/qmux` (`js/qmux`), `writer.abort()` on a stream whose +write is waiting for stream or connection send credit rejects that write and +sends RESET_STREAM at once. `close()` closes the socket only after +APPLICATION_CLOSE is handed to it, or after a bounded stall: a backed-up +WebSocket that drains in time delivers the frame, and one still wedged at the +deadline closes without it. Neither outcome waits for the peer to grant +credit, and close never relies on a fixed delay. Rust +`rs/qmux` already behaves this way and is out of scope. + +## Plan + +Decided 2026-10-22 (maintainer, from the release audit): one quest for both, +since both live in `js/qmux/src/session.ts` and share one contract: local +lifecycle never depends on peer cooperation. Both were deferred by +[moq-dev/moq#6124](https://github.com/moq-dev/moq/pull/6124). + +Abort: the sink's `write` reaches `#claimSendCredit`, which awaits +`flow.sendCredit.claim()` and `#connCredit.claim(1n)`. `Credit.claim` in +`js/qmux/src/credit.ts` takes no signal, and WritableStream runs the sink's +`abort` only after the in-flight write settles, so the abort waits for credit. +Give `Credit.claim` an `AbortSignal` that removes its waiter and rejects, and +pass the sink controller's `signal` down through the stream data path, so the +existing abort handler sends RESET_STREAM immediately. + +Close: `close()` and `#sendConnectionClose` queue APPLICATION_CLOSE, then +`sleep(100ms)` before `#transportClose()`. The scheduler holds the frame until +`sink.ready()`, and the WebSocket ponyfill holds a write while +`bufferedAmount` is high, so a backed-up socket closes with the frame never +sent. Replace both sleeps with a promise the scheduler resolves once the +control lane has flushed. Drop queued stream frames first, so +APPLICATION_CLOSE goes out next. Bound the wait like Rust's `CLOSE_TIMEOUT` +(`rs/qmux/src/session.rs`), started only while the writer is stalled. + +Test with the controlled clock and a wedged socket: an abort blocked on each +credit kind emits RESET_STREAM with no peer grant; a close behind a socket +that drains before the deadline writes APPLICATION_CLOSE before the transport +closes; and one that never drains closes at the deadline without it. + +Public API: none. Wire: none. + +## Related + +- [qmux no-wait opens](/quest/m1/qmux-no-wait.md) - another fix in the same file; its PR is [moq-dev/moq#6271](https://github.com/moq-dev/moq/pull/6271) diff --git a/quest/m2/README.md b/quest/m2/README.md index 79fe6fff086..fb7af1b961f 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -58,6 +58,7 @@ ranked just below deterministic TS legs, which it requires. - [AudioToolbox decode](/quest/m2/audio-decode-audiotoolbox.md) - macOS and iOS decode HE-AAC, multichannel AAC, and what else the framework offers - [AudioToolbox encode](/quest/m2/audio-encode-audiotoolbox.md) - macOS and iOS encode AAC-LC - [CMAF surround Opus](/quest/m2/cmaf-opus-surround.md) - a released mp4-atom carries the `dOps` channel mapping table, and fMP4 import and export carry it through the OpusHead +- [moqsrc surround Opus](/quest/m2/moqsrc-opus-surround.md) - moqsrc sets the Opus mapping caps, so opusdec plays 3 to 8 channels - [GPU CI](/quest/m2/gpu-ci.md) - NVIDIA tests gate the merge queue on a self-hosted GPU runner VM, and `just rs nvidia` runs them locally instead of skipping - [VA-API renderer tests](/quest/m2/vaapi-render-tests.md) - the ignored VA-API renderer import tests run in `just rs gpu` on AMD and Intel and fail instead of skipping - [Vendor-free CARLA bridge](/quest/m2/carla-bridge.md) - the CARLA demo's GPU bridge lets moq-video pick the encoder, and its stand-in test runs on AMD as well as NVIDIA @@ -81,6 +82,7 @@ ranked just below deterministic TS legs, which it requires. - [Front parking](/quest/m2/origin-front-parks.md) - an unroutable request waits on a front instead of re-asking on every route-table move - [Route wakes](/quest/m2/route-wakes.md) - a route change wakes only the fronts it can move, so pool churn stops scaling with served paths - [IETF lazy discovery](/quest/m2/ietf-lazy-discovery.md) - moq-transport sessions ask for announcements only under watched prefixes, starting with UNSUBSCRIBE_NAMESPACE on d14/15 +- [Draft 14-16 session errors](/quest/m2/ietf-adapter-session-errors.md) - the moq-transport adapter keeps session-level error causes, so a terminal rejection is not redialed - [Keyed prefix polling](/quest/m2/keyed-prefix-polling.md) - `SubscriberDriver` polls only the prefixes a wake concerns, if a bench shows the slope - [Route health](/quest/m2/route-health/README.md) - epoch-only active/passive track recovery over metadata-only subscriptions - [Wrapper traits over moq-net](/quest/m2/net-wrap/README.md) - moq-net exports producer and consumer traits, and every writer and reader above it takes them, so a wrapper such as e2ee passes through unchanged diff --git a/quest/m2/audit-quests.md b/quest/m2/audit-quests.md index 0781b510253..23d97f0a9bc 100644 --- a/quest/m2/audit-quests.md +++ b/quest/m2/audit-quests.md @@ -23,6 +23,17 @@ Run `/quest-audit` after the release cut. Read every outstanding quest with parallel auditors, then verify source claims, active PR ownership, Required ordering, and the latest maintainer decisions before changing plans. +Also reconcile merged follow-ups (decided 2026-10-22 from the release audit): +read the Follow-ups of every PR merged since the last release, and plan each +concrete remainder that has no quest, open PR, or recorded decline. The +release audit found seven such remainders across +[moq-dev/moq#6124](https://github.com/moq-dev/moq/pull/6124), +[moq-dev/moq#6153](https://github.com/moq-dev/moq/pull/6153), +[moq-dev/moq#6251](https://github.com/moq-dev/moq/pull/6251), +[moq-dev/moq#6243](https://github.com/moq-dev/moq/pull/6243), +[moq-dev/moq#4991](https://github.com/moq-dev/moq/pull/4991), and +[moq-dev/moq#5758](https://github.com/moq-dev/moq/pull/5758). + Baseline from the m1 run, 2026-10-22 UTC, against `main` at `bce217e063`: - Read all 463 quest documents across m0-m3 and the root with parallel diff --git a/quest/m2/ietf-adapter-session-errors.md b/quest/m2/ietf-adapter-session-errors.md new file mode 100644 index 00000000000..597ca50a207 --- /dev/null +++ b/quest/m2/ietf-adapter-session-errors.md @@ -0,0 +1,35 @@ +# [S] The draft 14-16 adapter keeps session errors + +## Goal + +On moq-transport drafts 14 to 16, a session-level transport error from +`ControlStreamAdapter` reaches the session with its cause instead of as +`Closed`. A peer that ends the session with a terminal error, such as +ProtocolViolation or Unauthorized, is not redialed as if the drop were +transient. + +## Plan + +Decided 2026-10-22 (maintainer, from the release audit): m2. The race is +narrow and limited to the older drafts. It is the session-scope follow-up +left by [moq-dev/moq#6251](https://github.com/moq-dev/moq/pull/6251), which +kept stream errors through `AdapterError::Real`. + +In `rs/moq-net/src/ietf/adapter.rs`, the adapter's `Session` impl maps the +native `poll_accept_bi`, `poll_open_uni`, `poll_accept_uni`, the datagram +send and receive, and `poll_closed` errors to `Error::Closed`. In +`ietf/session.rs`, `adapter_run` reads the real SETUP stream first and usually +keeps the peer's code. The code is lost when a unis, dispatch, or datagram +future fails first. The session then reports `Closed`, and +`moq-tokio`'s `is_terminal` treats that as retryable. + +Carry the transport error through the way `AdapterError::Real` does, mapped +with the negotiated registry. Reproduce first: a draft-14 peer closes with +ProtocolViolation while a uni or datagram poll is pending, and the client +must see a terminal error and not redial. + +Public API: none. Wire: none. + +## Related + +- [moq-net compiles once](/quest/m1/net-erase-transport.md) - its open PR [moq-dev/moq#6176](https://github.com/moq-dev/moq/pull/6176) touches the same adapter diff --git a/quest/m2/moqsrc-opus-surround.md b/quest/m2/moqsrc-opus-surround.md new file mode 100644 index 00000000000..275e451108a --- /dev/null +++ b/quest/m2/moqsrc-opus-surround.md @@ -0,0 +1,32 @@ +# [S] moqsrc plays surround Opus + +## Goal + +`moqsrc` outputs `audio/x-opus` caps that `opusdec` accepts for a 3 to 8 +channel Opus rendition, so surround Opus published through `moqsink` plays +back through GStreamer. + +## Plan + +Decided 2026-10-22 (maintainer, from the release audit): m2. Receive never +worked, so this is not a regression. +[moq-dev/moq#4991](https://github.com/moq-dev/moq/pull/4991) fixed publishing +and named this follow-up. + +`rs/moq-gst/src/source/imp.rs` builds the caps from rate and channels, plus +`codec_data` and `stream-format=ogg` when a description exists. `opusdec` +ignores `codec_data` and reads `streamheader` or the mapping fields, so with +more than two channels it assumes family 0 and refuses. + +Parse the catalog's OpusHead description with the existing `opus::Mapping` +and set `channel-mapping-family`, `stream-count`, `coupled-count`, and +`channel-mapping`. This needs no synthesized OpusTags, unlike a +`streamheader`. Test `opusenc` 5.1 through `moqsink`, a relay, `moqsrc`, and +`opusdec`, checking six decoded channels. The only moqsrc test rendition +today is stereo. + +Public API: none. Wire: none. + +## Related + +- [CMAF surround Opus](/quest/m2/cmaf-opus-surround.md) - the same mapping table through fMP4 `dOps`, independent of this caps path diff --git a/quest/m2/release/README.md b/quest/m2/release/README.md index b7a3bb4a310..d5cf127b30d 100644 --- a/quest/m2/release/README.md +++ b/quest/m2/release/README.md @@ -30,6 +30,7 @@ production stability proven merely because upstream artifacts published. - [API-diff check](/quest/m2/release/migrate-audit.md) - PRs into release diff the public API; backports refuse breaks, the cut confirms MIGRATE.md coverage - [API shape review](/quest/m2/release/api-review.md) - every new or changed public API reviewed for a better shape before the cut - [Feature review](/quest/m2/release/feature-review.md) - every major work line since the last release judged end to end before the cut +- [Claim new npm names](/quest/m2/release/npm-first-publish.md) - a maintainer claims `@moq/clock`, `libav`, `video`, and `p2p` on npm and JSR with trusted publishing, before the cut publishes them - [Cut and publish](/quest/m2/release/cut.md) - independently executable after m1; release-triggered publication - [Published assets](/quest/m2/release/assets-release.md) - npm carries hostable worklets - [C++ publication](/quest/m2/release/cpp-release.md) - archives and matching OBS plugin publish and install diff --git a/quest/m2/release/cut.md b/quest/m2/release/cut.md index a412b1b9a40..c76dac5ef05 100644 --- a/quest/m2/release/cut.md +++ b/quest/m2/release/cut.md @@ -36,3 +36,4 @@ the new `/quest/m1/README.md`, and the cut also requires both reviews. Only - [API-diff check](/quest/m2/release/migrate-audit.md) - every break on the cut PR has its MIGRATE.md entry - [API shape review](/quest/m2/release/api-review.md) - every new or changed public API was reviewed for a better shape, and approved changes landed - [Feature review](/quest/m2/release/feature-review.md) - every major work line since the last release was judged end to end, and its follow-ups landed +- [Claim new npm names](/quest/m2/release/npm-first-publish.md) - trusted publishing can publish every new `@moq/*` package diff --git a/quest/m2/release/npm-first-publish.md b/quest/m2/release/npm-first-publish.md new file mode 100644 index 00000000000..fa93377e496 --- /dev/null +++ b/quest/m2/release/npm-first-publish.md @@ -0,0 +1,42 @@ +# [XS] Claim the new @moq npm and JSR names + +## Goal + +`@moq/clock`, `@moq/libav`, `@moq/video`, and `@moq/p2p` exist on npm, and +the three with JSR packages exist on JSR, each with trusted publishing +attached to `moq-dev/moq`. The cut's release workflow can then publish +their real versions. A maintainer does this by hand. Delete this quest once +the checks pass: + +- `npm view @moq/` resolves for all four, and the package settings show + the `moq-dev/moq` trusted publisher. +- `https://api.jsr.io/scopes/moq/packages/` returns 200 for `clock`, + `video`, and `p2p`, each linked to the repository. `@moq/libav` sets + `"jsr": false`. + +## Plan + +Found 2026-10-22 while planning the release audit's follow-ups: all four +names return 404, and nearly every published package (net, hang, watch, +publish, auth, signals, and more) now depends on `@moq/clock`. npm trusted +publishing attaches only to an existing package, so without this the cut's +publish fails on these names and still publishes dependents that can't +install. [moq-dev/moq#6243](https://github.com/moq-dev/moq/pull/6243) raised +it for `@moq/libav` alone. + +Decided 2026-10-22 (maintainer): publish a minimal `0.0.0` placeholder +(README only) per name to claim it, then attach the trusted publisher. The +cut publishes the real `0.1.0` through the workflow with provenance, so no +real version is published by hand. `release.ts` compares against the +`latest` tag, so the placeholder does not suppress that publish. + +Until the cut publishes `@moq/libav` 0.1.0, Windows `bun install` keeps +failing, since `js/libav/build.ts` there unpacks the published tarball of +the workspace version. The placeholder does not change that. + +Public API: none. Wire: none. + +## Related + +- [Release preflight](/quest/m1/js-publish-preflight.md) - refuses a dependent whose `@moq/*` dependency did not publish +- [Dart publication](/quest/m2/release/dart-publish.md) - the same trusted-publishing check for pub.dev diff --git a/quest/m3/README.md b/quest/m3/README.md index f49ec9cb81e..65c1ff9bcd6 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -58,6 +58,7 @@ m4's upstream waits in here and moved m2 work with no consumer. - [fMP4 emsg](/quest/m3/emsg.md) - settles the shared framing and missing-data semantics before this section adopts them - [ID3 catalog section](/quest/m3/id3.md) - timed ID3 as a first-class container-neutral catalog section - [FLV script tags](/quest/m3/flv-script.md) - onMetaData and AMF data messages survive RTMP and FLV import +- [Half and quarter rate AC-3](/quest/m3/ac3-low-rate.md) - bsid 9 and 10 AC-3 parse through TS and FLV instead of being refused - [MSF feedback tracks](/quest/m3/msf-publish-tracks.md) - `--stats` and `--echo` work under `--catalog-format msf` through `publishTracks`, once a consumer asks - [MSFTS convergence](/quest/m3/msfts-convergence.md) - the demultiplexed TS lane converges on MSFTS where they differ, once an MSFTS implementer engages - [#2279](/quest/m3/2279-hang-typed-scte-35-ad-cue-signaling-carried-opaquely.md) - hang: SCTE-35 cues arrive immediately on an independent metadata track, optionally associated with a rendition diff --git a/quest/m3/ac3-low-rate.md b/quest/m3/ac3-low-rate.md new file mode 100644 index 00000000000..1b9363403df --- /dev/null +++ b/quest/m3/ac3-low-rate.md @@ -0,0 +1,21 @@ +# [XS] Half and quarter rate AC-3 + +## Goal + +`moq-mux` parses AC-3 with bsid 9 and 10, the half and quarter sample-rate +variants, through TS and FLV import and TS export, instead of refusing them. + +## Plan + +Decided 2026-10-22 (maintainer, from the release audit): support them rather +than only documenting the refusal. It is cheap and removes an inconsistency +noted in [moq-dev/moq#5758](https://github.com/moq-dev/moq/pull/5758): +`ac3_core` in `rs/moq-mux/src/ts/export.rs` counts bsid up to 10 as an AC-3 +core, then hands it to `ac3::parse_header`, which refuses anything above 8. + +In `rs/moq-mux/src/codec/ac3.rs`, keep the frame-size tables and shift the +sample rate right by `bsid - 8`. Add a bsid 9 or 10 fixture through TS import +and TS export. Broadcast specs require 48 kHz, so these streams are rare, and +this stays low priority. + +Public API: none. Wire: none.