From 94138cd3f19ce0496a62fb3374c1cca434e729df Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 11:25:46 -0700 Subject: [PATCH 1/2] quest: plan the 14 open issues without a quest Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 1 + quest/m0/relay-systemd-unit.md | 19 +++++++++++++ quest/m0/release.md | 1 + quest/m1/README.md | 11 +++++++- quest/m1/cluster-routing.md | 1 + quest/m1/cross-relay-bursts.md | 34 ++++++++++++++++++++++ quest/m1/fin-wait-expiry.md | 31 ++++++++++++++++++++ quest/m1/hop-aligned-import.md | 45 ++++++++++++++++++++++++++++++ quest/m1/moqsink-keyframe-latch.md | 43 ++++++++++++++++++++-------- quest/m1/splice-fetch-remainder.md | 41 +++++++++++++++++++++++++++ quest/m1/test-flakes-2.md | 4 +++ quest/m1/tooling/justfiles.md | 9 ++++++ quest/m1/ts-programs.md | 33 ++++++++++++++++++++++ quest/m1/ts-publish-muxdelay.md | 27 ++++++++++++++++++ quest/m1/watch-decoder-recovery.md | 28 +++++++++++++++++++ quest/m1/watch-video-guards.md | 37 ++++++++++++++++++++++++ quest/m1/watch-worklet-file.md | 29 +++++++++++++++++++ quest/m2/redundant-ingest.md | 1 + 18 files changed, 382 insertions(+), 13 deletions(-) create mode 100644 quest/m0/relay-systemd-unit.md create mode 100644 quest/m1/cross-relay-bursts.md create mode 100644 quest/m1/fin-wait-expiry.md create mode 100644 quest/m1/hop-aligned-import.md create mode 100644 quest/m1/splice-fetch-remainder.md create mode 100644 quest/m1/ts-programs.md create mode 100644 quest/m1/ts-publish-muxdelay.md create mode 100644 quest/m1/watch-decoder-recovery.md create mode 100644 quest/m1/watch-video-guards.md create mode 100644 quest/m1/watch-worklet-file.md diff --git a/quest/m0/README.md b/quest/m0/README.md index e621b682fb..a1eb04189a 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -94,6 +94,7 @@ do not add another media abstraction or a renderer crate during stabilization. ## Required - [Release](/quest/m0/release.md) - the release moq.pro adopts: binding docs, an upgrade page, and a staging soak gate it rather than the merge +- [Packaged relay starts](/quest/m0/relay-systemd-unit.md) - the `.deb` and `.rpm` relay service starts instead of crash-looping on `--file` - [Skip unchanged announce updates](/quest/m0/announce-update-dedupe.md) - a publisher sends an announce update only when the wire route changed - [Wildcard](/quest/m0/wildcard/README.md) - a relay resolves subscriptions against advertised prefixes, a service claims the prefix it could serve and refuses the rest instead of enumerating broadcasts, and the browser player treats a covering claim as availability - [Local origin](/quest/m0/local-origin.md) - localhost workers read only the broadcasts their relay ingested, from the internal listener diff --git a/quest/m0/relay-systemd-unit.md b/quest/m0/relay-systemd-unit.md new file mode 100644 index 0000000000..857bbfc7bc --- /dev/null +++ b/quest/m0/relay-systemd-unit.md @@ -0,0 +1,19 @@ +# [XS] Packaged relay starts + +## Goal + +The `.deb` and `.rpm` relay service starts. Today +`packaging/moq-relay/moq-relay.service` runs `moq-relay --file +/etc/moq-relay/relay.toml`, but the config path is positional and the relay +rejects unknown flags, so every packaged install crash-loops. + +## Plan + +`ExecStart=/usr/bin/moq-relay /etc/moq-relay/relay.toml`. Add the reporter's +unit test in the relay crate that parses the unit's `ExecStart` with the +relay's own `Cli`, so the unit and the parser can't drift again. This lands +before the release, since the release ships the packages. + +## Closes + +- [#4343](https://github.com/moq-dev/moq/issues/4343) - close this issue when the quest finishes diff --git a/quest/m0/release.md b/quest/m0/release.md index 81072cc0a6..8e3bdb2a64 100644 --- a/quest/m0/release.md +++ b/quest/m0/release.md @@ -86,4 +86,5 @@ Public API: none beyond the required quests. Wire: none. ## Required +- [Packaged relay starts](/quest/m0/relay-systemd-unit.md) - the shipped systemd unit passes the config path the relay accepts - The merged relay has soaked on moq.pro staging and the maintainer has signed it off diff --git a/quest/m1/README.md b/quest/m1/README.md index 674dbee9ae..a8a2b1c2ad 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -40,14 +40,20 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Optional max age](/quest/m1/ietf-max-age.md) - max age is optional, set only by the publisher, and crosses moq-transport as MAX_CACHE_DURATION - [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 - [kio waiter overflow](/quest/m1/kio-waiter-lost.md) - a retained `Waiter` past 8 lists stops adding a duplicate entry to lists it already recorded -- [moqsink keyframe latch](/quest/m1/moqsink-keyframe-latch.md) - a header-only buffer after a break no longer permanently invalidates a moqsink video pad +- [moqsink recoverable errors](/quest/m1/moqsink-keyframe-latch.md) - a header-only buffer, a leading delta, or a timestamp rewind drops frames until a keyframe instead of invalidating a moqsink pad pad - [Capture re-anchor](/quest/m1/capture-reanchor.md) - a repeating or restarting device clock never rewinds native capture during a fast backlog drain - [Splice edge cases](/quest/m1/splice-edges.md) - an unstamped successor, a pruned segment's boundary group, and a warm head during a takeover are each handled correctly +- [Splice remainder fetch](/quest/m1/splice-fetch-remainder.md) - an origin reader never parks forever on a half-delivered group after a same-name re-create - [Track tail hardening](/quest/m1/track-tail-hardening.md) - Rust and JS wait out a track's tail by the same rules, with the known hang, count, truncation, grace, and memory holes closed +- [FIN wait expiry](/quest/m1/fin-wait-expiry.md) - a group awaiting its FIN ack still expires and follows priority updates on lite and IETF +- [Cross-relay bursts](/quest/m1/cross-relay-bursts.md) - bursty small-group tracks cross two relays without lost groups, unanswered FETCHes, or stalls - [Session death parity](/quest/m1/session-death.md) - a local close ends tracks cleanly in both languages, and JS group readers see the session's error on session death - [JS scoped routes](/quest/m1/js-scoped-routes.md) - a scoped JS origin reader announces the preferred route among those in its scope, like Rust, with the fan-out benchmarked - [JS subtree at max depth](/quest/m1/js-pattern-depth.md) - `Pattern.subtree` returns the literal path at 32 segments like Rust, pinned by a shared pattern.json vector - [moq play decode schedule](/quest/m1/play-decode-schedule.md) - `moq play` video keeps valid pictures across rewinds, reordering deeper than 100 ms, and decoder batches larger than three +- [Watch video guards](/quest/m1/watch-video-guards.md) - promoting a video track holds the last picture, and an older group never reaches the codec between live deltas +- [Watch decoder recovery](/quest/m1/watch-decoder-recovery.md) - one malformed packet rebuilds the audio or video decoder instead of ending playback +- [Watch audio under CSP](/quest/m1/watch-worklet-file.md) - production builds ship the audio worklet as a file, so `script-src 'self'` pages play audio - [moqsrc stop](/quest/m1/moqsrc-stop.md) - moqsrc's stop blocks until its session ends, without deadlocking on a blocked pad push - [More tests under load](/quest/m1/test-flakes-2.md) - the second round of load-only failures, fixed at the cause - [UnknownSession log flood](/quest/m1/unknown-session-logs.md) - streams reset before their WebTransport header stop being reported as UnknownSession at WARN @@ -56,8 +62,11 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Accept-side flags](/quest/m1/cli-given-flags.md) - dial-only and local verbs refuse every `--listen-*` flag instead of ignoring it - [JS catalog path](/quest/m1/js-catalog-path.md) - `@moq/net` broadcast consumers expose their path and `Catalog.watch` rejects escaping references, like Rust - [Full codec string](/quest/m1/publish-codec-string.md) - browser-published video carries the encoder's full RFC 6381 codec string, so native players decode it +- [TS program selection](/quest/m1/ts-programs.md) - `import ts` refuses a multi-program stream unless `--program ` picks one or publishes each - [TS export jitter](/quest/m1/ts-export-jitter.md) - the video reorder bound follows later catalogs and observed reordering, so a late B-frame never reorders TS output - [TS import shared shift](/quest/m1/ts-import-shared-shift.md) - unflagged loop wraps move audio and video by one shift, so A/V sync holds across wraps +- [ffmpeg muxdelay](/quest/m1/ts-publish-muxdelay.md) - the documented MPEG-TS publish line adds `-muxdelay 0`, so quiet audio is not clumped +- [Same-hop importers](/quest/m1/hop-aligned-import.md) - importers sharing a `--hop` and fed one stream publish identical groups and timestamps, so failover survives - [PipeWire duplicate cameras](/quest/m1/pipewire-dup-cameras.md) - a webcam lists once with PipeWire enabled - [Catalog wall clock](/quest/m1/catalog-wall-clock.md) - `Clock::wall_clock` keeps the catalog's full precision instead of truncating to milliseconds - [Capture control](/quest/m1/capture-control.md) - on dev, `encode::Capture` replaces `CaptureOptions`, an unsupported `cut()` errors, and dropping the last `Control` cancels in-flight opens diff --git a/quest/m1/cluster-routing.md b/quest/m1/cluster-routing.md index 13d66d28a8..5b7140e524 100644 --- a/quest/m1/cluster-routing.md +++ b/quest/m1/cluster-routing.md @@ -114,3 +114,4 @@ make MoQ's common case. - [Skip unchanged announce updates](/quest/m0/announce-update-dedupe.md) - cuts duplicate updates on today's routing - [Local origin](/quest/m0/local-origin.md) - workers stop reading hop chains before they go - [Wildcard](/quest/m0/wildcard/README.md) - the specificity, pool spread, and reply identity this selection builds on +- [Cross-relay delivery under bursts](/quest/m1/cross-relay-bursts.md) - its #4349 report also shows closed broadcasts announced for up to 229 s and flapping between Retracted and Announced across nodes, evidence for per-incarnation seqnos diff --git a/quest/m1/cross-relay-bursts.md b/quest/m1/cross-relay-bursts.md new file mode 100644 index 0000000000..25607b1be8 --- /dev/null +++ b/quest/m1/cross-relay-bursts.md @@ -0,0 +1,34 @@ +# [L] Cross-relay delivery under bursts + +## Goal + +Bursty small-group tracks cross two relays without losing groups or stalling. +On cdn.moq.pro, publishers and a subscriber on different nodes saw groups +never arrive (FETCH for them unanswered for 2 s while the publisher stayed +connected), a subscription opened early deliver only a burst's tail, and +return tracks stall up to 31 s with `Stream(Old)`. The same workload on one +node, or one self-hosted relay, loses nothing. + +## Plan + +Reproduce first, on a local two-relay cluster with the reporter's harness +(offered in the issue) and the relay build cdn.moq.pro ran. Then fix what the +repro shows. Suspects: `Old` expiry or newest-first dropping on the relay hop, +and FETCH not forwarded or answered upstream. Re-measure the stalls after +[FIN wait expiry](/quest/m1/fin-wait-expiry.md), a candidate cause. Keep the +repro as a regression test in the cluster tests. + +The issue's stale and flapping announcements after a clean close are +cluster-routing evidence, not this quest's scope. + +## Required + +- [FIN wait expiry](/quest/m1/fin-wait-expiry.md) - removes one candidate cause of the stalls before measuring + +## Closes + +- [#4349](https://github.com/moq-dev/moq/issues/4349) - close this issue when the quest finishes + +## Related + +- [Cluster routing](/quest/m1/cluster-routing.md) - owns the stale and flapping announcements from the same report diff --git a/quest/m1/fin-wait-expiry.md b/quest/m1/fin-wait-expiry.md new file mode 100644 index 0000000000..eb163fa6fe --- /dev/null +++ b/quest/m1/fin-wait-expiry.md @@ -0,0 +1,31 @@ +# [S] A group awaiting its FIN ack still expires + +## Goal + +A group whose FIN is written but not yet acknowledged still expires with +`Old` and still follows priority updates, on lite and IETF. Today the +`Closed` arm only awaits `writer.poll_close`, so a stale group keeps its +queued bytes and its old send order, which can stall newer groups behind it. + +## Plan + +The issue ships four failing tests, a `with_fin_gate` test hook, and a fix: + +- lite: poll priority above the state match, not only in `Serve`. +- lite and IETF: in `Closed`, poll `poll_expired_while_pending` and abort with + `Old`. +- moq-tokio: `set_priority` in `Closing` fires the existing interrupt so the + new order applies, instead of only storing it. + +Each priority update rebuilds the `closed()` watch; check the publisher +benchmarks for a regression. The flaky +`subscription_end_integrity::a_subscription_cut_by_the_publisher_disconnecting_does_not_end_clean` +the reporter hit is tracked in [More tests under load](/quest/m1/test-flakes-2.md). + +## Closes + +- [#4332](https://github.com/moq-dev/moq/issues/4332) - close this issue when the quest finishes + +## Related + +- [Starvation](/quest/m1/qos/starvation.md) - its frontier relies on FIN acks, which can now end in `Old` diff --git a/quest/m1/hop-aligned-import.md b/quest/m1/hop-aligned-import.md new file mode 100644 index 0000000000..6f6f68b6c6 --- /dev/null +++ b/quest/m1/hop-aligned-import.md @@ -0,0 +1,45 @@ +# [L] Same-hop importers publish identical tracks + +## Goal + +Two importers sharing a `--hop` and fed the same encoded stream publish +identical tracks, so a relay can fail over between them. Today two +`moq import ts` fed one forked stream break that contract twice: a standby +refuses every track until it has parsed its PMT, which aborts every +subscriber the moment it connects (#4352), and each numbers its groups from +its own counter, so after a failover `export ts` sees a timestamp rewind and +exits (#4354). + +## Plan + +Decided: same-hop publishers MUST publish the same broadcasts and tracks. +Keep `--hop`, and make every container importer (ts, fmp4, flv, mkv, and the +SRT, RTMP, and HLS gateways that reuse them) meet the contract when fed one +encoded stream. Capture is out: two encoders never align. + +- A group's sequence derives from its keyframe's media timestamp (PTS in TS), + not a per-process counter. Decide how both processes agree across a + timestamp wrap (TS PTS wraps every 26.5 h) when they started on opposite + sides of it. +- Frame timestamps derive from the input alone too: any re-anchor shift must + come from the stream, not process start or wall clock. +- An importer announces only once it knows its tracks, so it never refuses a + track the incumbent serves. +- Docs (`doc/bin/cli.md` "Redundant publishers", the `--hop` doc comment) + narrow the contract to one encoded stream fed to each publisher. + +Tests: per importer, two instances started at different offsets into the same +input produce the same group sequences and timestamps. End to end: the +issues' 1+1 setup (one relay, two same-hop `import ts`, two `export ts`) +survives the standby joining and the incumbent stopping. + +## Closes + +- [#4352](https://github.com/moq-dev/moq/issues/4352) - close this issue when the quest finishes +- [#4354](https://github.com/moq-dev/moq/issues/4354) - close this issue when the quest finishes + +## Related + +- [Redundant ingest](/quest/m2/redundant-ingest.md) - splicing across first hops and two encoders, which this does not attempt +- [TS import shared shift](/quest/m1/ts-import-shared-shift.md) - the TS re-anchor shift that must stay input-derived +- [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - a redundant pair shares one epoch diff --git a/quest/m1/moqsink-keyframe-latch.md b/quest/m1/moqsink-keyframe-latch.md index d86c1e638a..90a2548491 100644 --- a/quest/m1/moqsink-keyframe-latch.md +++ b/quest/m1/moqsink-keyframe-latch.md @@ -1,14 +1,20 @@ -# [XS] moqsink waits for a published keyframe after a break +# [S] moqsink drops frames on recoverable importer errors ## Goal -A video pad in `moqsink` survives a pause or segment break whose first buffer -carries only codec headers. Today `Media::write` in -`rs/moq-gst/src/sink/pad.rs` clears its `keyframe` latch after any successful -decode. A header-only buffer decodes without emitting a frame, clears the -latch, and the next delta frame hits `MissingKeyframe` outside the guarded -arm, which permanently invalidates the pad -([#4239](https://github.com/moq-dev/moq/pull/4239)). +A `moqsink` pad survives the importer errors moq-mux documents as +recoverable: it drops frames until the next keyframe instead of calling +`self.fail()` and losing the rendition for good. Three paths reach that +failure in `Media::write` (`rs/moq-gst/src/sink/pad.rs`) today: + +- After a pause or segment break, a header-only buffer decodes without + emitting a frame, clears the `keyframe` latch, and the next delta hits + `MissingKeyframe` outside the guarded arm + ([#4239](https://github.com/moq-dev/moq/pull/4239)). +- A new pad starts unlatched (`keyframe: false`), so a first buffer that is a + delta frame fails it. +- A `TimestampRewind` with no signalled break (an rtspsrc jitterbuffer + re-anchoring) has no guard at all. ## Plan @@ -17,7 +23,20 @@ until the first buffer parses. That needs `import::Track::decode` to say whether it emitted a frame, or a comparable signal; prefer reusing what the importer already knows over inferring it in the pad. -Regression test beside `video_pause_drops_deltas_until_the_next_keyframe`: a -break followed by a header-only buffer (SPS/PPS alone for H.264), then a -delta, then a keyframe. The delta drops and the keyframe publishes; the pad -stays valid. +Decided: a video pad starts latched. On `TimestampRewind`, set the latch and +drop until a keyframe lands above the producer's live edge, losing up to one +GOP per re-anchor. Mapping a rewound timeline forward instead belongs to +[#3021](/quest/m1/3021-moq-gst-anchor-generated-media-timelines-to-wall-clock.md). +The reporter offered the guard they run in production. + +Regression tests beside `video_pause_drops_deltas_until_the_next_keyframe`: + +- A break, then a header-only buffer (SPS/PPS alone for H.264), then a delta, + then a keyframe: the delta drops, the keyframe publishes, and the pad stays + valid. +- A new pad's first buffer is a delta: it drops. +- A rewind without a break: deltas drop until a keyframe above the edge. + +## Closes + +- [#4366](https://github.com/moq-dev/moq/issues/4366) - close this issue when the quest finishes diff --git a/quest/m1/splice-fetch-remainder.md b/quest/m1/splice-fetch-remainder.md new file mode 100644 index 0000000000..519e130f9a --- /dev/null +++ b/quest/m1/splice-fetch-remainder.md @@ -0,0 +1,41 @@ +# [S] A spliced group fetches its remainder + +## Goal + +An origin-routed reader never parks forever on a half-delivered group. When a +publisher aborts a track mid-group and re-creates it under the same name, the +reader today waits in `read_frame` with no error while a direct reader gets +the abort. After the fix the half-delivered group either completes or ends +with an error, and the reader continues on the new track from its next group. + +## Plan + +Decided: a same-name re-create is a continuation, not new content, for local +and remote sources alike. No special case for local broadcasts. + +Why it hangs: `track_ended` in `rs/moq-net/src/model/front.rs` treats an abort +after delivery as failover and re-splices from the serving source at the first +missing frame. The resumed group then waits in `poll_peek_group` +(`rs/moq-net/src/model/resume.rs`) on the new copy. A copy only answers +"never" for a group below its declared start, and an in-process producer +declares none, so "not yet" lasts forever. The expiry guard is attached only +once a copy is found. + +Decided: once the new copy is past the missing group, the resumed group FETCHes +the remainder (`frame_start` at the missing frame) instead of peeking. +`fetch_group` always answers: a local producer with no fetch handler says +`NotFound` at once, so the group ends with that error, and a remote copy can +serve the rest from the upstream cache. Keep peeking while the live route may +still deliver the group, since that is what avoids a redundant fetch. + +Regression test: the issue's repro (abort mid-group 2, re-create at group 3, +through the origin) on a paused clock; group 2 ends with an error and group 3 +arrives. + +## Closes + +- [#4365](https://github.com/moq-dev/moq/issues/4365) - close this issue when the quest finishes + +## Related + +- [Splice edge cases](/quest/m1/splice-edges.md) - the same splice code and test harness diff --git a/quest/m1/test-flakes-2.md b/quest/m1/test-flakes-2.md index c1742598ca..8cc9502a74 100644 --- a/quest/m1/test-flakes-2.md +++ b/quest/m1/test-flakes-2.md @@ -22,6 +22,10 @@ retry. [#4084](https://github.com/moq-dev/moq/pull/4084) removed from its sibling after [#4055](https://github.com/moq-dev/moq/pull/4055) papered over it with a retry. +- moq-tokio + `subscription_end_integrity::a_subscription_cut_by_the_publisher_disconnecting_does_not_end_clean` + ends `Ok(None)` with 10 of 20 frames in 3 of 8 full-suite runs on a clean + tree, and passes alone (#4332). - `just test media` late join failed once after [#4181](https://github.com/moq-dev/moq/pull/4181): "joined at frame 111, 16 frames behind 127", against a budget of one GOP (15). diff --git a/quest/m1/tooling/justfiles.md b/quest/m1/tooling/justfiles.md index 22597e1461..429dedb432 100644 --- a/quest/m1/tooling/justfiles.md +++ b/quest/m1/tooling/justfiles.md @@ -86,8 +86,17 @@ wanted), `_shell`, `_flake` as its one-liner, `clean`, the rs `package` and `fuzz` bodies, and the OBS `compile`, `_includes`, `_unit`, `test`, `check`, and `preset` bodies. +Arguments reach tools intact: `just rs test`, `check-test`, and +`capture-test` interpolate `{{ args }}` today, so a quoted nextest filterset +becomes a shell syntax error (#4342). Recipes that forward arguments use +`[positional-arguments]` and `"$@"`. No self-test for it, per the rule above. + Docs: `doc/setup/dev.md`, `CONTRIBUTING.md`, `test/README.md`, and the `AGENTS.md` mentions of `just wasm` follow the survivors. Verify with `just check`, `just ci test`, and `just check --all`, and confirm every recipe name check.yml, cache.yml, nightly.yml, interop.yml, wasm.yml, obs.yml, swift.yml, and release-*.yml invoke still resolves. + +## Closes + +- [#4342](https://github.com/moq-dev/moq/issues/4342) - close this issue when the quest finishes diff --git a/quest/m1/ts-programs.md b/quest/m1/ts-programs.md new file mode 100644 index 0000000000..8097d658c0 --- /dev/null +++ b/quest/m1/ts-programs.md @@ -0,0 +1,33 @@ +# [L] moq import ts: select programs + +## Goal + +A multi-program transport stream is never silently merged. Today `import ts` +keeps the first program's identity, adds every PMT's streams to one broadcast +on one clock, and loses content; `export ts` rebuilds one program. After +this quest `import ts` refuses a multi-program input by default, naming the +programs, and `--program` chooses what to import. + +## Plan + +Decided: + +- Default: error before publishing when a PAT (initial or later) lists more + than one non-zero program, naming each program and pointing at `--program`. +- `--program ` imports program `n` only. +- `--program all` publishes one broadcast per program, named + `/`, each with its own clock and catalog. +- `export ts` stays one program per broadcast. + +Tests: a synthetic two-program input with far-apart clocks fails naming both +programs; `--program 2` publishes only program 2's streams on program 2's +clock; `--program all` publishes both broadcasts, each timed correctly. Update +`doc/bin/cli.md` and the `--help` text. + +## Closes + +- [#4353](https://github.com/moq-dev/moq/issues/4353) - close this issue when the quest finishes + +## Related + +- [TS import shared shift](/quest/m1/ts-import-shared-shift.md) - its per-program shift assumes the single program this settles diff --git a/quest/m1/ts-publish-muxdelay.md b/quest/m1/ts-publish-muxdelay.md new file mode 100644 index 0000000000..688904da4d --- /dev/null +++ b/quest/m1/ts-publish-muxdelay.md @@ -0,0 +1,27 @@ +# [XS] Documented ffmpeg line flushes audio + +## Goal + +The documented `ffmpeg ... -f mpegts -pes_payload_size 0 -` publish line stops +holding small audio frames in ffmpeg's muxer for up to 0.35 s. Those clumps +raise the importer's catalog `jitter` for the life of the track, and watch +adds about 230 ms of playout delay to match. + +## Plan + +Decided: add `-muxdelay 0` everywhere the line appears +(`demo/pub/justfile`, `doc/bin/cli.md`, `doc/concept/standard.md`, +`rs/moq-cli/README.md`). `import ts` needs no PCR lead. The reporter's patch +is ready. Verify by re-measuring the catalog jitter on a quiet-audio source; +there is no automated test for a docs line. + +The importer's jitter estimate keeping its maximum forever is a separate +problem; the player side is covered by the audio jitter target. + +## Closes + +- [#4347](https://github.com/moq-dev/moq/issues/4347) - close this issue when the quest finishes + +## Related + +- [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - a decaying playout estimate softens a transient burst diff --git a/quest/m1/watch-decoder-recovery.md b/quest/m1/watch-decoder-recovery.md new file mode 100644 index 0000000000..f4e208676d --- /dev/null +++ b/quest/m1/watch-decoder-recovery.md @@ -0,0 +1,28 @@ +# [M] Watch decoders recover from an error + +## Goal + +One malformed packet no longer ends audio or video for the rest of a +subscription. Today a WebCodecs error closes the decoder, both audio loops +`break` (added in #2415 to stop an `InvalidStateError` loop), and video calls +`effect.close()`; nothing rebuilds the decoder, and playback silently stops. + +## Plan + +Decided: rebuild inside the loop, for audio and video alike. On a decoder +error, rebuild through one shared `build()` helper, skip to the next keyframe, +and give up after three rebuilds in a row with no output. A rebuilt legacy +audio decoder re-runs its warmup priming. Cover the legacy and CMAF loops. + +Tests: a corrupt packet mid-stream is followed by decoded output from the next +keyframe; a stream of only corrupt packets gives up after three rebuilds +instead of spinning. + +## Closes + +- [#4324](https://github.com/moq-dev/moq/issues/4324) - close this issue when the quest finishes + +## Related + +- [Audio warmup](/quest/m1/audio-warmup.md) - the same audio loops and priming +- [Watch video guards](/quest/m1/watch-video-guards.md) - keeps bad input from reaching the video codec diff --git a/quest/m1/watch-video-guards.md b/quest/m1/watch-video-guards.md new file mode 100644 index 0000000000..765b652fc7 --- /dev/null +++ b/quest/m1/watch-video-guards.md @@ -0,0 +1,37 @@ +# [S] Watch video holds its picture and its order + +## Goal + +Two `js/watch/src/video/decoder.ts` defects stop reaching the viewer: + +- Promoting a video track when none is active (after pause, scrolling back + into view, or a hidden rendition returning) holds the last picture instead + of painting black for a round trip plus a keyframe. +- An older finished group accepted behind the live group is never fed to the + codec between live deltas. + +## Plan + +Decided: + +- `#runActive` ignores the new track's initial `undefined` frame; clearing + stays with `#clearCurrentFrame` and `close()`. The held timestamp stays + with the held frame, so the outputs describe what is on screen. +- The guard lives in both video decode loops (legacy and CMAF): skip a frame + whose group is older than the newest group already fed, and reset on a + discontinuity. The consumer keeps serving older groups, since audio's + timestamp-indexed ring needs them. Fix the consumer comment claiming video + drops a late frame at render; that only holds after decode. + +Tests: a promote from nothing keeps the previous frame and timestamp; an older +group arriving after a newer one never reaches the decoder. + +## Closes + +- [#4338](https://github.com/moq-dev/moq/issues/4338) - close this issue when the quest finishes +- [#4339](https://github.com/moq-dev/moq/issues/4339) - close this issue when the quest finishes + +## Related + +- [#3056](/quest/m1/3056-watch-video-decoder-captures-the-rewind-generation-at.md) - another fix in the same decoder +- [Watch decoder recovery](/quest/m1/watch-decoder-recovery.md) - a bad feed today ends video for the subscription diff --git a/quest/m1/watch-worklet-file.md b/quest/m1/watch-worklet-file.md new file mode 100644 index 0000000000..b6a778364a --- /dev/null +++ b/quest/m1/watch-worklet-file.md @@ -0,0 +1,29 @@ +# [S] Watch audio under a strict CSP + +## Goal + +A page with CSP `script-src 'self'` plays watch audio. Today +`js/common/vite-plugin-worklet.ts` always exports the render worklet as a +`blob:` URL, so `addModule` is refused and audio never starts, with only a +console "spawn error". + +## Plan + +Decided: in `vite build`, emit the worklet as its own file and export +`new URL(".js", import.meta.url)`; keep the blob URL only in +`vite serve`. Vite, webpack 5, and esbuild (with a plugin) resolve that +pattern inside a dependency. This applies to the publish capture worklet too +and deletes the runtime blob code. Document the consumer-bundler requirement. +The publish capture worker (`?worker&inline`) is out of scope. + +Verify in a browser under `script-src 'self'` (Chromium and Firefox) and +from a Vite consumer app. + +## Closes + +- [#4323](https://github.com/moq-dev/moq/issues/4323) - close this issue when the quest finishes + +## Related + +- [JS bundle trims](/quest/m1/js-bundle-trims.md) - minifies worklets through the same plugin +- [Plan: watch worker](/quest/m1/plan-watch-worker.md) - notes the capture worker's `worker-src blob:` need diff --git a/quest/m2/redundant-ingest.md b/quest/m2/redundant-ingest.md index 6713d87dd3..25bac0ae8b 100644 --- a/quest/m2/redundant-ingest.md +++ b/quest/m2/redundant-ingest.md @@ -20,4 +20,5 @@ decision, with a quest for the chosen mechanism. ## Related +- [Same-hop importers](/quest/m1/hop-aligned-import.md) - identical tracks from one encoded stream under one first hop, the case this generalizes - [Broadcast epochs](/quest/m1/broadcast-epoch/README.md) - explicit epochs are what a redundant pair would share From 6cab8d6e34ac1ad95bd8c5607578c9ff311f0db7 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 28 Sep 2026 16:14:42 -0700 Subject: [PATCH 2/2] quest: resolve the m0 conflicts the merge left staged The release shipped (#4393 removed its quest), so the systemd unit fix stands alone in m0. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 4 -- quest/m0/relay-systemd-unit.md | 3 +- quest/m0/release.md | 90 ---------------------------------- 3 files changed, 1 insertion(+), 96 deletions(-) delete mode 100644 quest/m0/release.md diff --git a/quest/m0/README.md b/quest/m0/README.md index 7f2b3e6239..935edeae68 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -30,11 +30,7 @@ Published API or wire breaks still land on dev; each quest's Plan says so. ## Required -<<<<<<< HEAD -- [Release](/quest/m0/release.md) - the release moq.pro adopts: binding docs, an upgrade page, and a staging soak gate it rather than the merge - [Packaged relay starts](/quest/m0/relay-systemd-unit.md) - the `.deb` and `.rpm` relay service starts instead of crash-looping on `--file` -======= ->>>>>>> origin/main - [Skip unchanged announce updates](/quest/m0/announce-update-dedupe.md) - a publisher sends an announce update only when the wire route changed - [Wildcard](/quest/m0/wildcard/README.md) - a relay resolves subscriptions against advertised prefixes, a service claims the prefix it could serve and refuses the rest instead of enumerating broadcasts, and the browser player treats a covering claim as availability - [Audio quality harness](/quest/m0/audio-quality-harness/README.md) - a browser playout latency regression fails a nightly run instead of arriving as a bug report, and its recorder supplies the jitter target's replay traces diff --git a/quest/m0/relay-systemd-unit.md b/quest/m0/relay-systemd-unit.md index 857bbfc7bc..37a7e51a19 100644 --- a/quest/m0/relay-systemd-unit.md +++ b/quest/m0/relay-systemd-unit.md @@ -11,8 +11,7 @@ rejects unknown flags, so every packaged install crash-loops. `ExecStart=/usr/bin/moq-relay /etc/moq-relay/relay.toml`. Add the reporter's unit test in the relay crate that parses the unit's `ExecStart` with the -relay's own `Cli`, so the unit and the parser can't drift again. This lands -before the release, since the release ships the packages. +relay's own `Cli`, so the unit and the parser can't drift again. ## Closes diff --git a/quest/m0/release.md b/quest/m0/release.md deleted file mode 100644 index 8e3bdb2a64..0000000000 --- a/quest/m0/release.md +++ /dev/null @@ -1,90 +0,0 @@ -# [M] Cut the release moq.pro adopts - -## Goal - -The first release from the merged tree is the one moq.pro pins: every -binding's docs match the surface it exposes, an upgrade page walks a -consumer from the last main release to this one, and the merged relay has -run on staging long enough that the origin and HLS rewrites are trusted. -The binding restructure in [FFI shape](/quest/m1/ffi-shape/README.md) -follows this release rather than riding it. - -## Plan - -Write `doc/setup/upgrade.md`, one section per package group, each break -with its PR and the replacement call: - -- net: announcements are prefix routes (#3225); serving folds into - `origin::Producer::dynamic` and broadcasts announce themselves (#3400, - #3581); `Reload`/`Shared` collapse into one `Connection` (#3614, #3636); - `writeDatagram` is `insertDatagram` (#3666); reader group and frame limits - are explicit (#3647); groups expire on timestamps alone; an oversized group - aborts with GROUP_TOO_LARGE instead of shedding its head (#3585); the - `Latency` type became `max_age` and delivery order became the `Ordered` - handle (#2688, #2955); the four moq-lite stream codes sent from the - reserved range moved to 0x36-0x39 in the draft's own range; subscriptions resume - across routes sharing a first hop (#3312); the send estimate is split among - JS publishers (#3616); @moq/net and @moq/pattern mirror Rust (`consume`, `Time.Milli`, - one `readFrame()`, `InvalidPattern`); the announce and request names; and - moq-tokio's names sit under their modules (`connection::Goaway`, `cli::Duration`, - `transport::Session`, `watch::Files`, `resolve()`; #3745). -- hang and json: the catalog `timeline` is `archive` (#3612); `json` and - `binary` catalog sections (#3109) take one options object (#3640); Rust - `modify()` is fallible and a failed dropped edit aborts the track (#3644), - paired with JS `update()`/`mutate()` and the Rust `mutate()`; an fMP4 export - fragment is a group (#3573). -- watch and play: `latency` splits into `delay` and `buffer` and - `--latency-max` is renamed (#3396); `moq play` has a real playout clock with - `--delay` (#3528); Firefox hardware encoding and screen-source scaling - (#3535). -- relay and CLI: embedders own listeners and workers (#3638); the cluster - origin is constructed once (#3582); LAN discovery is partitioned by - application (#3621) and meshes CLI and relay peers (#3648); config merges - with provenance (#3587); auth is one contract, a `Request` in and a - `Grant` with a lease out, and `--auth-api-mode` is gone (#3688); the CLI parses - with usage-rs and refuses the flags it dropped (#3030); moq-native is - moq-tokio (#2896). Released spellings refuse rather than warn or silently - alias: `--cluster-linger` is gone; `--cluster-connect` needs a full URL; - TOML `connect`/`failover_delay`/`listen`/`disable_verify`/`[server]`/`[client]` - name `url`/`race`/`bind`/`insecure`/`[listen]`/`[connect]`; CLI `--origin`/ - `--name`/`--latency-max` and `publish`/`subscribe` name `--hop`/`--broadcast`/ - `--max-age` and `import`/`export`. Unused `#[deprecated]` items are gone. - JS `announced()` always drops reflected announces (`ignoreSelf` is gone); - an `oct` JWK without `kty` is refused. The gstmoq properties - `estimated-send-bitrate`/`estimated-recv-bitrate` are `estimated-*-rate` - with no alias, a runtime failure for a `gst-launch` line. -- bindings: the Go module is `moq.dev/moq` with `context.Context` on every - blocking call (#2957); `MoqAudioCodec` is an `opus()` object (#3671); the - configuration setters are fallible (#3642); durations are microseconds and - the rate estimates are `estimated_*` (#3744); `MoqVideoDecoderOutput.format` - picks I420 or RGBA decode output, additively. - -Release-notes outline, the additions worth leading with, in order of value to -a consumer: prefix routes and wildcard Pattern events (#3225, #3649); -self-announcing broadcasts and `dynamic` handles (#3400, #3581); the archive -catalog and store (#3612); the delay/buffer split and playout clock (#3396, -#3528); GROUP_TOO_LARGE (#3585); explicit reader limits and timestamp-only -expiry (#3647); publish robustness (Firefox hardware encoding, file demux, -`stalled` on lagging renditions #3630, stream resets at boundaries #3580); -relay embedding and the LAN mesh (#3638, #3648, #3621, #3587); one auth -contract with leases (#3688, #3739); data tracks and captions (#3109, #3640); one `Connection` with URL -replacement (#3614, #3636); first-hop resume and the shared send estimate -(#3312, #3616). The m0 release API quests settle archive, sock, and uring -before release without making the independent Pronto or media tracks a release -prerequisite. `moq-e2ee` ships the `moq-e2ee-00` epoch profile; the -[E2EE](/quest/m1/e2ee/README.md) questline owns its twin and interop. - -The soak bullet below is cleared by hand: the merged relay serves moq.pro -staging with `/metrics` watched and a fresh viewer joining a days-old -`moq import ts` broadcast over HLS at the end; the bounded `moq_json::window` -timeline (#3240) is what makes that hold, and only a long run proves it. dev -landed on main as #3793; before cutting, run `just check --all` and -`just test interop --all` on the release revision and record it. Then cut the release under the existing release-plz and npm workflows; this -quest bumps no versions itself. - -Public API: none beyond the required quests. Wire: none. - -## Required - -- [Packaged relay starts](/quest/m0/relay-systemd-unit.md) - the shipped systemd unit passes the config path the relay accepts -- The merged relay has soaked on moq.pro staging and the maintainer has signed it off