From 81b7af4a65f56aca91cddc7ca55de91f66bbf270 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 16:06:34 -0800 Subject: [PATCH 1/3] quest: plan the Finished handle and the catalog finish fix Co-Authored-By: Claude Opus 5.5 --- quest/m1/explicit-state/README.md | 1 + quest/m1/explicit-state/catalog-finish.md | 24 ++++++++++++ quest/m2/README.md | 1 + quest/m2/finished-handle.md | 48 +++++++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 quest/m1/explicit-state/catalog-finish.md create mode 100644 quest/m2/finished-handle.md diff --git a/quest/m1/explicit-state/README.md b/quest/m1/explicit-state/README.md index 2850e41ae87..3ab6b3d68d2 100644 --- a/quest/m1/explicit-state/README.md +++ b/quest/m1/explicit-state/README.md @@ -80,6 +80,7 @@ which collapses that state and waits on [Drop set](/quest/m1/drop-set.md). - [Track and group end-of-life](/quest/m1/explicit-state/track-end.md) - moq-net's track and group outcomes are one enum each, in Rust and JS - [moq-net phase data](/quest/m1/explicit-state/net-phases.md) - moq-net's session and subscription state machines keep each phase's data in its variant - [moq-mux and moq-gst lifecycles](/quest/m1/explicit-state/mux-gst.md) - export sources, TS and fMP4 export tracks, and gst pads are explicit state +- [A failed catalog finish is final](/quest/m1/explicit-state/catalog-finish.md) - the catalog producer never reports closed with a catalog track left open ## Related diff --git a/quest/m1/explicit-state/catalog-finish.md b/quest/m1/explicit-state/catalog-finish.md new file mode 100644 index 00000000000..b13e67125e6 --- /dev/null +++ b/quest/m1/explicit-state/catalog-finish.md @@ -0,0 +1,24 @@ +# [XS] A failed catalog finish is final + +## Goal + +`moq_mux::catalog::Producer::finish` is never half-finished: when finishing +either catalog track fails, the producer reports that failure for good and +both catalog tracks end. It is never left closed with a track still open. + +## Plan + +Planned 2026-11-07 (maintainer, /quest-complete), a follow-up of +[moq-dev/moq#8210](https://github.com/moq-dev/moq/pull/8210), which fixed +this shape in the importers and the container, audio, and JSON producers +but left the catalog producer outside its list. + +`finish` (`rs/moq-mux/src/catalog/producer.rs`) stores +`closed = Some(Error::Closed)` before `self.outputs.hang.finish()?` and +`self.outputs.msf_track.finish()?`. If the hang track's finish fails, the +MSF track is never finished, yet every later call reports a clean `Closed`. +Record the terminal outcome after the fallible work, as #8210 did: keep the +first error, abort or end the other track, and store the failure rather +than `Closed`. Test a failing track finish. + +Public API: none. Wire: none. diff --git a/quest/m2/README.md b/quest/m2/README.md index ab4cbc6096b..40fb62afc6c 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -202,6 +202,7 @@ quests that require them. - [A recreated or restarted track cannot reuse group sequences](/quest/m2/sequence-reuse.md) - recreating a track or restarting an instance cannot give new content an old group identity unless the app seeds past it, and datagrams are documented as exempt - [Subscriptions name the oldest group without a u64::MAX sentinel](/quest/m2/anchor-oldest.md) - `Anchor` gets an explicit oldest variant, decoded the same way on every wire - [moq auth serve bounds what a leaked token or public caller can do](/quest/m2/auth-serve-policy.md) - tokens need an `exp` unless the operator opts out, and a public listener authenticates relays +- [Finishing a producer returns a Finished handle](/quest/m2/finished-handle.md) - `finish(self)` consumes moq-net's and the media crates' producers, so appending after finish doesn't compile - [Cut readiness](/quest/m2/cut-gate/README.md) - the checks the cut needs that break nothing: main no slower than release, vendored fixes ported, and moq-c at parity - [Probe](/quest/m2/probe/README.md) - a session probes up to the bitrate it wants to send or receive, so a stopped simulcast rendition resumes and a viewer up-switches; browser padding is parked in m3 (held in m2 by open claims; moves to m3) - [Finish the milestone re-cut](/quest/m2/recut-held.md) - claim-held epics move to the milestone their kind and priority give them once their claims merge diff --git a/quest/m2/finished-handle.md b/quest/m2/finished-handle.md new file mode 100644 index 00000000000..732e0717095 --- /dev/null +++ b/quest/m2/finished-handle.md @@ -0,0 +1,48 @@ +# [L] Finishing a producer returns a Finished handle + +## Goal + +`finish(self)` consumes a producer and returns a `Finished` handle, so +appending after finish is a compile error instead of a runtime one. The +handle keeps what finishing still allows: aborting, which reports a later +failure and releases the cache, and, where the type supports it today, +inserting below the end, such as a late group under a finished track's end. +It has no append. One shape covers moq-net's `track::Producer` and +`group::Producer` and the media producers and importers, with JS mirrors. + +## Plan + +Decided 2026-11-07 (maintainer, /quest-complete), a follow-up of +[moq-dev/moq#8210](https://github.com/moq-dev/moq/pull/8210), which made a +failed finish final with explicit state and recommended against +`finish(self)`. The maintainer chose the typestate: + +- ✅ **Scope: moq-net and media.** moq-net's `track::Producer` and + `group::Producer`, plus #8210's producers and importers (the container + producer, the MKV, fMP4, FLV, and TS importers, the audio and JSON + producers), and their `js/net` and `js/hang` mirrors. Not chosen: moq-net + only; media only. +- ✅ **Name: `Finished`**, as `track::Finished`, `group::Finished`, and so + on. + +Today each producer borrows on finish and enforces the rest at runtime. +`track::Producer::finish(&self)` (`rs/moq-net/src/model/track.rs`) sets the +end after `max_sequence`, so appending at or above it fails, while older +groups below it can still arrive. `group::Producer::finish(&mut self)` +(`rs/moq-net/src/model/group.rs`) borrows so that `abort` can still report +a later failure and its cache stays readable. `Finished` makes exactly that +contract the type. Holding a finished importer or producer, which #8210 +documents as a use, becomes holding its `Finished`. Decide per type what +`Finished` exposes beyond abort, such as read access to the cache or insert +below the end, without adding anything the producer can't do today. + +This breaks every caller that finishes and then keeps using the producer. +List them in the PR and update each package's `MIGRATE.md`. + +Public API: breaking in moq-net, moq-mux, moq-audio, moq-json, and their JS +packages. Wire: none. + +## Related + +- [Track and group end-of-life](/quest/m1/explicit-state/track-end.md) - the outcome enums `Finished` would wrap +- [A failed finish is final](https://github.com/moq-dev/moq/pull/8210) - the explicit state this builds on From 7b2129d5820215f66b165ae7221596728dd46427 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 16:11:41 -0800 Subject: [PATCH 2/3] quest: the Finished handle's migration goes in its PR's Impact section Co-Authored-By: Claude Opus 5.5 --- quest/m2/finished-handle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quest/m2/finished-handle.md b/quest/m2/finished-handle.md index 732e0717095..effa6ae291e 100644 --- a/quest/m2/finished-handle.md +++ b/quest/m2/finished-handle.md @@ -37,7 +37,7 @@ documents as a use, becomes holding its `Finished`. Decide per type what below the end, without adding anything the producer can't do today. This breaks every caller that finishes and then keeps using the producer. -List them in the PR and update each package's `MIGRATE.md`. +List them in the PR, with the migration for each package in its Impact section. Public API: breaking in moq-net, moq-mux, moq-audio, moq-json, and their JS packages. Wire: none. From e338f863352575ae463c6cdcc3d188e59131206d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 7 Nov 2026 16:22:07 -0800 Subject: [PATCH 3/3] quest: plan video's declared-end freeze as no stall Co-Authored-By: Claude Opus 5.5 --- quest/m1/explicit-state/README.md | 1 + quest/m1/explicit-state/video-declared-end.md | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 quest/m1/explicit-state/video-declared-end.md diff --git a/quest/m1/explicit-state/README.md b/quest/m1/explicit-state/README.md index 3ab6b3d68d2..2176ad8e799 100644 --- a/quest/m1/explicit-state/README.md +++ b/quest/m1/explicit-state/README.md @@ -81,6 +81,7 @@ which collapses that state and waits on [Drop set](/quest/m1/drop-set.md). - [moq-net phase data](/quest/m1/explicit-state/net-phases.md) - moq-net's session and subscription state machines keep each phase's data in its variant - [moq-mux and moq-gst lifecycles](/quest/m1/explicit-state/mux-gst.md) - export sources, TS and fMP4 export tracks, and gst pads are explicit state - [A failed catalog finish is final](/quest/m1/explicit-state/catalog-finish.md) - the catalog producer never reports closed with a catalog track left open +- [Video's declared-end freeze is not a stall](/quest/m1/explicit-state/video-declared-end.md) - video holding its last picture through a declared end counts no stall, like audio's declared silence ## Related diff --git a/quest/m1/explicit-state/video-declared-end.md b/quest/m1/explicit-state/video-declared-end.md new file mode 100644 index 00000000000..88f31eaae8d --- /dev/null +++ b/quest/m1/explicit-state/video-declared-end.md @@ -0,0 +1,20 @@ +# [XS] Video's declared-end freeze is not a stall + +## Goal + +`` doesn't count a stall while video holds its last picture +through a publisher's declared end, as audio already doesn't for a declared +silence. The stall count, stalled time, and the echo `stalls`/`stalled` +feedback agree between audio and video. + +## Plan + +Planned 2026-11-07 (maintainer, /quest-complete), a follow-up of +[moq-dev/moq#8209](https://github.com/moq-dev/moq/pull/8209), which made +watch count a stall only while a rendition plays. It left this one case as +on `main`: a picture held through a declared end for longer than the 500 ms +buffering timer still counts as a stall. Give video's playback state a +declared-end phase, as audio's handles a declared silence, so the buffering +timer doesn't run there. Test a declared end held past 500 ms. + +Public API: none. Wire: none.