diff --git a/quest/m1/README.md b/quest/m1/README.md index 440054a151..07e925cd97 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -24,6 +24,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Announce compression](/quest/m1/announce-compression.md) - a lite-07 announce reuses the path head and hop-chain tail of a live announcement on its stream instead of resending them - [Rust track tail](/quest/m1/rust-track-tail.md) - a moq-net subscriber accepts groups that arrive after the subscription's end, and PublishDone carries the real stream count - [Session death error](/quest/m1/session-death-error.md) - a dying session ends its tracks with its own error in Rust and JS, never a clean end, `Dropped`, or `Cancel` +- [JS bare FIN](/quest/m1/js-bare-fin.md) - a `@moq/net` subscriber aborts a track whose subscribe stream FINs before its declared end, like Rust - [Signal.race cleanup](/quest/m1/signal-race.md) - `Signal.race` releases its signal listeners when its result loses a race - [Origin narrowing](/quest/m1/origin-narrowing.md) - a live origin grant narrows in place and ends the subscriptions it no longer covers, the deafen boundary #2714 asked for - [Auth expiry clock](/quest/m1/auth-expiry-clock.md) - moq-auth and the relay hold one fixed expiry deadline and honour the same skew allowance @@ -39,6 +40,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [IETF announce count](/quest/m1/ietf-announce-count.md) - an opt-in moq-transport extension carries the replay count, so IETF announce consumers go live without a timer - [Jitter clock](/quest/m1/jitter-flush-clock.md) - renditions advertise `delay` (lag behind the earliest track) and `jitter` (spread), measured at encoder flush, never lowered; js/watch sizes playout over what it subscribes +- [Import discontinuity](/quest/m1/import-discontinuity.md) - a seek or pause resets the flush jitter baseline, from moqsink, libmoq, and moq-ffi - [Data jitter](/quest/m1/data-jitter.md) - JSON and binary tracks with a capture time advertise a detected `delay` and `jitter` - [Play tune-in backpressure](/quest/m1/play-tunein-backpressure.md) - moq play: a tune-in burst larger than the video queue parks the decoder, so the clock never reaches live at a wide `--delay` - [JavaScript FETCH](/quest/m1/js-fetch.md) - generic on-demand group serving and IETF FETCH for browser publishers @@ -47,6 +49,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Tooling](/quest/m1/tooling/README.md) - justfiles become a one-line menu over `sh/`, one impact map scopes CI, and every workflow step runs a recipe - [Path patterns](/quest/m1/path-patterns.md) - one matcher for every predicate over broadcast paths: tokens, origins, interest - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish +- [Tests under load](/quest/m1/test-flakes.md) - three tests that time out or run out of file descriptors under `just check` are fixed at the cause - [Decoded frame ownership](/quest/m1/decoded-frames.md) - retain moq-video Frames across bindings, with native views or CPU conversion as needed - [C++ through moq-ffi](/quest/m1/cpp/README.md) - generated C++ over moq-ffi with futures and expected-style errors, shipped as a tarball, vcpkg, and Conan, and adopted by the OBS plugin - [OBS native codecs](/quest/m1/obs-moq-video/README.md) - remove FFmpeg decoding dependencies, deliver GPU frames, and use native audio/video encoders diff --git a/quest/m1/archive/README.md b/quest/m1/archive/README.md index acff3c3023..8c388a19bc 100644 --- a/quest/m1/archive/README.md +++ b/quest/m1/archive/README.md @@ -105,6 +105,8 @@ owned by that prerequisite, not duplicated in archive storage. - [Recording writer](/quest/m1/archive/writer.md) - feed the segmenter from a `broadcast::Consumer`, store each segment, then commit its record - [Recording reader](/quest/m1/archive/reader.md) - serve archived FETCH through a supplied `broadcast::Producer` +- [Paced replay](/quest/m1/archive/paced-replay.md) - a replay pushes its groups to live subscribers on one shared clock, so any live player plays it +- [Replay provenance](/quest/m1/archive/provenance.md) - a replay's catalog names its timeline, replay path, store URL, and format version - [Browser archive](/quest/m1/archive/browser.md) - the same contract for browser-published broadcasts - [Offline archive HLS](/quest/m1/archive/hls.md) - render playlists from the archive timeline and fetch segment media lazily - [DVR rewind](/quest/m1/archive/dvr.md) - seek through a bounded archive and return to live playback diff --git a/quest/m1/archive/paced-replay.md b/quest/m1/archive/paced-replay.md new file mode 100644 index 0000000000..c17cd658e6 --- /dev/null +++ b/quest/m1/archive/paced-replay.md @@ -0,0 +1,27 @@ +# [M] Paced replay + +## Goal + +A live player plays a replayed recording unchanged: `moq import archive` +publishes each recorded track to SUBSCRIBE as well as FETCH, pushing its groups +when a shared clock reaches their media time, like `ffmpeg -re`. `export ts` +and web watch play a replay the way they play a live broadcast, and a viewer +who joins late joins mid-replay. + +## Plan + +- Today `moq_archive::Reader` publishes only the timeline track live and serves + media groups on FETCH through `track::Dynamic`, so a subscriber sees none. +- One clock per import, not per subscriber. It starts at the earliest recorded + timestamp across the selected tracks and every track paces against it, so + tracks stay in sync and every viewer sees the same moment. +- A timeline gap is skipped at pace (the clock keeps running). A growing + archive is followed as new timeline segments land. A finite archive ends + each track after its last group. +- FETCH keeps serving any advertised group, so DVR and HLS are unaffected. +- The API shape (a reader option vs a separate paced publisher) and the CLI + default follow the root API rules; report both in the PR. Update + `doc/bin/cli.md`. +- Test with paused time: record, replay paced, and assert a plain subscriber + gets every group in order at media pace, that a late subscriber starts at the + current group, and that two tracks stay aligned. diff --git a/quest/m1/archive/provenance.md b/quest/m1/archive/provenance.md new file mode 100644 index 0000000000..c766a0dc70 --- /dev/null +++ b/quest/m1/archive/provenance.md @@ -0,0 +1,18 @@ +# [S] Replay provenance + +## Goal + +The catalog a replay publishes carries the root `archive` entry the line +promises: the timeline track, the replay path, the store URL, and the format +version. A player or tool can tell a replay from the live source and find the +recording. + +## Plan + +- Check which of these fields `rs/hang` and `js/hang` already define. Add any + missing ones as optional fields in both, and in the Recording section of + `drafts/draft-lcurley-moq-hang.md`. +- Never advertise credentials: strip userinfo and query from the store URL, and + let the importer opt out of advertising the URL at all. +- `moq import archive` fills the entry. Test that a round trip through + `export archive` and `import archive` advertises it. diff --git a/quest/m1/broadcast-close/README.md b/quest/m1/broadcast-close/README.md index 22d5bf99ba..65302ef805 100644 --- a/quest/m1/broadcast-close/README.md +++ b/quest/m1/broadcast-close/README.md @@ -41,6 +41,7 @@ removal. - [Rust close](/quest/m1/broadcast-close/rust.md) - moq-net gains `close()`, deprecates `finish`/`abort`/`is_finished`, and every Rust and JS caller moves over - [Binding close](/quest/m1/broadcast-close/bindings.md) - moq-ffi, libmoq, and every wrapper expose `close()` and deprecate `finish` +- [Kotlin end](/quest/m1/broadcast-close/kotlin-end.md) - Kotlin exposes `close()` as `end()`, since `AutoCloseable.close()` takes the name - [Remove finish](/quest/m1/broadcast-close/remove.md) - on dev, the deprecated broadcast end APIs are gone and `closed()` carries no cause ## Related diff --git a/quest/m1/broadcast-close/kotlin-end.md b/quest/m1/broadcast-close/kotlin-end.md new file mode 100644 index 0000000000..4ecb51e805 --- /dev/null +++ b/quest/m1/broadcast-close/kotlin-end.md @@ -0,0 +1,18 @@ +# [XS] Kotlin end + +## Goal + +Kotlin can force-end a broadcast after `finish` is removed. Its +`MoqBroadcastProducer` exposes the moq-ffi `close()` as `end()`, because a +generated `close()` would collide with `AutoCloseable.close()`. + +## Plan + +- Rename the method for Kotlin only in `rs/moq-ffi/uniffi.toml`; every other + binding keeps `close()`. Kotlin's `close()` still only releases the handle. +- Test that `end()` ends the broadcast while a `dynamic()` handle is still + held. Document it in `doc/lib/kt`. + +## Required + +- [Binding close](/quest/m1/broadcast-close/bindings.md) - moq-ffi gains the `close()` this renames diff --git a/quest/m1/broadcast-close/remove.md b/quest/m1/broadcast-close/remove.md index 805896a6ee..4b62cc1db5 100644 --- a/quest/m1/broadcast-close/remove.md +++ b/quest/m1/broadcast-close/remove.md @@ -19,3 +19,4 @@ This is a published API break, so it targets `dev`. ## Required - [Binding close](/quest/m1/broadcast-close/bindings.md) - every binding already has `close()` to move to +- [Kotlin end](/quest/m1/broadcast-close/kotlin-end.md) - Kotlin keeps a forced end once `finish` is gone diff --git a/quest/m1/import-discontinuity.md b/quest/m1/import-discontinuity.md new file mode 100644 index 0000000000..c5c2fd6025 --- /dev/null +++ b/quest/m1/import-discontinuity.md @@ -0,0 +1,20 @@ +# [S] Import discontinuity + +## Goal + +A publisher that seeks or pauses tells its importer so, and the flush jitter +measurement restarts instead of counting the break. `moqsink` does it on a +seek, and C and FFI publishers can do it too. + +## Plan + +- `moq_mux::container::Producer::discontinuity()` already resets the flush + baseline (`catalog/estimate.rs`). `import::Track` gains a `discontinuity()` + that forwards to it, and the codec importers without one gain it too. Only + the baseline resets; advertised values are never lowered. +- `moqsink` calls it when a pad re-anchors after a flush or a new segment + (`rs/moq-gst/src/sink/pad.rs`). Test that a seek on an `encoder` pad does + not raise the advertised jitter. +- Expose it as `moq_publish_media_discontinuity` in libmoq and as + `discontinuity()` in moq-ffi and its hand-written wrappers, per the + Cross-Package Sync table. This is additive, so it lands on `main`. diff --git a/quest/m1/jitter-flush-clock.md b/quest/m1/jitter-flush-clock.md index ad27a5edf6..e83e361955 100644 --- a/quest/m1/jitter-flush-clock.md +++ b/quest/m1/jitter-flush-clock.md @@ -34,13 +34,11 @@ media span of each emitted batch and advertise no `delay`. it. The provisional PTS-gap floor is gone, and `moq_mux::Error::JitterDecreased` plus zero-as-absent text jitter enforce never-lower in Rust and JS. `moq-gst` pads opt in with `encoder=true`; imports stay clock-free. - What remains below is `delay` and the player. libmoq and moq-ffi expose - `flush` but no discontinuity, so a binding publisher that pauses and resumes - on a re-anchored PTS within the window would count the pause; add one when - such a caller appears. A `moq-gst` encoder pad has the same gap across a - `PLAYING -> PAUSED -> PLAYING` cycle (running time stops, the wall clock - does not) and a flushing seek, since `import::Track` forwards no - discontinuity. + What remains below is `delay` and the player. A seek or pause resetting the + baseline from moqsink and the bindings is + [Import discontinuity](/quest/m1/import-discontinuity.md), including a + `moq-gst` encoder pad across a `PLAYING -> PAUSED -> PLAYING` cycle (running + time stops, the wall clock does not) and a flushing seek. - **Measurement.** Lateness is `now - timestamp`, observed by the existing `flush` calls, so no call site changes. Each rendition keeps its own baseline, the minimum lateness over a sliding window (about 10 s), so a media diff --git a/quest/m1/js-bare-fin.md b/quest/m1/js-bare-fin.md new file mode 100644 index 0000000000..212b0cdb53 --- /dev/null +++ b/quest/m1/js-bare-fin.md @@ -0,0 +1,18 @@ +# [S] JS bare FIN + +## Goal + +A `@moq/net` subscriber aborts a track whose subscribe stream FINs before the +publisher declared its end, over moq-lite and IETF, as Rust does since #4083. +A bare FIN is a failed request, never a clean end. + +## Plan + +- The drafts agree: draft-19 section 3.3.2 treats a FIN before the required + messages (PUBLISH_DONE) as a failure, and the moq-lite draft has a publisher + FIN only after SUBSCRIBE_END. +- Abort with the error Rust uses, so both languages report the same thing. Add + an interop case where a publisher FINs without declaring an end, in both + directions. + +The clean-end path this tightens landed in #4086. diff --git a/quest/m1/test-flakes.md b/quest/m1/test-flakes.md new file mode 100644 index 0000000000..909d6cbc28 --- /dev/null +++ b/quest/m1/test-flakes.md @@ -0,0 +1,22 @@ +# [M] Tests hold up under load + +## Goal + +Three tests that pass alone but fail under a full `just check` pass reliably, +fixed at the cause rather than by raising a timeout or adding a retry: + +- `js/json/src/snapshot/snapshot.test.ts:359`, "a compressed delta is gated + on its encoded size", which takes about 4.3 s against a 5 s limit. +- The `js/net/src/declarations.test.ts` test that times out at 5 s. +- `rs/moq-tokio/tests/backend.rs:739` `noq_cert_reload`, which fails with + "Too many open files". + +## Plan + +- Find why each JS test is slow. It should shrink its input or reveal a real + slowdown in the code under test; fix whichever it is. +- For `noq_cert_reload`, find what holds the descriptors: a leak in the test + or code under test, or nextest parallelism against the file limit. Fix a leak + at its source, and otherwise cap the test's concurrency in + `.config/nextest.toml`. +- Prove it by running `just check --all` several times on a loaded machine. diff --git a/quest/m2/README.md b/quest/m2/README.md index b6ac5e7697..0f3b7111fa 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -18,6 +18,8 @@ upstream release waits in [m4](/quest/m4/README.md). - [AV1 metadata separation](/quest/m2/av1-metadata.md) - retain metadata OBUs inline while evaluating separate delivery - [SEI separation](/quest/m2/sei/README.md) - retain inline SEI until measured savings or a metadata-only consumer justify a split - [Catalog track identity](/quest/m2/catalog-tracks.md) - compare immutable track definitions with explicit catalog-to-group binding +- [Archive recovery listing](/quest/m2/archive-recovery-listing.md) - a resumed DVR lists what changed since its checkpoint, not every stored group +- [Archive backward timestamps](/quest/m2/archive-backward-timestamps.md) - a resumed recording refuses a track whose timestamps go backward - [Mobile ownership](/quest/m2/mobile-ownership.md) - decide whether Rust or platform code owns mobile capture, codecs, and rendering - [iOS capture](/quest/m2/mobile-capture-ios.md) - camera and screen capture if the mobile ownership decision selects Rust - [Android capture](/quest/m2/mobile-capture-android.md) - NDK/JNI capture using the existing codecs if mobile ownership selects Rust @@ -34,6 +36,7 @@ upstream release waits in [m4](/quest/m4/README.md). - [MediaCodec decode](/quest/m2/audio-decode-mediacodec.md) - Android decodes HE-AAC, multichannel AAC, and what else the device offers - [MediaCodec encode](/quest/m2/audio-encode-mediacodec.md) - Android encodes AAC-LC - [AAC encode refusal](/quest/m2/aac-encode-refusal.md) - AAC config encode refuses channel counts it cannot name, on dev +- [OBS channel layouts](/quest/m2/obs-wave-layout.md) - the OBS source maps channel counts to the WAVE default layouts, like moq-audio - [Video codec coverage](/quest/m2/video-codec-coverage.md) - prioritize remaining native AV1 and portable decoder gaps - [#2147](/quest/m2/2147-moq-video-10-bit-hevc-and-av1-support-in-the-nvidia-codec.md) - moq-video: 10-bit HEVC and AV1 support in the NVIDIA codec path - [NVENC buffer pool](/quest/m2/nvenc-pool.md) - NVENC reuses input and output buffers instead of allocating per frame, if a benchmark shows it wins @@ -75,6 +78,7 @@ upstream release waits in [m4](/quest/m4/README.md). - [SIP media stack](/quest/m2/sip-stack.md) - terminate one inbound SIP audio call leg and expose it as Opus frames - [Carrier voice](/quest/m2/carrier-voice/README.md) - determine whether MoQ should be the call fabric for programmable carrier voice - [LiveKit WebRTC bridge](/quest/m2/livekit-webrtc-bridge.md) - a go/no-go verdict, backed by a spike, on per-track LiveKit-to-MoQ bridging +- [Expired token error](/quest/m2/auth-expired-error.md) - an expired token reports `Error::Expired`, not `Unauthorized`, in Rust, JS, and the bindings - [Common Access Tokens](/quest/m2/cat/README.md) - a moq-transport client presents a CAT in SETUP and `moq auth serve` admits it with the scope its `moqt` claim names - [Runtime QA hosts](/quest/m2/runtime-qa-hosts.md) - run exact source snapshots on accessible Linux and device hosts with retrievable debug evidence - [Media QA on other engines](/quest/m2/browser-media-qa-engines.md) - the media harness measures a Firefox or WebKit player over the fallback and names what each engine lacks diff --git a/quest/m2/aac-encode-refusal.md b/quest/m2/aac-encode-refusal.md index 3008bf0702..4f7a7371bf 100644 --- a/quest/m2/aac-encode-refusal.md +++ b/quest/m2/aac-encode-refusal.md @@ -4,17 +4,17 @@ Writing an AudioSpecificConfig for a channel count that no AAC channelConfiguration names is an error, not a stereo config with a warning, -in `moq_mux::codec::aac::Config::encode` and `@moq/hang`'s -`audioSpecificConfig`. This mirrors the parse side, which since #4093 refuses -reserved values instead of guessing stereo. +in `moq_mux::codec::aac::Config::encode`, as `@moq/hang`'s +`audioSpecificConfig` already refuses since #4119. This mirrors the parse +side, which since #4093 refuses reserved values instead of guessing stereo. ## Plan -Both functions become fallible, a published API break in each language, so -this targets `dev`. Counts with a PCE-free configuration map as today. For -the others, either write channelConfiguration 0 with a PCE derived from the -layout, or refuse. Pick one at PR time and apply it in both languages. Test -every count from 1 to 8 and one beyond. +`Config::encode` becomes fallible, a published API break, so this targets +`dev`. Counts with a PCE-free configuration map as today. Refuse the others, +matching JS; writing channelConfiguration 0 with a PCE derived from the layout +is a later additive change in both languages. Test every count from 1 to 8 and +one beyond. ## Related diff --git a/quest/m2/archive-backward-timestamps.md b/quest/m2/archive-backward-timestamps.md new file mode 100644 index 0000000000..c5175e4c41 --- /dev/null +++ b/quest/m2/archive-backward-timestamps.md @@ -0,0 +1,16 @@ +# [XS] Archive refuses backward timestamps + +## Goal + +A resumed recording refuses a track whose timestamps go backward past the +recovered timeline, failing loud like the group-ID check, instead of writing +overlapping media time. The caller starts a new prefix. + +## Plan + +Check the first group's timestamp against the recovered track's last recorded +timestamp at enrollment, and test both a backward and a forward restart. + +## Required + +- [Archive](/quest/m1/archive/README.md) - the recovery this hardens ships with the line diff --git a/quest/m2/archive-recovery-listing.md b/quest/m2/archive-recovery-listing.md new file mode 100644 index 0000000000..1c62bb5dc8 --- /dev/null +++ b/quest/m2/archive-recovery-listing.md @@ -0,0 +1,19 @@ +# [M] Archive recovery listing + +## Goal + +A DVR writer resuming a recording lists storage in proportion to what changed +since its last checkpoint, not every stored `groups/` object, while still never +deleting a referenced object. + +## Plan + +- Recovery today reconciles a complete `groups/` listing of every recorded + track before accepting input. Compare bounding it with an ordered + `list_with_offset` from the oldest retained range, with orphans below it left + to a background sweep, against a checkpointed listing marker. +- Benchmark recovery time and requests over archive size before and after. + +## Required + +- [Archive](/quest/m1/archive/README.md) - the recovery this bounds ships with the line diff --git a/quest/m2/auth-expired-error.md b/quest/m2/auth-expired-error.md new file mode 100644 index 0000000000..013ab441fe --- /dev/null +++ b/quest/m2/auth-expired-error.md @@ -0,0 +1,19 @@ +# [S] Expired token error + +## Goal + +A session whose token expired reports `moq_net::Error::Expired`, not +`Unauthorized`, over moq-lite and moq-transport, mirrored in `@moq/net` and +the bindings. A client can then refresh its token instead of treating the +refusal as final. + +## Plan + +- Add the variant to the `#[non_exhaustive]` error, so the change is additive + on `main`. Map it from lite's `AUTH_ERROR { Expired }` and moq-transport's + `EXPIRED_AUTH_TOKEN`, and back again when refusing. +- Carry it through moq-ffi's error mapping and each wrapper. + +## Required + +- [In-band auth](/quest/m1/auth/README.md) - the AUTH streams that carry these codes diff --git a/quest/m2/obs-wave-layout.md b/quest/m2/obs-wave-layout.md new file mode 100644 index 0000000000..b62afa7030 --- /dev/null +++ b/quest/m2/obs-wave-layout.md @@ -0,0 +1,17 @@ +# [XS] OBS channel layouts + +## Goal + +The OBS source maps a channel count to the same default layout moq-audio does, +the WAVE convention (3 is 2.1, 4 is quad, 6 is 5.1, 8 is 7.1), so a +multichannel broadcast plays with its speakers where the publisher put them. + +## Plan + +`audio_layout_to_speakers` in `cpp/obs/src/moq-source.cpp` maps from FFmpeg +layouts today. Map each count to the nearest OBS `speaker_layout` and refuse +the ones OBS cannot place rather than guess. Note the mapping in `doc/bin/obs.md`. + +## Related + +- [Audio codecs](/quest/m1/audio-codecs/README.md) - the channel layouts this mirrors