Repository navigation
feat(scene)!: pin moq-mux's decoupled playout clock (moq-dev/moq#8272) - #144
Conversation
Quest-Claim: bf747538139c5204
Cross the moq pin to 644d48996 (moq-dev/moq#8272). A broadcast source's audio and video each hold the shared sync clock, so a sound stall no longer pauses the picture: the sound comes back on the clock, its late part trimmed. Also crosses #8269 (encode::Producer takes a track, Exporter::new returns the config) and #8274 (mix.minus takes every strip). upgrade-moq.sh now retries a crate a newer dependent holds back, which the rind 0.3 downgrade needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts sound Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pullfrog's workflow isn't active on your default branch, so this run didn't start. |
|
No actionable comments were generated in the recent review. WalkthroughBroadcast playout now uses shared sync schedules, with audio and video following independent timing behavior. Layout readers apply layouts when their schedule due time arrives. Publishing code derives video track suffixes from codecs and adapts producer and encoder setup. The crate upgrade script reports all version differences and attempts updates across pending crates. The README, quest notes, and studio tooltip reflect the playout changes. Merge Risk:
|
|
@pullfrog review this pull request. Its base now requires an approving review, and this head predates Pullfrog on this repository. (Written by Opus 5.5) |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed the full diff at 7c50ed0. That covers the moq pin crossing to 644d48996 and the move of broadcast playout onto moq_mux::sync. I checked the new code against rs/moq-mux/src/sync.rs at the pin, including how received, reset, release and set_target behave.
- Pin:
644d48996is a commit on moq'smain(0 ahead, behind only). The quest file is deleted andpin-main.mddrops itsRequiredlink to it, as the convention expects. playout.rs:Holdwraps async::Trackand signalsmovedwhenever the schedule shifts.set_targetcaps the audio target plusWRITE_LEADatMAX_DELAY.follownow keeps a frame that is partly due, andWriter::writealready trims the part the bus has mixed.source.rs: video presents onpresentwith no gate on the sound, and audio works out each frame's moment again when it writes it. An audioGaporEndfreezes the moments of the queued tail before the reset that comes with the next frame. That matters becauseState::resetclears the anchor, and with no video anchorduewould becomeNoneand drop the tail. Video resets as soon as it sees a gap.watch.rs:take_dueand the keyframe start now read onlydue, sinceplayheadis gone.- Upstream API breaks:
encode::Producer::new(track, ..)uses the sharedpublish::suffixtable,Exporter::newnow returns the config, and the studio callsmix.minus([strip]). sh/upgrade-moq.sh: each pass now tries every drifted crate. Sincedrift()printsname@version,-pnames one package exactly. The script only fails when a whole pass moves nothing.- Docs: the
README.mdbroadcast-source paragraph and the Sources tooltip now describe the decoupled clock.
Check was still running when this review was submitted.
claude-opus-5-5 | 𝕏

Crosses the
moqsubmodule from79960a9ddto644d48996(moq-dev/moq#8272) and moves a broadcast source's playout ontomoq_mux::sync's decoupled clock, following the maintainer's 2026-11-12 decision recorded in the quest: a sound stall no longer pauses the picture.Finishes and deletes
quest/m0/pin-sync.md.Changes
just upgrade-moq 644d48996, withCargo.lock.bun.lockdid not change.sh/upgrade-moq.sh: the drift loop used to move one crate at a time to moq's version and failed when a newer dependent held it back. Here that wasrind0.3.2 → 0.3.1, held back byrind-rcgen0.14.3 andrind-rustls0.23.3. It now tries every drifted crate on each pass, retries the ones held back once their dependents have moved, and fails only when a whole pass moves nothing.rs/moq-scene/src/playout.rs: theSpeaker(which owned the clock's anchor, skipped bursts, and deferred reports),Place,heard, andSLACKare gone. What replaces them:Holdwraps async::Track. It folds in arrivals, holds the audio's jitter target plusWRITE_LEAD(capped atMAX_DELAY), resets at a discontinuity, and sends on awatchwhenever the schedule may have moved, so the other track wakes and recomputes its deadline.follow(due, duration, now)decides when to write a sound: its lead before it is due; still written once partly due, becauseWritertrims what the bus has already mixed; dropped once it has fully played out.rs/moq-scene/src/source.rs:play_videoandplay_audioeach hold the clock for their kind. Video shows whateverpresentsays is due, with no wait for sound.GaporEndresets that track's hold. Audio resets at its next frame, keeping its queued tail's moments.rs/moq-studio/src/watch.rs: a layout takes effect once the program's clock says its stamp is due. Keyframes start atdue(start), sinceplayheadis gone.encode::Producer::newtakes a track. The studio names it.avc3or.hev1from the probed codec and fails on any other codec.with_trackbecamenew, andExporter::newreturns the config.mix.minus([strip]).README.md: the broadcast-source paragraph drops "Each picture waits for the sound stamped with it" and the sound-stall pause. It now says the picture plays on through a sound stall and the sound returns on the clock.Public API
Breaking, in
moq-scene:source::heardis removed. Readclock.due(..)instead.Feed::clock()returnsmoq_mux::sync::Consumer, nottimeline::Consumer.Feed::delay()now includes the 50 ms write lead, because that is how far behind arrival the sound and picture actually play.No wire change.
Choices made without asking
WRITE_LEAD, capped atMAX_DELAY. Every sound is then handed to the bus whole when it is on time, video plays with it, and playback stays within the 2 s that--watch's layout queue assumes. The alternative was to leave out the lead, which would trim the start of every frame whenever the jitter target is under 50 ms.GaporEndresets that track's hold, because the decoder reports a timeline that does not continue as aGap. Without the reset, a rewound audio timeline would be dropped forever. Audio resets only when the next frame arrives, and freezes the moments of the frames still queued first, so a tail that nothing replaces still sounds on time (review round 1). Video resets at once: at most it shows its few queued pictures early.RECHECK(10 ms) while it waits, since that clock does not notify its readers. The broadcast's own tracks wake on thewatchinstead.Validation
just cipasses: 305 tests.source::tests::a_broken_off_sound_keeps_its_tailfails with an immediate reset and passes with the deferred one. The new and rewritten tests ran 30 times under--stress-count 30with no failures.source::tests::a_picture_plays_with_its_sound_and_through_its_stall. It runs a real broadcast through the source driver. The picture shows with its sound at arrival plus the delay. Through a 1 s sound stall, every picture shows on its exact moment. After the sound's backlog lands late, pictures keep showing. The oldheardgate would hold every picture during the stall.MAX_DELAYcap.moqlistener (built from the submodule) imported an ffmpeg MPEG-TS oftestsrc(H.264) and a sine tone (Opus), with the audio cut for 3 s (aselect='not(between(t,15,18))'). Themoq-studiobinary (--features openh264) played it as a broadcast source under headless sway (lavapipe Vulkan) for 35 s, with temporary trace points (not committed) on arrivals, shows, and writes.maintoo (screenshot ofmain's build showedfailed). This is unrelated to this change.Decisions
dueonly to anchor and skip, since the sound drifts up to one step from the picture; damping the clock's delay, since it keeps the same total silence and skips and responds slower to real jitter).Caused by
None. This is a planned pin and a move onto the new API, not a bug fix.
Follow-ups
/quest-planthere): a stretching positioned writer in moq-audio's bus, so a source's sound converges on the clock after a jitter-target step instead of gapping or skipping. No open decisions.Models
Size: M
Implementer: Claude Code, claude-opus-5-5, unknown effort
Implementer: Claude Code, claude-opus-5-5, unknown effort (iterate, S)
(Written by Claude Opus 5.5)
Review
Rewritten by
quest reviewafter each round.Head: e100a3b, named in full once no reviewer reports a finding
Base: origin/main at 77ae0c8
Reviewer: Codex, gpt-6.1-sol, high effort
Session:
01a1d10f-8d77-7a73-9e28-c91690772e7a, round 3Score: 10/10,
The incremental changes preserve codec suffix behavior and clarify the documented audio delay; code review found no issues, and builds and tests were not run.Findings: none
Reviewer: opencode, opencode-go/deepseek-v4.1-flash, high effort
Session:
ses_e2e696341ffe1mcwy6s1RwSF7S, round 1Score: 8/10,
The pin and decoupled playout are complete, with the audible-gap trade-off settled by the maintainer; only a stale user-facing tooltip remains.Findings, open until the next round settles them:
rs/moq-studio/src/ui.rs:1970:Sources hover text still says the picture waits for its soundThe last round's findings, at e100a3b:
rs/moq-scene/src/source.rs:1577:The program's sound gaps or loses content on every step of the jitter estimate;The base's quest/m0/pin-sync.md records the maintainer's 2026-11-12 decision to write each frame at the bus position `due` gives it and trim a late burst, which is exactly the per-jitter-step gap or skip the finding objects to, and rs/moq-scene/src/source.rs:1577 implements that settled rule.Head 555c4bf fixes the opencode round 1 low finding (
rs/moq-studio/src/ui.rs:1970, the delay tooltip now says the picture and sound share one clock); the earlier high finding stays refuted by the maintainer decision under## Decisions.