diff --git a/AGENTS.md b/AGENTS.md index 62b87bd63aa..0e4ef5e14e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,7 @@ The API is the most important thing to get right. A bad shape costs a breaking change in every language, and the surface is huge. - Report the public API and wire impact of every change, in the PR description and whenever asked. -- A breaking change marks its title with `!` and its Impact bullet says how to migrate. Only the release cut writes `MIGRATE.md` sections; leave existing Unreleased notes for that cut to consume. +- A breaking change marks its title with `!` and its Impact bullet says how to migrate. Feature PRs add their migration notes under `## Unreleased`; only the release cut writes version sections. - Keep things private until a current or planned consumer needs them. Scrutinize every new exported item. - Never add `foo_with_x`, `foo_checked`, or a compatibility shim. Make the breaking change to `foo` instead. - Let the type system make misuse unrepresentable diff --git a/quest/m1/README.md b/quest/m1/README.md index 83a8810e5b4..5db356e09dd 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -136,6 +136,7 @@ removed). - [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - a lite-07 SUBSCRIBE asks for past and live ranges with absolute or relative ends, in either order, and replaces FETCH; merged ranges never starve a subscriber; relays fill misses by range, including over moq-transport - [Drop set](/quest/m1/drop-set.md) - each cursor judges missing groups against its own budget, so a narrow reader drops promptly and a widening re-offers groups already reported dropped on lite-07 - [Broadcast archives](/quest/m1/archive/README.md) - ANNOUNCE, TRACKS, and a SUBSCRIBE per track record every track under a prefix to any object_store, a host serves a stored broadcast back, and the catalog drops its archive pointer for a per-rendition `hls` field +- [Subscriptions take a Delay { min, max }](/quest/m1/subscription-delay.md) - one Delay type across every subscription API, and omitted max means 0 in every binding - [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it - [Smoke token subject](/quest/m1/smoke-token-subject.md) - condition: grant subject ships; then the release smoke test signs with `--subject` - [Moxygen pin bump](/quest/m1/moxygen-pin-bump.md) - waiting on moxygen#264, then the TRACK_STATUS patch is dropped @@ -171,6 +172,7 @@ removed). - [JS join tests cover draft 20](/quest/m1/join-tests-draft20.md) - join.test.ts runs on every supported draft, not just 18 and 22 - [Poll-first async](/quest/m1/poll-first/README.md) - library crates compile no coroutines by default: async wrappers are generated from `poll_*` and gated behind an `async` feature - [Audit retention defaults](/quest/m1/max-age-defaults.md) - every max-age default listed, with a proposed finite default for the maintainer +- [JS ordered reads wait only on owed groups](/quest/m1/js-ordered-owed.md) - @moq/net matches Rust's owed-group wait - [Binding announcements always carry captures](/quest/m1/ffi-required-captures.md) - moq-ffi and its generated bindings make `captures` a required list, as #7266 always sets it - [HLS export follows a resolution change](/quest/m1/hls-reinit-same-codec.md) - a decoder-identity change under the same codec string starts a new init and segment tag - [A JS reader finishes after a clean close](/quest/m1/js-close-range-finish.md) - a range ending below the cache delivers what's held and then ends diff --git a/quest/m1/js-ordered-owed.md b/quest/m1/js-ordered-owed.md new file mode 100644 index 00000000000..8feb7a02cbe --- /dev/null +++ b/quest/m1/js-ordered-owed.md @@ -0,0 +1,21 @@ +# [S] JS ordered reads wait only on owed groups + +## Goal + +`@moq/net`'s ordered reader on a subscribed track waits at a hole while the feed may still deliver that group, bounded by its max delay, as Rust's does since [moq-dev/moq#7403](https://github.com/moq-dev/moq/pull/7403). It no longer skips the gap, and local tracks still skip. + +## Plan + +Decided 2026-10-30 (maintainer, follow-up planning): m1, for parity with Rust before the cut. Port #7403's rule rather than invent a JS one: + +- Only subscription-fed copies owe groups. +- An unlimited budget waits until the hole resolves, with no grace period. +- A finite budget gives up on the same media-time and wall-clock rule. + +Port #7403's Rust tests, using JS's manual clock. + +Public API: none. Wire: none. + +## Related + +- [Ordered reads wait only on owed groups](https://github.com/moq-dev/moq/pull/7403) - its PR, #7403, is the Rust rule this ports diff --git a/quest/m1/lite07-finalize.md b/quest/m1/lite07-finalize.md index 8d35753ff1b..73a951d9774 100644 --- a/quest/m1/lite07-finalize.md +++ b/quest/m1/lite07-finalize.md @@ -114,6 +114,7 @@ timestamp bound in [#6168](https://github.com/moq-dev/moq/pull/6168). - [Lite-07 by start](/quest/m1/time-addressing/lite07.md) - lite-07 carries group starts, previous-start links, GROUP_END, and time bounds instead of sequence numbers - [@moq/net by start](/quest/m1/time-addressing/js.md) - JS speaks the same lite-07 wire - [A job's exact route continues its claim's readers](/quest/m1/announce-takeover.md) - the job's route inherits the claim's identity, with no new wire message +- [Subscriptions take a Delay { min, max }](/quest/m1/subscription-delay.md) - lite-07 SUBSCRIBE carries the delay's min as well as its max ## Related diff --git a/quest/m1/subscription-delay.md b/quest/m1/subscription-delay.md new file mode 100644 index 00000000000..f83f4370502 --- /dev/null +++ b/quest/m1/subscription-delay.md @@ -0,0 +1,27 @@ +# [L] Subscriptions take a Delay { min, max } + +## Goal + +Every subscription API takes the same `Delay { min, max }` the consumer APIs got in [moq-dev/moq#7311](https://github.com/moq-dev/moq/pull/7311), in place of a bare max delay. That covers moq-net `Subscription`, `@moq/net`'s `Subscription`, FFI `MoqSubscription`, and moq-c `moq_subscription`. The `Delay` type moves to moq-net / `@moq/net`, and an omitted max means 0 in every binding, including Dart, Go, and C. + +## Plan + +Decided 2026-10-30 (maintainer, follow-up planning): m1, because it breaks the public API and doing it before the cut avoids a second break. + +- **Move:** move `Delay` from `moq_mux::container` / `Container.Delay` into `moq_net::track` / `Track.Delay`. Update moq-mux's callers directly and leave no re-export, since `AGENTS.md` forbids compatibility shims; the MIGRATE note names the new path. +- **Rename:** rename `max_delay` to `delay.max` throughout, about 1,050 Rust and 600 JS references. `max` travels on the wire as today. +- **`min` on the wire:** decided 2026-10-30 (maintainer): because `Delay` lives in moq-net, `min` goes on the lite-07 SUBSCRIBE (and SUBSCRIBE_UPDATE) too, following the rule that everything in moq-net survives the wire. Had `Delay` stayed in moq-mux, it would have stayed local. A relay forwards a subscriber's `min` upstream, and merged subscriptions take the smallest. Nothing reads it on the publisher side yet; later uses could include starting a cold join server-side, delay-tolerant batching, and priority under congestion. Versions that can't carry `min` (lite-01..06, moq-transport) follow the `carries_max_delay` precedent (`rs/moq-net/src/lite/subscribe.rs:113`): encoding to them drops `min`, and decoding from them yields "not stated", not 0. A relay forwards a not-stated `min` as not stated, and it doesn't count toward a merged subscription's smallest, so a legacy peer doesn't pull a merged `min` to 0. +- **Semantics:** they're settled in [moq-dev/moq#7403](https://github.com/moq-dev/moq/pull/7403): every default is 0, unlimited is explicit (Rust `None`, JS `null`, FFI null), and an omitted JS field means 0. +- **Binding defaults:** this change also fixes the bindings where an omitted max is still unlimited. + - Dart: an optional nullable parameter. + - Go: a nil `MaxDelay` in a zero `Subscription{}`. + - C: `max_delay_present` is false in a zeroed struct. + + Each one's omitted or zeroed form becomes 0, and unlimited stays expressible. Dart defaults the parameter to `Duration.zero`, so an explicit `null` stays unlimited. Go's nil and C's unset `present` can't mean both, so each gains an explicit unlimited flag in the delay; the PR names it. The breaking binding changes get MIGRATE notes under `## Unreleased`. + +Public API: breaking in every binding. Wire: lite-07 SUBSCRIBE and SUBSCRIBE_UPDATE gain `min`. + +## Related + +- [A max-delay decoder joins an earlier group](https://github.com/moq-dev/moq/pull/7311) - its PR, #7311, adds `Delay` +- [Ordered reads wait only on owed groups](https://github.com/moq-dev/moq/pull/7403) - the Rust rule; its PR, #7403, settles the max-delay defaults and touches the same fields diff --git a/quest/m2/README.md b/quest/m2/README.md index 9bcf9e51893..0b036312330 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -229,6 +229,9 @@ lite-07; bitrate enforcement is not part of this release. - [The HLS server follows each path once](/quest/m2/hls-follow-once.md) - `Following` reports synced absence, so moq-hls resolves with one follow - [moq-gst playback shares one session context](/quest/m2/gst-session-context.md) - the playback helpers take one context instead of six arguments - [moq-bench pins the instances it measures](/quest/m2/bench-epoch-pin.md) - pooled requests pin their announced epoch, so a takeover is not counted as the same broadcast +- [Audio's Latest start reaches back](/quest/m2/audio-latest-reach-back.md) - moq_audio's start covers min plus warmup like other consumers +- [moq-lite tails count drops](/quest/m2/lite-tail-drop-count.md) - SUBSCRIBE_DROP accounting replaces the session-tail grace from lite-05 +- [Re-run the SPS fuzzer on h264-reader](/quest/m2/sps-fuzz-h264-reader.md) - the H.264 SPS overflow is rechecked on the new parser - [Bindings see a broadcast end](/quest/m2/binding-broadcast-end.md) - `MoqBroadcastConsumer` exposes the close, and a moq-transport run end is a protocol error - [Audio track changes are gapless](/quest/m2/gapless-audio-switch.md) - no silence beyond a frame on audio format or rendition changes, enforced by media QA - [We own our WebRTC stack](/quest/m2/webrtc/README.md) - moq-rtc becomes moq-webrtc with moq-ice, the data-channel transport, and a vendored, trimmed str0m/dimpl/is/sctp-proto diff --git a/quest/m2/audio-latest-reach-back.md b/quest/m2/audio-latest-reach-back.md new file mode 100644 index 00000000000..7cef2998013 --- /dev/null +++ b/quest/m2/audio-latest-reach-back.md @@ -0,0 +1,16 @@ +# [S] Audio's Latest start reaches back by min plus warmup + +## Goal + +`moq_audio`'s decoder under `Start::Latest` starts at the group covering live edge − (`delay.min` + warmup), as the other consumers do since [moq-dev/moq#7311](https://github.com/moq-dev/moq/pull/7311). Today it floors at the live-edge group and primes its warmup by trimming. + +## Plan + +Decided 2026-10-30 (maintainer, follow-up planning): m2. Use the same start resolution the container consumer uses rather than a second one. With `min` and warmup at 0, the start is unchanged. Add a test of a cold join with a 2-group-old start. [Audio join budget](https://github.com/moq-dev/moq/pull/7342) changes the same `joining_latest` path in `rs/moq-audio/src/decode/consumer.rs`; whichever lands second builds its start on the other's. + +Public API: none. Wire: none. + +## Related + +- [A max-delay decoder joins an earlier group](https://github.com/moq-dev/moq/pull/7311) - its PR, #7311, adds the start rule +- [Audio join budget](https://github.com/moq-dev/moq/pull/7342) - the other `Start::Latest` join fix in `rs/moq-audio/src/decode/consumer.rs` diff --git a/quest/m2/lite-tail-drop-count.md b/quest/m2/lite-tail-drop-count.md new file mode 100644 index 00000000000..d72ee0fed33 --- /dev/null +++ b/quest/m2/lite-tail-drop-count.md @@ -0,0 +1,20 @@ +# [M] moq-lite session tails count drops instead of waiting a grace + +## Goal + +From lite-05 on, a moq-lite session's tail is complete once every group up to the end has arrived or been reported in SUBSCRIBE_DROP. The 1 s `ledger::GRACE` (`rs/moq-net/src/ledger.rs:19`) then no longer decides it, in Rust and JS. + +## Plan + +Decided 2026-10-30 (maintainer, follow-up planning): m2. Found by the audit on [moq-dev/moq#7403](https://github.com/moq-dev/moq/pull/7403). The maintainer dislikes grace periods. + +- Count per version. lite-01..04 have no drop message, so they keep today's behaviour. +- Account for holes left by lost datagrams. +- Guard against a peer that never accounts for a group: decide with the maintainer whether that ends the tail with an error or keeps waiting. Don't add a new grace. +- The IETF tail keeps its grace until the moq-transport gap-signal extension (planned in [moq-dev/moq#7425](https://github.com/moq-dev/moq/pull/7425)) lands, since moq-transport can't signal gaps. + +Public API: none. Wire: none. + +## Related + +- [Drop set](/quest/m1/drop-set.md) - the per-cursor drop reporting this counts diff --git a/quest/m2/sps-fuzz-h264-reader.md b/quest/m2/sps-fuzz-h264-reader.md new file mode 100644 index 00000000000..8fb06b343fc --- /dev/null +++ b/quest/m2/sps-fuzz-h264-reader.md @@ -0,0 +1,16 @@ +# [XS] Re-run the SPS fuzzer on h264-reader + +## Goal + +The H.264 SPS fuzz target runs against moq-mux's parser after [moq-dev/moq#7398](https://github.com/moq-dev/moq/pull/7398) moves it onto `h264-reader`. Any overflow or panic it finds there is fixed in moq-mux's validation and recorded in the fuzz-findings SPS quest. + +## Plan + +Decided 2026-10-30 (maintainer, follow-up planning): [moq-dev/moq#7313](https://github.com/moq-dev/moq/pull/7313) found the crop and width overflows in `h264-parser` ([therishidesai/h264-parser#9](https://github.com/therishidesai/h264-parser/issues/9)). #7398 replaces that crate for H.264, so the finding has to be rechecked on the new parser. Run the target long enough to cover the old reproducer and a fresh corpus. If `h264-reader` is clean, narrow the SPS quest to H.265. + +Public API: none. Wire: none. + +## Required + +- [Parse H.264 with maintained crates](/quest/m2/h264-reader.md) - its PR, #7398, moves SPS parsing to h264-reader +- [Media parsers have fuzz targets](/quest/m2/media-fuzz.md) - its PR, #7313, adds the SPS target and the fuzz-findings epic diff --git a/rs/moq-net/AGENTS.md b/rs/moq-net/AGENTS.md index 19c3fb7943c..89318f865fd 100644 --- a/rs/moq-net/AGENTS.md +++ b/rs/moq-net/AGENTS.md @@ -5,6 +5,7 @@ The wire layer. Generic over the transport and media-agnostic: the relay never i - New capabilities are additive SETUP extensions; receivers ignore unknown parameters. - Closes never cascade. Aborting a group must not tear down its track, broadcast, or siblings. - A route change is never fatal on its own. A subscription stays on the instance it started on while that instance still has a route, even after a newer route wins the path. Once none is left (e.g. the publisher's session closed), it ends with an error, never a clean end, and the app requests the path again. Never stitch it onto another instance. A clean end means only a deliberate publisher finish. Players start, stop, and restart on announcements, never on request errors. See "Publisher epochs" in `doc/concept/moq-lite.md`. +- Nothing in moq-net is local-only: every field and behaviour in its API survives the wire, so a subscription, announcement, or end means the same through a relay as in-process. A local-only knob belongs in a crate above moq-net (moq-mux, hang). - Local and remote paths behave the same: a local close, cancel, or error reaches local readers exactly as the remote equivalent would, never as a cleaner end. - The cache is a performance optimization. A relay behaves the same warm or cold: report upstream state (e.g. the largest group), not what the cache happens to hold. - A relay never blocks an answer on other nodes finishing something (replay, catch-up, a full announce set). Report what is known now; a rare undercount is acceptable.