diff --git a/quest/m0/broadcast-epoch/README.md b/quest/m0/broadcast-epoch/README.md index 3b4082003a..cce1026afe 100644 --- a/quest/m0/broadcast-epoch/README.md +++ b/quest/m0/broadcast-epoch/README.md @@ -107,6 +107,7 @@ maintainer's pre-release merge of `release` into `main` keeps `main`'s - [JS consume identity](/quest/m0/broadcast-epoch/js-consume-identity.md) - requests carry the serving prefix's epoch and new winners cannot reuse an old cached broadcast - [JS restart keeps the request](/quest/m0/broadcast-epoch/js-restart-keeps-request.md) - a resolved request remains on its old instance, matching Rust's sticky subscriptions +- [Source pin](/quest/m0/broadcast-epoch/source-pin.md) - an export's later requests stay on the broadcast it resolved, so an epochless replacement never splices into the old program - [Publish catalog restart](/quest/m0/broadcast-epoch/publish-catalog-restart.md) - `@moq/publish` never reuses catalog group numbers under one name and epoch after a re-announce - [Bindings](/quest/m0/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and let a publisher announce one - [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the same release retires the per-path stats maps for totals and on-demand prefix tracks (decided 2026-10-05) diff --git a/quest/m0/broadcast-epoch/source-pin.md b/quest/m0/broadcast-epoch/source-pin.md new file mode 100644 index 0000000000..02ac13c458 --- /dev/null +++ b/quest/m0/broadcast-epoch/source-pin.md @@ -0,0 +1,48 @@ +# [S] An export's later requests stay on the broadcast it resolved + +## Goal + +`moq_mux::Source` keeps the first `broadcast::Consumer` it resolves for each +path, its own and every cross-broadcast reference, and serves every later +request for that path from that handle. A late rendition subscribe or SI +repoint never lands on a replacement, epoch or not. On a replaced instance +it fails loudly instead of splicing the new publisher's data into the old +program. + +## Plan + +Found by Codex on #5147 (2026-10-10), planned the same day. `Source` pins +its own path by epoch (`epoch` in `rs/moq-mux/src/source.rs`, set through +`pinned` from `ts::Export`). An epochless route (every default lite-06 +session) has `None`, so each later request re-resolves the path, and if a +replacement won in the meantime, it serves that request even without +`--stitch`. + +- Decided: pin by handle, replacing the epoch field. One mechanism covers + epoch and epochless routes. A replaced instance's sticky front refuses new + tracks, so a late request on it errors, and the Follower decides what + happens next (`Replaced`, or a stitch). +- Decided: pin every resolved path, not only the source's own, so a replaced + sibling (`broadcast: ./source`) cannot splice in either. +- The pin lifecycle follows the export's instance. A stitch builds a new + `Source` whose own-path entry is seeded with the broadcast `follow` was + given (sibling entries start empty), so its first track request can't + resolve a newer replacement. A same-epoch return replaces the own-path + handle with the returned broadcast, since the old one's routes are gone. +- The guarantee starts at a path's first resolution. A sibling the catalog + first references after a replacement resolves whatever serves it then. + The export never held another instance of that path, so that isn't a + splice. +- Tests, with mocked time, on an epochless route: an export resolves `live`, + a replacement wins `live`, and then a rendition added to the old catalog + (and an SI repoint) fails rather than reading the replacement. The same + for a sibling reference already resolved. A stitch whose replacement is + itself replaced before the first track request still reads the broadcast + it followed. A same-epoch return reads the returned handle. Today's + epoch-pin test (`source.rs`) moves to the handle. + +Public API: no signature change, but a behavior change. A `Source` reused +across a route replacement now returns its pinned handle for a path it +already resolved (`broadcast`, `catalog`, and track requests), or fails on +the replaced instance, instead of resolving the current route. Document it +on `Source`. Wire: none. diff --git a/quest/m1/README.md b/quest/m1/README.md index 542d1a9121..c854a0187e 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -178,6 +178,7 @@ bench all require its crate. - [Catalog warmup](/quest/m1/catalog-warmup.md) - `warmup` on video and audio renditions, in the catalog and the draft - [Audio warmup](/quest/m1/audio-warmup.md) - a viewer joining an Opus rendition mid-stream never hears the unconverged first 80 ms - [#3021](/quest/m1/3021-moq-gst-anchor-generated-media-timelines-to-wall-clock.md) - moq-gst picks the broadcast wall epoch; a restarted source is a new epoch, not a forward re-anchor +- [TS stitch catalog bound](/quest/m1/ts-follow-catalog-bound.md) - `--linger` bounds a mid-stream `--stitch` until the replacement's catalog arrives, so it ends loudly instead of stalling - [TS passthrough export](/quest/m1/ts-passthrough-export.md) - `export ts --passthrough` writes the `m2ts` track back byte-identical (less late drops) on a fixed delay - [FLV and MKV export delay](/quest/m1/export-delay.md) - FLV and MKV interleave through the shared jitter buffer on a fixed delay, breaking the CLI once - [MKV lacing](/quest/m1/mkv-lacing.md) - laced MKV blocks import as one timed frame each, refused without DefaultDuration diff --git a/quest/m1/ts-follow-catalog-bound.md b/quest/m1/ts-follow-catalog-bound.md new file mode 100644 index 0000000000..2ab57704a0 --- /dev/null +++ b/quest/m1/ts-follow-catalog-bound.md @@ -0,0 +1,34 @@ +# [S] A TS export stitch is bounded by the linger until its catalog arrives + +## Goal + +When `moq export ts --stitch` (or moq-srt egress with `stitch`) switches to +a replacement mid-stream, `--linger` bounds the wait for the replacement's +catalog, as it bounds a same-epoch return. A replacement that never delivers +its first catalog snapshot ends the export with an error instead of stalling +it with nothing going out. + +## Plan + +Found while iterating #5147 (2026-10-10), planned the same day. `ts::Follower` +(`rs/moq-mux/src/container/ts/follower.rs`) carries a linger deadline for a +return, which #5147 extended to the returned catalog's first snapshot. A +stitch that starts after the old broadcast ended inherits that deadline. A +mid-stream stitch, taken from `Running` while the old instance still serves, +carries none (`settling: None` in `State::Following`), so its catalog wait is +unbounded. + +- Decided: the linger is the one bound, from the `Restart` to the + replacement's first catalog snapshot. With `--linger 0` a stitch only + succeeds when the catalog is already there. Rejected: bounding by + `--delay`, and a fixed constant. +- On expiry the export ends with an error naming the replacement that never + served its catalog. +- Tests, with mocked time: a stitch onto a replacement whose catalog never + answers ends at the linger. One whose catalog answers within it switches + as today. +- Docs: the `--stitch` and `--linger` text in `doc/bin/cli.md` and + `doc/bin/srt.md`, including that the default `--linger 0s` lets a stitch + follow only a replacement whose catalog is already there. + +Public API: none. Behavior: a stitch can now end the export. Wire: none.