Repository navigation
fix(moq-mux): an HLS viewer starts on-demand capture - #7054
Conversation
Quest-Claim: 11696052163fc158
An HLS export reads a rendition's records and fetches media per segment, so it never subscribes to the media. An on-demand publisher such as `moq publish` capture therefore never started for an HLS-only viewer. Each recorder now lends its record track's demand to its media track: while the records have a subscriber, a local subscription keeps the media wanted. It is driven by the record track's wakeups (no runtime) and holds only weak track handles. The capture HLS tests that idle between runs now read the records afterward, since a record reader keeps capture running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9d5db6809
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: f9d5db6.
P1 — record refresh can deadlock the publisher (rs/moq-mux/src/hls.rs:737–739). I verified the identical existing finding, rather than adding a duplicate inline comment: TrackState::advance wakes subscription waiters while its write guard remains held; this synchronous callback re-enters Demand::subscriptions, which reads that same state. A later viewer triggering the record track's refresh can therefore stall publication. Defer re-polling, or move those notifications outside the track guard, and add a regression covering a join after the refresh threshold.
Direction: lending record-reader demand at the shared recorder boundary is appropriate, and weak handles avoid extending producer lifetime. The runtime-free callback adds a lock-order requirement that the current implementation does not satisfy. The documented audio-discovery limitation is pre-existing, not a new finding here.
Verification: static inspection of the diff, enrollment paths, demand/subscription lifecycle and refresh locking; no tests run.
`TrackState::advance` woke the subscription waiters while the track-state write guard was held, unlike every other track wake (`join`, preferences, kio's consumer and guard paths). The HLS record lend polls the record track's demand inline from its waker, so a join over the record track's refresh ratio deadlocked the next record publish. Defer `advance`'s wakes until each caller drops its guard, and cover the refreshing join in moq-mux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: d82743f, compared with f9d5db6 (unchanged base).
The previous P1 refresh deadlock is addressed: TrackState::advance now defers demand notifications, and the group, datagram and fill callers release the state guard before firing them. The added a_refreshing_join_keeps_lending regression exercises the problematic late join and verifies that a fresh group is produced.
No new actionable bugs found in this update. Direction is sound: fixing the notification boundary preserves the runtime-free demand bridge without introducing a task or extending producer lifetime. The explicit deferred-wake drain is justified here; future callers of advance must preserve it.
Verification: static review of the incremental changes and all production callers of the affected group/sequence paths. I did not run tests; the author's reported results were not independently reproduced.
|
Merge summary for head
(Written by Claude Opus 5.5) |
|
Re-landing after the merge queue dropped this with
(Written by Claude Opus 5.5) |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re-merged
(Written by Claude Opus 5.5) |
|
Heads-up from moq-dev/moq#7164, which makes
(Written by Claude Opus 5.5) |
|
Re-landing after the third
(Written by Claude Opus 5.5) |
Completes quest/m1/hls-record-demand.md (deleted here, along with its link in the m1 README).
Problem
The HLS export reads a rendition's records and FETCHes media groups per segment request, so it never subscribes to the media. On-demand publishers (
moq publishcapture viamoq-video/moq-audio) gate ontrack.demand().wanted()of the media track. So when an HLS export was the only viewer, the camera never opened and no segments appeared.Change
rs/moq-mux/src/hls.rs: eachRecordernow lends its record track's demand to its media track. While the record track has any subscriber, a local subscription keeps the media wanted. The demand ends when the record track's last subscriber leaves, the record track ends, or the recorder drops.hls.rs: aWakerbuilt on aWeakto the lend re-polls the record track'sDemand::poll_wanted_changedand opens or drops the media subscription inline. kio fires wakers only outside its own locks, and the lend's own work touches only the media track.track::ConsumerfromProducer::consume(), and the lend holds an unresolvedtrack::Subscribing, which is demand while held. So the lend never keeps a track open. My first version held a strongtrack::Producerclone. That kept the media track open past its owner's handle and changed teardown order:moq-hls'srecord_cursors_yield_renditions_and_segmentssawCatalogFailedbeforeRecordsFailed.rs/moq-net/src/model/track.rs:TrackState::advancewoke the demand waiters while holding the track-state write guard. It was the only track path doing so, against kio's "release before waking" rule thatjoinalso follows. Because the lend polls inline from its waker, a join over the record track's refresh ratio deadlocked the next record publish (found by review).advancenow defers its wakes into the state, and its five guarded callers fire them after dropping the guard.container::Producer::with_recorder, and the fMP4 passthrough's enrollment. Every catalog-enrolled video/audio track gets it, not only the CLI.No public API change and no wire change. The lend's inline poll depends on every wake of a track's waiters happening after its lock is released, which kio and moq-net now both uphold. The
Lenddocs record that requirement.Tests
moq-muxhls::test::record_readers_want_the_media: record readers make the media wanted, the last reader leaving releases it, and dropping the recorder releases it.moq-muxhls::test::a_refreshing_join_keeps_lending: puts a reader on the records, passes the refresh ratio, joins late, and publishes the record that answers the join with a fresh group. It hangs (nextest's 60s timeout) with the moq-net change reverted, and passes with it.moq-videoencode::producer::tests::control::an_hls_only_viewer_starts_capture: runs the real capture drivermoq publishuses, against a synthetic camera. The only subscriber is amoq_mux::hls::Consumeron the advertised records. The camera opens and the first segment record arrives.moq-videoandmoq-audiohls_records_keep_the_live_timestampsheld a record reader across an idle restart. Under the new semantics that reader keeps capture running, so both now read the records after the runs. What they cover (idle gap left out of the segments) is unchanged.Choices made without asking
with_recorder+ fMP4 enrollment, not by changingTracks::track/catalog::Producer::enrollto take the media track. Binding at enrollment would be marginally harder to miss, but it would change ~20 name-based test call sites for the same two production paths.Subscription::default()(priority 0, zero delay budget), the same assubscribe(None)elsewhere. On a publisher-local track the aggregate isn't forwarded anywhere.Not covered: audio records aren't advertised until audio has run
moq-audiocapture enrolls its rendition without an HLS hint, so itshlscatalog field appears only after its first segment. An HLS-only viewer of an A/Vmoq publishtherefore finds only the video records. Video starts now, but audio's records stay unadvertised, so audio never starts and the export lists no audio rendition. This has a separate, pre-existing cause; once audio records are advertised, the demand path in this PR starts audio too. Audio groups have a fixed cadence (Options::group_duration), which is exactly what a hint declares. Two catches:catalog.reserve().with_hls_hint(..)would make the rendition withhold the catalog until layout discovery, which audio capture deliberately avoids.moq-muxAPI.So it's suggested as a follow-up below rather than done here.
Suggested follow-ups
moq-audio's capture (rs/moq-audio/src/encode/producer.rs,Reserved::new→catalog.audio(..)) should pass its fixed group cadence as an HLS hint, so the catalog names its record track before audio runs. That needs a way to hint a rendition that doesn't withhold the catalog, e.g. a hint oncatalog::Producer::audio/track, or aReservedvariant that doesn't gate. Then extendan_hls_only_viewer_starts_capture(or an audio twin) to an HLS-only viewer of audio capture.moq-mux.cargo doc -p moq-mux --document-private-items -D warningsfails on pre-existing links (catalog/consumer.rsmsf,catalog/producer.rsRenditionConfig::insert,catalog/tracks.rsInit::label,container/producer.rsReserved). CI doesn't build private docs, so they don't fail it.Checks run locally (all through
quest gate)just rs test -p moq-net -p moq-mux -p moq-hls -p moq-relay: 5532 passedjust rs test -p moq-video -p moq-audio -p moq-scene -p moq-studio -p moq-cli --features moq-video/capture,moq-video/openh264,moq-audio/capture,moq-audio/playback,moq-scene/capture,moq-studio/capture: 841 passedcargo fmt --all -- --check,RUSTDOCFLAGS="-D warnings" cargo doc -p moq-mux --no-deps,quest check: clean(
just rs testenforces clippy throughclippy-driver.)🤖 Generated with Claude Code
(Written by Claude Opus 5.5)