feat(moq-gst): relay-less listen mode for moqsink and moqsrc + live catalog subscription#15
feat(moq-gst): relay-less listen mode for moqsink and moqsrc + live catalog subscription#15santi-ferreiro wants to merge 7 commits into
Conversation
Adds `listen` and `tls-generate` properties to moqsink so it can run its own QUIC/WebTransport server and serve a broadcast directly to subscribers that dial it, with no relay in between. `listen` is mutually exclusive with `url` (dial mode stays the default); the server branch in run_session mirrors the relay accept-and-serve path. Documents both properties in doc/bin/gstreamer.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the `ordered` field to Track initializers in tests so the suite compiles after the group-order feature added that field to Track. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- require tls-generate in listen mode (server needs a cert; fail early with a clear error instead of a cryptic runtime init failure) - signal accepted sessions to close when the element stops, so detached session tasks don't leak connections past run_session - doc: state listen and url are mutually exclusive (the code already errors if both are set) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror of moqsink listen mode. moqsrc can run its own QUIC/WebTransport server (listen + tls-generate) and consume a broadcast from a publisher that dials it, with no relay. listen is mutually exclusive with url; the server branch in run_session accepts one session and consumes into the origin. Keeps the publisher as the dialing (QUIC client) peer, which is required for QUIC connection migration when the publisher's address changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The publisher briefly publishes an empty catalog while replacing a track: the old importer drop removes its rendition before the successor re-adds it. moqsrc treated an empty catalog as a completed session and exited silently, permanently stalling the stream. Skip catalog updates with no tracks and wait for one that lists renditions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pear A single catalog snapshot races the publisher pipeline: audio registers at caps time, video only at the first keyframe, and a replaced track briefly publishes an empty catalog before reappearing under a new name. Reading one snapshot could yield no tracks (silent stall) or an audio-only or doomed track list. Keep consuming catalog updates and subscribe to each new track, deduplicated by name. Session errors now surface through the catalog stream, posting an element error instead of idling on a dead session. Drop the NoMorePads message since pads can now appear at any time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a listen mode for both moqsink and moqsrc GStreamer elements, allowing them to run a local QUIC/WebTransport server and serve or consume broadcasts directly without a relay. It also updates moqsrc to dynamically handle catalog updates and subscribe to tracks as they appear. A critical issue was identified in moqsrc's listen mode where the server's accept loop does not select on the shutdown signal, potentially causing the pipeline shutdown to hang indefinitely if no publisher connects.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| server_config.tls.generate = tls_generate; | ||
| let mut server = server_config.init()?; | ||
| gst::info!(CAT, "moqsrc listening on {bind}"); | ||
| let request = server.accept().await.context("listener closed before a session arrived")?; |
There was a problem hiding this comment.
In moqsrc listen mode, server.accept().await is called without selecting on the shutdown signal. If the element is stopped before a publisher connects, the task will hang indefinitely on this await, causing the GStreamer pipeline shutdown to stall.
We should wrap server.accept() in a tokio::select! with shutdown.changed() to ensure graceful termination.
let request = tokio::select! {
res = server.accept() => res.context("listener closed before a session arrived")?,
_ = shutdown.changed() => return Ok(()),
};There was a problem hiding this comment.
Good catch — fixed in af73560. server.accept() is now wrapped in a tokio::select! with shutdown.changed(), matching how the dial path already guards the announce wait. Verified the accept path still works end-to-end after the change. (Written by Claude)
…ession Stopping the element before a publisher dialed left the session task parked in server.accept() forever, keeping the UDP socket bound. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Enables relay-less MoQ in moq-gst by letting either element run its own QUIC/WebTransport server, and fixes a catalog race in moqsrc that this exposed. Supersedes #14 (includes its commits; this targets benchmark-develop directly).
listen+tls-generateproperties; the sink runs a server and serves its broadcast to subscribers that dial it (publisher-as-server topology, used by the group-ordering verification).Listen mode
Both elements gain a
Transport { Client { url } | Server { bind, tls_generate } }resolution;listenis mutually exclusive withurland dial mode stays the default. The sink's server branch mirrors the relay accept-and-serve path (request.with_publish); the source's mirrors it on the consume side (request.with_consume(origin).ok().await). Everything downstream of session establishment is unchanged in both elements.Catalog fix
The catalog is a live track that mutates during publisher startup: audio registers at caps time, video only at the first keyframe, and a track replacement briefly publishes an empty catalog (the old importer's
Dropremoves its rendition before the successor re-adds it under a new name).moqsrc previously read one catalog snapshot. Depending on timing it could see zero tracks (and exit as if the stream had ended: a silent, unrecoverable stall) or an audio-only list (never subscribing to video). The race is latent in every topology; subscriber-listen exposes it because the subscriber is connected and reading before the publisher's pipeline warms up.
Fix:
run_sessionloops oncatalog_updates.next(), subscribing to each new track (deduplicated by name) until the catalog track finishes or shutdown. A dead session now surfaces as agst::element_error!instead of idling forever. The now-invalidNoMorePadsmessage is removed since pads can appear at any point in the stream.Testing
Cross-package sync
doc/bin/gstreamer.md— listen-mode sections for both elements.(Written by Claude)