Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions quest/m0/broadcast-epoch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ then. MoQ Pro's VOD `storage.json` moves from the same seed to its own epoch.
- [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
- [TS restart](/quest/m0/broadcast-epoch/ts-restart.md) - a signalled backward TS discontinuity finishes the broadcast and continues the same input under a fresh epoch
- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and let a publisher announce one
- [moqsrc](/quest/m0/broadcast-epoch/moqsrc.md) - moqsrc switches to the new broadcast on a `Restart`, keeping its pads by rendition name
- [Claim-served epochs](/quest/m0/broadcast-epoch/claim-epochs.md) - a claim's answer names the instance that served it, so a restarted output is never spliced and a per-output epoch costs no restart
- [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)
- [Dynamic epoch update](/quest/m0/broadcast-epoch/dynamic-epoch.md) - an epoch change through `Dynamic::update` announces a `Restart` downstream, so subscribers drop the old copy and resubscribe, while the origin keeps serving
93 changes: 93 additions & 0 deletions quest/m0/broadcast-epoch/moqsrc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# [M] moqsrc follows a restart

## Goal

When the source behind `moqsrc`'s path restarts (an announce `Restart`), the
pipeline keeps running and switches to the new broadcast: a fresh catalog on
the same pads, with no error on the bus. Once subscriptions are sticky,
`moqsrc` would otherwise keep playing the replaced broadcast until its route
goes, then end or error. A broadcast that ends with no successor still ends as
it does today, unless the opt-in `linger` holds it.

## Plan

Decided in planning (2026-10-06, after #4955 found that no quest covered
`moqsrc`), re-scoped onto [Restart](/quest/m0/broadcast-epoch/restart.md)
(2026-10-07):

- **Follow announcements.** `moqsrc` follows the path's announcements
(`origin::Consumer::announced`) rather than resolving once with
`routed_broadcast`. It plays on `Start`, switches on `Restart` by
re-requesting the path (which resolves the new route), and ends on `End`.
An `Update` (a re-price or same-epoch failover) changes nothing, so it
stays a seamless mid-group resume. Restart owns deciding what is a source
change, so `moqsrc` never compares epochs itself.
- **Route-level restarts (maintainer, 2026-10-08).** A `Restart` names a
route, which can be a prefix covering the path (such as `pool`) rather
than the path itself. `moqsrc` switches on a `Restart` of the route its
path resolves through; the re-request re-resolves upstream, and if it
lands on the same source the run still restarts on the same pads.
- **Errors keep today's handling.** A replacement no longer ends the old
subscriptions, so a catalog error posts the session error and a track error
logs and ends that pad, as today. The earlier rule treating `Unroutable` as
a switch in flight is dropped: it could not tell a switch from a dead route
and could hang. Only `linger` below defers an error, and only for its
window.
- **Cutover.** Cancel the old pumps as soon as the `Restart` is seen, without
waiting for their subscriptions to end.
- **Pads (decided 2026-10-07, reconfirmed after the Restart re-scope).**
Keep each pad by rendition name across a switch, so a `gst-launch`
pipeline linked by name (`s.video_0 ! queue`) keeps playing. Fresh pads
were rejected: pad ids come from a process-wide
counter, so a new run's pads get new names, stay unlinked, and the pipeline
goes idle silently. Pad state outlives a pump: a pump hands its pad back to
the session instead of removing it. On the switch, push stream-start, the
new caps (even when they differ, so a codec change fails loudly as
not-negotiated rather than going quiet), and a segment whose `base` is the
current running time, so a synced sink does not drop the new run (PTS
restarts at 0) as late. An in-run caps or container change keeps its pad
the same way. A rendition the new catalog drops ends with EOS; a new or
renamed one gets a new pad.
- **Linger (decided 2026-10-07).** A `linger` property, default 0, that
covers a lite-06 or moq-transport restart whose END and START are not
coalesced into a `Restart`, and a crashed or withdrawn source. While
`linger` > 0, a track that ends or loses its source (the route went,
`Dropped`, or the upstream session closed) hands its pad back without EOS,
since tracks, the catalog, and `End` arrive on different streams in either
order. A rendition the catalog already retired still drains to EOS as today.
The window starts on the first of `End`, the catalog finishing (a prefix
can stay announced after its output finishes), or the catalog losing its
source. A `Start` or `Restart` in it resumes on the same pads. At expiry
every held pad gets EOS, and the session error posts only if the catalog
lost its source; a pump still streaming ends as it would without linger. A
held track whose catalog and path stay live waits the same bound from its
own end, then ends its pad as it would at 0; opening the window cancels
those per-track timers, so held pads follow the window's expiry. The bound
keeps this from becoming a silent hang. At 0 nothing changes from today.
- **Where.** The logic stays in `rs/moq-gst/src/source`. If
[Apps](/quest/m0/broadcast-epoch/apps.md) lands a shared helper for
following announcements first, use it instead.
- **Test.** An in-process test republishes the path under a newer epoch while
the old publisher stays up: the new run's buffers render through a synced
sink, a pad linked by name keeps flowing, and the bus carries no error.
Cover an epochless source change that also switches, a `Restart` of a
prefix route covering the path that switches too, a same-epoch
re-announce (`Update`) that does not, a switch with new caps that keeps the
pad, and with `linger` set: an `End` then `Start` that resumes on the same
pads (media FIN, a delay, catalog FIN, `End`, then `Start`), an old
publisher's session closed (not finished) over a relay that resumes on the
next `Start`, and a finished output under a still-announced prefix that
gets EOS by the deadline, and a lone track losing its source while its
siblings and the catalog stay live, which ends only that pad. With
`linger` at 0, each of these ends or errors as today. Drive the linger
window with mocked time.

Update the `moqsrc` section of `doc/bin/gstreamer.md`: a restart keeps each
pad by rendition name, a format change keeps its pad, a dropped rendition gets
EOS, and `linger`.

Public API: a new `linger` property on `moqsrc`. Wire: none.

## Required

- [Restart](/quest/m0/broadcast-epoch/restart.md) - delivers the `Restart` announce event and sticky subscriptions this follows
4 changes: 2 additions & 2 deletions quest/m0/broadcast-epoch/restart.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ Decided in planning (2026-10-07, from #4970's review):
lite-07 changelog's "ends subscriptions to the older one". The rule that a
subscription between routes without an Epoch "stays on its route and ends
with it" holds as written.
- **Players** follow `Restart` in [Apps](/quest/m0/broadcast-epoch/apps.md);
`moqsrc` (planned in #4960) switches on it too. On lite-06 and
- **Players** follow `Restart` in [Apps](/quest/m0/broadcast-epoch/apps.md),
and [moqsrc](/quest/m0/broadcast-epoch/moqsrc.md) switches on it too. On lite-06 and
moq-transport, a pair not coalesced reaches players as `End` then
`Start`: a stop, then a fresh play.
- **Every Rust consumer** of announce events handles `Restart` in the same
Expand Down
Loading