From e3caf5c26a5d6081f326dcfd0bf080259ff51955 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 15:41:14 -0700 Subject: [PATCH 1/5] quest: datagrams are live-only; drop hls-linger; demo serve-hls renditions Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/datagram-unfetchable.md | 44 ++++++++++++++++++++++++++++++++ quest/m1/hls-linger.md | 41 ----------------------------- quest/m2/README.md | 2 ++ quest/m2/serve-hls-renditions.md | 16 ++++++++++++ 5 files changed, 63 insertions(+), 42 deletions(-) create mode 100644 quest/m1/datagram-unfetchable.md delete mode 100644 quest/m1/hls-linger.md create mode 100644 quest/m2/serve-hls-renditions.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 4b9ac7a8f1..5a68f4c02d 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -56,7 +56,6 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Video surface](/quest/m1/video-surface.md) - on dev, moq-ffi's `native` becomes `surface`, refused on platforms with no surface - [HLS discontinuity sequence](/quest/m1/hls-discontinuity-sequence.md) - on dev, `Segment::discontinuity` is the absolute sequence, so every cursor agrees - [Auth client CA](/quest/m1/relay-auth-client-ca.md) - on dev, `auth::Config::validate` and `init` take the client-CA flag, so no caller can skip the check -- [HLS linger](/quest/m1/hls-linger.md) - `moq_hls::Server` serves an ended broadcast for its playlist window plus grace, so the moq.pro edge drops its own pool - [Remove live()](/quest/m1/remove-live.md) - on dev, importers publish stream timestamps verbatim, the catalog clock maps them to wall time, and an encoder restart becomes a new epoch - [Go and Dart doc samples](/quest/m1/doc-samples-go-dart.md) - Go and Dart doc samples compile against their wrappers - [Data capture in bindings](/quest/m1/data-capture-bindings.md) - moq-ffi and every wrapper pass a data frame's capture time, and the JSON window producer takes one @@ -64,6 +63,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [JS IETF reprice](/quest/m1/js-ietf-reprice.md) - `@moq/net`'s IETF publisher reprices a held namespace in place, like Rust, instead of withdraw-then-advertise - [Cluster publisher in place](/quest/m1/cluster-publisher-in-place.md) - under the IETF cluster extension a publisher change updates an advertisement in place, like lite, and never splices across publishers - [JS IETF datagrams](/quest/m1/js-ietf-datagram.md) - `@moq/net` sends and receives datagram groups over moq-transport, like Rust +- [Datagrams are live-only](/quest/m1/datagram-unfetchable.md) - no FETCH, replay to a new subscriber, or cache fill ever returns a datagram group - [#2991](/quest/m1/2991-net-coalesce-dynamic-tracks-and-preserve-sequences-across.md) - one dynamic producer per track name in both languages, with the sequence namespace surviving a replacement - [JavaScript FETCH](/quest/m1/js-fetch.md) - generic on-demand group serving and IETF FETCH for browser publishers - [JS fetch cancel](/quest/m1/js-fetch-cancel.md) - `FetchGroupOptions.signal` abandons one pending fetch without closing the track diff --git a/quest/m1/datagram-unfetchable.md b/quest/m1/datagram-unfetchable.md new file mode 100644 index 0000000000..febf484b02 --- /dev/null +++ b/quest/m1/datagram-unfetchable.md @@ -0,0 +1,44 @@ +# [S] Datagrams are live-only + +## Goal + +A datagram group is delivered only to subscribers present when it is sent. +FETCH never returns one, a new subscription never replays buffered datagrams, +and a relay fills no cache miss with one. Anything that needs reliable or +late-joiner delivery should use a stream group instead. + +## Plan + +Decided in planning (09-29), after the moxygen fetch-fill agent found that the +relay's group-fetch decoder refuses a draft-16+ fetch object that carries the +datagram flag, and reports it as "unsupported": + +- Keep the existing 64-datagram send buffer per track + (`rs/moq-net/src/model/track.rs`, `MAX_DATAGRAMS`). An age bound was + considered and dropped. +- A new subscription starts at the next datagram sent, not at the buffered + backlog. This covers Rust and `@moq/net`, lite and IETF. +- FETCH never serves a datagram group. A publisher answers a FETCH for one the + way it answers any group it won't deliver (dropped or does not exist), + never with its payload. +- A received fetch object with the datagram flag is refused as not fetchable, + a protocol error rather than `Unsupported`. That covers the relay's group + fill (`recv_group_fetch_objects`, on the moxygen line) and the joining-fetch + fill (`run_fill_objects`). +- Update `drafts/draft-lcurley-moq-lite.md` to say datagrams are neither + cached nor fetchable, and `doc/concept/`. Run `just drafts check` and + `just test interop --all`. + +Not planned: reliable or cached datagrams. If they are ever wanted, the way to +get them is back-pressure in the QUIC library and treating each datagram like +a one-shot stream, not a cache. + +Tests: a subscriber joining after datagrams were sent receives only later +ones. A FETCH covering a datagram group gets no payload. A fetch stream +carrying a datagram-flagged object is refused. + +## Related + +- [Moxygen compatibility](/quest/m1/moxygen/README.md) - says it carries one datagram per group live; this keeps them out of FETCH +- [JS IETF datagrams](/quest/m1/js-ietf-datagram.md) - JS datagram send and receive over moq-transport +- [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - datagram groups stay best effort there too diff --git a/quest/m1/hls-linger.md b/quest/m1/hls-linger.md deleted file mode 100644 index 9da2c54d1c..0000000000 --- a/quest/m1/hls-linger.md +++ /dev/null @@ -1,41 +0,0 @@ -# [S] moq-hls keeps an ended broadcast for its playlist window - -## Goal - -A player polling `moq_hls::Server` can finish the last segments of a broadcast -that just ended, and a republish of the same name takes over cleanly. Today -`Server` evicts a broadcaster as soon as its broadcast closes, so the playlist -404s mid-window. The moq.pro edge works around it with its own pool on top of -`Broadcaster` that holds an ended broadcast for the playlist window plus a -grace period and checks for a republish -([#3964](https://github.com/moq-dev/moq/pull/3964)). Once moq-hls owns this, -the edge deletes its pool. - -## Plan - -Decided: moq-hls owns the retention through an `export::Config` field (the -playlist window plus a grace period), so every embedder gets it and the edge -has nothing to duplicate. `Config` is `#[non_exhaustive]`, so the field is -additive on `main`. - -Guidance: - -- The eviction lives in `server/mod.rs` (`evict_closed` and the - `is_closed` checks in `Server::broadcaster`). An ended broadcaster keeps - serving its final playlist, marked ended if the renditions know it, until - the linger expires. -- A republish during the linger replaces the ended broadcaster. Decide - whether a request for the name resolves the new broadcast first and falls - back to the ended one only when nothing is announced; match what the edge - pool does today. -- Segments are fetched from the relay cache on request, so the linger is only - useful within the cache's retention; say so on the field, as `window` does. -- A zero linger keeps today's behavior. Pick the default with the edge's - current value in mind. -- Tests: a request inside the linger after close still gets the playlist and - a cached segment; after it expires the name 404s; a republish inside the - linger is served. - -## Related - -- [Export linger](/quest/m1/export-linger.md) - the CLI exporter's wait for a broadcast to return; same idea, separate code diff --git a/quest/m2/README.md b/quest/m2/README.md index d1a1a141c7..32dd0af66a 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -93,3 +93,5 @@ upstream release waits in [m4](/quest/m4/README.md). - [Audio capture time](/quest/m2/audio-capture-time.md) - native audio stamps a buffer's capture instant, not when the driver reads it - [X11 capture transport](/quest/m2/x11-capture-shm.md) - move X11 capture to shared memory and RandR events instead of a per-frame socket copy - [Capture frame buffers](/quest/m2/capture-frame-buffers.md) - stop rebuilding a full-frame buffer every tick in the X11 and Windows backends + +- [Demo serve-hls renditions](/quest/m2/serve-hls-renditions.md) - `just pub serve-hls` serves 720p and 144p instead of two 256-wide copies \ No newline at end of file diff --git a/quest/m2/serve-hls-renditions.md b/quest/m2/serve-hls-renditions.md new file mode 100644 index 0000000000..9953f140a9 --- /dev/null +++ b/quest/m2/serve-hls-renditions.md @@ -0,0 +1,16 @@ +# [XS] Demo serve-hls publishes distinct renditions + +## Goal + +`just pub serve-hls` serves two renditions at different resolutions, as +`just pub hls` does. Today both come out 256 wide: the `-vf:0` and `-vf:1` +options aren't per-stream in ffmpeg, so the last filter wins. + +## Plan + +Use per-stream filters (`-filter:v:0`, `-filter:v:1`), scaled to 720p and +144p to match `hls`. Scaling the 720p source up to 1080p couldn't encode in +real time on a dev machine. The recipe body moves to `sh/demo/serve-hls.sh` +when the [tooling line](/quest/m1/tooling/README.md) lands; fix it wherever it +lives. Check it by fetching both playlists and comparing each rendition's +`RESOLUTION`. From 7db2ab9fdf040f411654d433aeef5a1e4f45360b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 15:42:40 -0700 Subject: [PATCH 2/5] quest: capture is a default feature Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/capture-default.md | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 quest/m1/capture-default.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 5a68f4c02d..ee5b48eb00 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -53,6 +53,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [TS PSI reassembly](/quest/m1/ts-psi-reassembly.md) - `import ts` reads a PAT or PMT that spans packets or follows a nonzero pointer_field instead of aborting, and one corrupted section costs a repetition and a counted `CRC_error`, not the import - [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 - [Capture control](/quest/m1/capture-control.md) - on dev, `encode::Capture` replaces `CaptureOptions` without a `clock` field (it reads the catalog's), an unsupported `cut()` errors, and dropping the last `Control` cancels in-flight opens +- [Capture by default](/quest/m1/capture-default.md) - moq-video and moq-audio build `capture` by default, so pre-merge checks test it and the capture gate goes away - [Video surface](/quest/m1/video-surface.md) - on dev, moq-ffi's `native` becomes `surface`, refused on platforms with no surface - [HLS discontinuity sequence](/quest/m1/hls-discontinuity-sequence.md) - on dev, `Segment::discontinuity` is the absolute sequence, so every cursor agrees - [Auth client CA](/quest/m1/relay-auth-client-ca.md) - on dev, `auth::Config::validate` and `init` take the client-CA flag, so no caller can skip the check diff --git a/quest/m1/capture-default.md b/quest/m1/capture-default.md new file mode 100644 index 0000000000..8e2264afc8 --- /dev/null +++ b/quest/m1/capture-default.md @@ -0,0 +1,38 @@ +# [S] Capture is a default feature + +## Goal + +`capture` is on by default in moq-video and moq-audio, so the pre-merge +`just check` compiles and tests device capture like any other default code, +and the separate capture CI gate goes away. The flag stays so a minimal build +can opt out. Workspace consumers that set `default-features = false` (the +relay, the language bindings) keep building without it. + +## Plan + +Decided in planning (09-29), after capture-control (#4545) found that +pre-merge `just check` never runs moq-video's `capture` tests: + +- **Default-on, keep the flag.** Removing the flag was rejected: consumers + that don't want device stacks would pay for them. +- moq-video's `capture` already costs nothing to build on any platform. Its + Cargo comment says it stays off only "until every distribution ships it", a + packaging concern this reverses. +- moq-audio's `capture` links libasound when it builds on Linux, which is why + it depends on [ALSA link](/quest/m1/capture-alsa-link.md). Once libasound + loads at runtime, the default costs no system library. +- Delete the special capture gate (`rs capture`, `capture-test`, and the + capture branch in `sh/rs/select.sh` on the tooling line) once default + `just check` covers it. Keep the platform jobs (`just rs macos`, + `just rs windows`). +- Update the Cargo feature comments, moq-cli's `capture` feature (it may + become redundant), and `doc/` wherever capture is described as opt-in. + +Verify: `just check` on a moq-video or moq-audio change runs the capture +tests, and a `default-features = false` consumer (for example +`cargo tree -p moq-ffi -e features`) pulls no capture dependency. + +## Required + +- [Tooling](/quest/m1/tooling/README.md) - rewrites the capture gate this deletes +- [ALSA link](/quest/m1/capture-alsa-link.md) - moq-audio capture can't be on by default while it links libasound at build time From 87c27c7564215efc51744203b32281674f66b78f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 15:54:40 -0700 Subject: [PATCH 3/5] quest: check serve-hls resolutions in the master playlist Co-Authored-By: Claude Opus 5.5 --- quest/m2/README.md | 3 +-- quest/m2/serve-hls-renditions.md | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/quest/m2/README.md b/quest/m2/README.md index 32dd0af66a..95f0845640 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -93,5 +93,4 @@ upstream release waits in [m4](/quest/m4/README.md). - [Audio capture time](/quest/m2/audio-capture-time.md) - native audio stamps a buffer's capture instant, not when the driver reads it - [X11 capture transport](/quest/m2/x11-capture-shm.md) - move X11 capture to shared memory and RandR events instead of a per-frame socket copy - [Capture frame buffers](/quest/m2/capture-frame-buffers.md) - stop rebuilding a full-frame buffer every tick in the X11 and Windows backends - -- [Demo serve-hls renditions](/quest/m2/serve-hls-renditions.md) - `just pub serve-hls` serves 720p and 144p instead of two 256-wide copies \ No newline at end of file +- [Demo serve-hls renditions](/quest/m2/serve-hls-renditions.md) - `just pub serve-hls` serves 720p and 144p instead of two 256-wide copies diff --git a/quest/m2/serve-hls-renditions.md b/quest/m2/serve-hls-renditions.md index 9953f140a9..3e86c953bc 100644 --- a/quest/m2/serve-hls-renditions.md +++ b/quest/m2/serve-hls-renditions.md @@ -12,5 +12,5 @@ Use per-stream filters (`-filter:v:0`, `-filter:v:1`), scaled to 720p and 144p to match `hls`. Scaling the 720p source up to 1080p couldn't encode in real time on a dev machine. The recipe body moves to `sh/demo/serve-hls.sh` when the [tooling line](/quest/m1/tooling/README.md) lands; fix it wherever it -lives. Check it by fetching both playlists and comparing each rendition's -`RESOLUTION`. +lives. Check it by fetching `master.m3u8` and comparing the two renditions' +`RESOLUTION` attributes. From e86f00a3f19c5d9a41e3863eb8b53564c8d7467b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 16:25:10 -0700 Subject: [PATCH 4/5] quest: data tracks stamp on the catalog's current clock Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/data-track-clock.md | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 quest/m1/data-track-clock.md diff --git a/quest/m1/README.md b/quest/m1/README.md index ee5b48eb00..9c7b0a0b3a 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -58,6 +58,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [HLS discontinuity sequence](/quest/m1/hls-discontinuity-sequence.md) - on dev, `Segment::discontinuity` is the absolute sequence, so every cursor agrees - [Auth client CA](/quest/m1/relay-auth-client-ca.md) - on dev, `auth::Config::validate` and `init` take the client-CA flag, so no caller can skip the check - [Remove live()](/quest/m1/remove-live.md) - on dev, importers publish stream timestamps verbatim, the catalog clock maps them to wall time, and an encoder restart becomes a new epoch +- [Data track clock](/quest/m1/data-track-clock.md) - JSON and binary data tracks stamp on the catalog's clock at write time, matching the media's anchored clock - [Go and Dart doc samples](/quest/m1/doc-samples-go-dart.md) - Go and Dart doc samples compile against their wrappers - [Data capture in bindings](/quest/m1/data-capture-bindings.md) - moq-ffi and every wrapper pass a data frame's capture time, and the JSON window producer takes one - [Moxygen compatibility](/quest/m1/moxygen/README.md) - one subgroup per group, whole-group FETCH, and one datagram per group, never a full moxygen pass diff --git a/quest/m1/data-track-clock.md b/quest/m1/data-track-clock.md new file mode 100644 index 0000000000..170d34a09a --- /dev/null +++ b/quest/m1/data-track-clock.md @@ -0,0 +1,19 @@ +# [XS] Data tracks stamp on the catalog's current clock + +## Goal + +A JSON or binary data track publishes timestamps on the same clock mapping as +the media next to it, however early it was created. Today a data track copies +the catalog clock when it's created. An importer sets that clock from its +first frame (remove-live, #4543), so a data track created before that frame +maps its timestamps to a different clock than the media. + +## Plan + +Have data tracks read the catalog's clock when they stamp, not a copy made +when they were created. Keep it crate-private if possible. Test: a data track +created before an importer's first frame stamps on the anchored clock. + +## Required + +- [Remove live()](/quest/m1/remove-live.md) - introduces the first-frame clock anchor From 5b433390556378ee45f167aa97150249e65bb090 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 19:24:27 -0700 Subject: [PATCH 5/5] quest: a datagram-flagged fetch object fails only its fetch Co-Authored-By: Claude Opus 5.5 --- quest/m1/datagram-unfetchable.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quest/m1/datagram-unfetchable.md b/quest/m1/datagram-unfetchable.md index febf484b02..264d256331 100644 --- a/quest/m1/datagram-unfetchable.md +++ b/quest/m1/datagram-unfetchable.md @@ -21,8 +21,9 @@ datagram flag, and reports it as "unsupported": - FETCH never serves a datagram group. A publisher answers a FETCH for one the way it answers any group it won't deliver (dropped or does not exist), never with its payload. -- A received fetch object with the datagram flag is refused as not fetchable, - a protocol error rather than `Unsupported`. That covers the relay's group +- A received fetch object with the datagram flag is refused as not fetchable. + It fails only that fetch or fill, not the session, because draft-16+ allows + the flag (maintainer, 09-29, on Codex's review). That covers the relay's group fill (`recv_group_fetch_objects`, on the moxygen line) and the joining-fetch fill (`run_fill_objects`). - Update `drafts/draft-lcurley-moq-lite.md` to say datagrams are neither @@ -35,7 +36,7 @@ a one-shot stream, not a cache. Tests: a subscriber joining after datagrams were sent receives only later ones. A FETCH covering a datagram group gets no payload. A fetch stream -carrying a datagram-flagged object is refused. +carrying a datagram-flagged object fails that fetch and leaves the session up. ## Related