diff --git a/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md b/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md index d9de5a2fdd..e1a1377b0a 100644 --- a/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md +++ b/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md @@ -42,8 +42,11 @@ grant; `Options::bandwidth` documents that `opus::bitrate_floor(codec_rate, frame_size).max(500)` to `300_000 * channels` (`backend/libopus.rs`, `rs/moq-audio/src/opus.rs`). `Policy::min` defaults to a tenth of the ceiling (`rs/moq-mux/src/rate.rs`); for Opus it is the codec floor. A - grant below it disables the rendition until the grant recovers (decided - 2026-10-04 with the enabled flag); the encoder never errors. The reservation's ceiling is + grant below it disables the rendition until bandwidth recovers (decided + 2026-10-04 with the enabled flag); the encoder never errors. Once viewers + deselect it, its demand and so its grant go away, so recovery evaluates a + hypothetical share against the current estimate, as the ladder controller + does. The reservation's ceiling is the configured bitrate; only the policy target moves. - PCM: `pcm::bitrate(sample_rate, channels)` is `pub(crate)` (`rs/moq-audio/src/pcm.rs`), `Settings::bitrate` is refused for it @@ -58,8 +61,8 @@ grant; `Options::bandwidth` documents that Tests: retain the shared `Control` tests; an Opus Producer whose grant drops below its configured bitrate reports the lower `bitrate()` after one policy step, holds it on a `None` grant, and ramps back when the grant -returns; a grant below the Opus floor disables the rendition and a recovered grant -enables it; a PCM Producer +returns; a grant below the Opus floor disables the rendition, and recovered +bandwidth enables it again even after the last viewer unsubscribed; a PCM Producer ignores every grant. ## Closes diff --git a/quest/m1/README.md b/quest/m1/README.md index d292e5f712..49c666dbc5 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -33,6 +33,7 @@ QUIC studies there on that rule. - [Demand everywhere](/quest/m1/demand-everywhere.md) - requests and producers at every level watch subscribers through `demand()` alone, ahead of the bindings exposing it - [Request linger](/quest/m1/request-linger.md) - an upstream FETCH or SUBSCRIBE outlives its last reader by a short linger, so quick re-requests don't churn upstream - [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main +- [Flat questlines](/quest/m1/quest-flat-lines.md) - moq pins the current quest CLI, lands its questline branches on main, and retires them - [Resampler tail](/quest/m1/resampler-tail.md) - `moq play` plays a finite track to its last sample when its rate differs from the device's - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay - [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below diff --git a/quest/m1/auth/ws-unauthorized.md b/quest/m1/auth/ws-unauthorized.md index ccddab2965..a4621a6071 100644 --- a/quest/m1/auth/ws-unauthorized.md +++ b/quest/m1/auth/ws-unauthorized.md @@ -17,8 +17,9 @@ upgrades, reads SETUP, and then admits; the relay is the outlier. Decided (2026-10-04): the relay hands the upgraded socket to moq-tokio's `Request`, so `Connection::run` serves WebSocket as it serves QUIC, and the relay's duplicate admit and supervise loop is deleted. Every refusal becomes -an in-band close, mapped as the io_uring path does (403 to `Unauthorized`, -502 to `App(502)`). It admits after SETUP, which +an in-band close, mapped as the io_uring path does: 401 and 403 to +`Unauthorized`, 502 to `App(502)`. An expired token keeps its own +`Error::Expired` path ([expired error](/quest/m1/auth/expired-error.md)). It admits after SETUP, which [token in band](/quest/m1/auth/token-in-band.md) needs anyway. It needs a new public moq-tokio constructor for an already-upgraded socket; keep it minimal. diff --git a/quest/m1/catalog-enabled.md b/quest/m1/catalog-enabled.md index 85d0a126d6..c3ec780a4b 100644 --- a/quest/m1/catalog-enabled.md +++ b/quest/m1/catalog-enabled.md @@ -14,8 +14,9 @@ instead of a removal. Decided (2026-10-04): - Wire: an optional `enabled` boolean on every rendition, audio and video, - written only when false. Writers stop writing `stalled`; readers treat a - legacy `stalled: true` as `enabled: false`. Update + written only when false. Writers stop writing `stalled`, and readers ignore + it: released publishers still flap it, and following it would keep #4772 + alive for them. Update `drafts/draft-lcurley-moq-hang.md` (replacing the `stalled` section, with a changelog entry, since `stalled` shipped in -03), `doc/concept/hang.md`, and run `just drafts check`. MSF stops mirroring `stalled` and does not mirror @@ -37,7 +38,10 @@ Decided (2026-10-04): graph ([audio graph lifetime](/quest/m1/watch-audio-graph.md)). - Bandwidth: a rendition is enabled only once its reservation is granted, and disabled, with encoding stopped, when the grant falls below its floor, - until the grant recovers. The [ladder](/quest/m2/ladder/README.md) and + until the grant recovers. A disabled rendition loses its subscribers, and + the allocator grants nothing to an undemanded track, so recovery evaluates + a hypothetical share against the current estimate instead of waiting for a + grant. The [ladder](/quest/m2/ladder/README.md) and [audio grant following](/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md) adopt that rule; this quest only defines the field. - Everything else that names `stalled` moves to `enabled`: moq-transcode's @@ -48,8 +52,8 @@ Decided (2026-10-04): Tests: disabling an audio rendition publishes a catalog with the same rendition and `enabled: false`, and a viewer deselects it and keeps one -AudioContext across disable and enable; a legacy `stalled: true` reads as -disabled; nothing in the tree writes `stalled`. +AudioContext across disable and enable; a legacy `stalled: true` changes +nothing; nothing in the tree writes `stalled`. ## Required diff --git a/quest/m1/fmp4-export-tracks.md b/quest/m1/fmp4-export-tracks.md index f2999bc0aa..f295397158 100644 --- a/quest/m1/fmp4-export-tracks.md +++ b/quest/m1/fmp4-export-tracks.md @@ -19,11 +19,14 @@ that is not in the moov, and ffprobe rejects the file. Decided (2026-10-04): -- Init from the catalog: write avc3 and hev1 sample entries from the - catalog's profile and level, with SPS and PPS in-band, so the init is ready - at the first catalog and the wait disappears for H.264 and H.265. Apple - prefers hvc1 for HEVC and some editors handle avc3 poorly; that is the - accepted trade. +- Init from the catalog: write avc3 and hev1 sample entries with SPS and PPS + in-band, so the init is ready at the first catalog and the wait disappears + for H.264 and H.265. Only when the catalog carries everything the entry's + configuration record needs: hvcC (and avcC for high profiles) also holds + chroma format and bit depths, which `build_hvcc` reads from the SPS and the + codec string does not carry. Otherwise the track waits for its SPS in the + bounded queue below. Apple prefers hvc1 for HEVC and some editors handle + avc3 poorly; that is the accepted trade. - For a codec whose entry still needs the bitstream, drain every ready track into a per-track fragment queue before the init, keeping fragment boundaries, and flush after the moov. The queue is bounded; exceeding it @@ -41,7 +44,9 @@ Decided (2026-10-04): Tests: an H.264 Annex-B export whose audio starts 2 s before the first keyframe keeps that audio; a rendition removed and re-added after the init writes under its original id and the output parses; a new name, an -incompatible config, and a backwards replay each fail the export. +incompatible config, and a backwards replay each fail the export; a 10-bit +HEVC source whose catalog lacks bit depth waits for its SPS and is described +correctly. ## Closes diff --git a/quest/m1/quest-flat-lines.md b/quest/m1/quest-flat-lines.md new file mode 100644 index 0000000000..7913dfb82a --- /dev/null +++ b/quest/m1/quest-flat-lines.md @@ -0,0 +1,37 @@ +# [M] moq adopts flat questlines from the current quest CLI + +## Goal + +moq pins a quest release where questlines are planning groups with no branch +of their own: every quest, including a questline README's remaining work, is +its own PR against `main`. The child work already merged into questline +branches reaches `main`, and those branches retire. + +## Plan + +kixelated/quest#44 dropped questline branches, `quest branch`, and +`quest ready --remote`; kixelated/quest#51 changed the import skill to treat a +report's fix as a claim and plan each issue fully. moq still pins the quest +input in `flake.nix` from before both. + +Decided (2026-10-04): a quest of its own, not a routine pin bump, because 16 +questline branches carry merged child work that is not on `main` +(`git ls-remote origin 'refs/heads/quest/*README'`). + +- Bump the `quest` input and run `quest check` with the new CLI; fix what it + reports. +- For each questline branch, land its accumulated work on `main` through its + existing umbrella PR, or a new one, then delete the branch. A branch whose + work cannot land yet is decided with the maintainer, not left orphaned. +- Add the `quest-takeover` skill stub beside the other `.claude/skills/quest-*` + stubs, and decide with the maintainer whether it replaces the existing + `takeover` skill. +- Update `AGENTS.md` and `CONTRIBUTING.md` only where they describe questline + branches, with the maintainer's approval, as both files require. + +Done when `flake.nix` pins the new quest, no `quest/*README` branch remains, +and `quest check` passes. + +## Related + +- [Merge queue](/quest/m1/merge-queue.md) - the other trunk workflow change diff --git a/quest/m1/ts-export-rewind.md b/quest/m1/ts-export-rewind.md index aabcc25280..a4c6e797c7 100644 --- a/quest/m1/ts-export-rewind.md +++ b/quest/m1/ts-export-rewind.md @@ -4,8 +4,8 @@ `moq export ts` keeps its program clock running through gaps and sets the PCR `discontinuity_indicator` only when the clock really restarts: a `--linger` -resume or a replaced broadcast. One resume flags once. A timestamp that goes -backwards within one broadcast fails the export. +resume or a replaced broadcast. One resume flags once. A track whose timestamps +restart within one broadcast fails the export. ## Plan @@ -23,17 +23,24 @@ Decided (2026-10-04): export already emits null packets through quiet periods. - Restart the clock and set the flag only on a resume or a replaced broadcast. -- Time going backwards within one broadcast is a publisher bug (a name always - means the same content), so the export fails with an error rather than - rewinding. +- Timestamps restarting within one broadcast are a publisher bug (a name + always means the same content), so the export fails with an error rather + than rewinding. The only input timestamp is the frame's PTS, and it legally + moves backwards in decode order with B-frames (0, 120, 40, 80 ms); the + authored DTS cannot show a reset, since `author_dts` clamps every backwards + candidate to `prev + 1`. So the check runs on PTS before that clamp, and is + group-aware: reordering, including open-GOP leading pictures, stays inside + a group, so a frame whose PTS is below the largest PTS of the track's + previous group is a reset. - No new public API: the generation-change handling lives behind the crate-private `ExportSource`. - [Fixed-delay release](/quest/m1/tstd/delay.md) rewrites the same release path and re-anchors on each rewind; land this inside or after it. Tests: a resume that walks gaps on two tracks sets the indicator once; a 10 s -forward gap on one track keeps the PCR continuous with no flag; a backwards -timestamp fails the export. +forward gap on one track keeps the PCR continuous with no flag; a source whose +timestamps restart at zero after 10 s fails the export, while a B-frame +sequence and open-GOP leading pictures are still accepted. ## Closes diff --git a/quest/m2/ladder/README.md b/quest/m2/ladder/README.md index 7375b19519..55bfab5d78 100644 --- a/quest/m2/ladder/README.md +++ b/quest/m2/ladder/README.md @@ -25,10 +25,10 @@ state shipped in [moq#2865](https://github.com/moq-dev/moq/pull/2865): rendition. Routing, decoder, and presentation identities are split so a metadata-only change cannot rebuild WebCodecs. -Decided (2026-10-04): a rung is enabled only once its reservation is granted -at or above its boundary, and disabled, with encoding stopped, when the grant -falls below it, until the grant recovers. Demand loss alone never changes the -flag. +Decided (2026-10-04): a rung's `enabled` follows its last applied target. It +is disabled, with encoding stopped, when its share falls below its boundary, +and enabled once a target at or above the boundary is applied. Demand loss +alone never changes the flag. What remains is the publisher side. The allocator ([moq#2854](https://github.com/moq-dev/moq/pull/2854)) divides a connection's @@ -61,7 +61,7 @@ stall = (max + 2 * lower) / 3 The lowest rendition takes `lower = 0`, so its boundary is `max / 3`. An encoder may adapt within `[stall, max]`; below the boundary the rung is disabled (`enabled: false`) and stops encoding, and it is enabled again only -once a target above the same boundary is successfully applied. Catalog state follows the last target the encoder +once a target at or above the same boundary is successfully applied. Catalog state follows the last target the encoder *accepted*, not the one the controller requested, so a transient rate-control failure retains the last applied target rather than lying. diff --git a/quest/m2/mp4-export.md b/quest/m2/mp4-export.md index ef59a1dbe4..4525d2a2ba 100644 --- a/quest/m2/mp4-export.md +++ b/quest/m2/mp4-export.md @@ -26,11 +26,19 @@ Decided (2026-10-04): - A new `mp4` format that requires `--output` (it must seek); `export fmp4` stays a stream to stdout. -Open for the PR: a graceful-finish path in `moq-cli` (today SIGINT drops the -task set), and what a second Ctrl+C or SIGTERM does; edit lists or stretched -durations for gaps and track start offsets; co64 and a 64-bit mdat header for -files over 4 GB; whether a returning rendition with a new config gets an -extra sample description. +- Finishing is required, not best effort: the first SIGINT or SIGTERM + finalizes the file, which needs a graceful-finish path in `moq-cli` (today + SIGINT drops the task set). A second signal aborts at once and leaves the + crash-safe fragmented file. +- No size limit: always write co64 chunk offsets and a 64-bit (largesize) mdat + header. + +Tests: SIGINT during a recording produces a regular MP4 that parses with its +moov at the end; a second SIGINT leaves a playable fragmented file. + +Open for the PR: edit lists or stretched durations for gaps and track start +offsets; whether a returning rendition with a new config gets an extra sample +description. ## Related