Skip to content

feat(moq-gst): moqsrc follows a restart on the same pads - #5181

Closed
t0ms wants to merge 22 commits into
moq-dev:mainfrom
t0ms:quest/m0/broadcast-epoch/moqsrc
Closed

t0ms wants to merge 22 commits into
moq-dev:mainfrom
t0ms:quest/m0/broadcast-epoch/moqsrc

Conversation

@t0ms

@t0ms t0ms commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Problem

moqsrc resolves its path once. Now that subscriptions are sticky (#5087), it keeps playing a replaced broadcast until that broadcast's route goes away, and then ends with EOS or a session error. A restarted publisher, a newer instance taking over, or a lite-06 end and start never reaches the pipeline as a switch.

Completes quest/m0/broadcast-epoch/moqsrc.md.

Approach

  • Follow announcements. The session follows its path with origin::Consumer::follow from feat(apps): players follow announce Start, Restart, and End #5154. A Start or Restart cancels the old run's pumps, then starts a run on the route serving the path. An Update starts one only when nothing is playing, meaning the path came back after a gap that the announcements folded away. An End holds the pads.

  • Pads by rendition. A slot per rendition name holds the pad between pumps, so a restart or an in-run caps or container change hands the same pad to the next pump. The new pump pushes stream-start and its caps; refused caps fail as not negotiated. On its first buffer it pushes a segment whose base is the current running time, so a synced sink does not drop the new run as late. A rendition the catalog retires ends with EOS, and a new one gets a new pad.

  • Source loss holds. A track or catalog that ends or loses its source hands its pad back without EOS. This works whether the loss arrives before or after End, and the next Start resumes on the same pads. A catalog that the live source lacks or refuses (NotFound, Unauthorized) still posts the session error, as does a malformed one, and losing the connection still ends the session. Mid-stream only malformed content counts against the source, because a producer that goes away surfaces as almost any error, including NotFound for a group that can never be served.

  • Tests. Unit tests cover both orders of source loss and End, a reshape keeping its pad, and a broadcast without a catalog failing the session with NotFound. tests/source.rs runs moqsrc ! fakesink sync=true max-lateness=20ms against an in-process relay. It covers:

    • a newer instance taking over while the old one stays up;
    • an epochless republish;
    • a restarted prefix route;
    • an update that does not switch;
    • new caps on the same pad;
    • an end followed by a start;
    • a closed publisher session.

    Each case checks that the new run renders at its frame rate on one pad with no bus error. Pushing the segment with a base of zero fails all seven, and switching on every Update fails the update case.

Impact

  • No new property. An ended broadcast no longer sends EOS on its pads; they hold until the path is announced again.
  • A caps or container change within a run keeps its pad instead of replacing it.
  • A downstream flow error other than flushing, not-linked, or EOS now posts an element error instead of stopping the pad silently.
  • Losing the source is no longer a session error. A missing, refused, or malformed catalog still is.
  • Wire: none.

Alternatives

  • Fresh pads for each run: rejected in planning, because pad ids come from a process-wide counter, so a pipeline linked by name would go idle.
  • A linger timer before EOS: rejected in planning, because announcements already say when the source is back, and a timer cannot tell a slow restart from a dead source.

Follow-ups

(Written by Claude Opus 5.5)

kixelated and others added 20 commits October 9, 2026 15:26
moq play follows its path through a new moq_mux::Source::follow: it plays on
Start, starts over with a fresh catalog, decoders, and clock on Restart, lets
what is playing finish on End and waits for the next Start, and ignores
Update. @moq/watch video skips the catch-up gate and both halves re-anchor the
shared clock on a republished instance. demo/web stats resubscribes on a
restart. Publishers and the lite announce logs name the epoch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`origin::Consumer::follow(path)` returns `announce::Follow`, which reduces the
announcements of every route covering a path to the Start, Update, Restart,
and End of the one serving it. `@moq/net` mirrors it as
`Origin.Consumer.follow(path)` (and on `Producer` and `Table`), returning an
`Announce.Consumer`. `routed` is now the follower's first event.

`moq play` builds on it, and `moq_mux::Source::follow` / `moq_mux::Follow` are
gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Broadcast` follows its name through `Origin.follow`, so the exact route
ending while a covering prefix still serves the name moves playback to the
prefix instead of going offline. A name outside the origin's scope is now a
refusal (`status` "error") instead of an effect error logged to the console.

`@moq/net`'s follower is a synchronous reducer inside the announced loop, so a
followed stream delivers in step with an announced one. The async version
delivered the initial start after a quick refusal, which re-requested and
cleared it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	doc/lib/rs/moq-net.md
#	quest/m0/broadcast-epoch/README.md
…ev#5141)

`quest check` fails on main: both blockers landed and their quests were deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A route claiming only paths beneath the followed one (a scoped dynamic at the
same prefix) could win that prefix on cost and mask the route that serves the
path, so a republish of the real one produced no restart. Scoping the follower
to the literal path drops such routes, as requests already do.

JS also falls back to the whole scope for a path no pattern can spell (a `*`
in a segment), as Rust does, and an announced stream no longer closes when it
reaches such a path: its captures are none, as in Rust.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restart check read the catalog broadcast's instance, so a rendition
reading another broadcast through a `broadcast` override missed that one's
republish and froze, while a catalog-only republish reset the clock with the
media unchanged. Key it on the resolved media's path and instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Falling back to the whole scope for a path with a `*` segment lets a route
that never serves it win its prefix, and JS announcements already stop at
such a prefix. Refuse it instead (`Error::InvalidPath`, a throw in JS), as
`routed` did before it was built on `follow`, and drop the partial JS
captures guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The replay a follower starts with names a covering prefix before the exact
path beneath it, which came out as a start on the prefix and then a restart
onto the exact path, so a late-joining player requested the broadcast twice.
Fold every announcement on hand (one table change in JS) into one event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… path

Folding every change on hand into one event turned an end followed by a
start under the same epoch into an update, which hid a gap that had already
ended the request on the old route, so @moq/watch never asked again. Fold
only while the path is unserved (the late-join replay), and pass each change
through once something serves it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A covering route of one epoch arriving as the exact route goes, both before
the follower is polled, reaches it as an update: the announce cursor delivers
by prefix, so the path never shows unserved. A player whose broadcast had
already ended ignored that update and stayed stopped. With nothing playing,
an update now plays the path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@t0ms t0ms changed the title feat(moq-gst): moqsrc follows a restart feat(moq-gst): moqsrc follows a restart on the same pads Oct 10, 2026
@t0ms

t0ms commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Outcome: implementation complete. This PR stays a draft until its dependencies land.

(Written by Claude Opus 5.5)

t0ms and others added 2 commits October 10, 2026 15:03
moqsrc holds its pads only when the source goes away. A catalog the live
source lacks or refuses (NotFound, Unauthorized) posts the session error,
as on main. Mid-stream, only malformed content counts against the source,
since a producer going away surfaces as almost any error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…och/moqsrc

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	quest/m0/broadcast-epoch/README.md
#	quest/m0/broadcast-epoch/moqsrc.md
@t0ms

t0ms commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #5191, which landed the same moqsrc quest: following the path's announcements, pads kept by rendition across restarts, held pads on source loss, and a missing or refused catalog failing the session. The two follow-ups planned from this PR (moqsink's first catalog and moqsrc A/V alignment) are in #5189. The moq-net fix this PR found, a restarted prefix restarted twice on lite-06, landed separately in #5182.

(Written by Claude Opus 5.5)

@t0ms t0ms closed this Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants