Skip to content

feat(scene)!: pin moq-mux's decoupled playout clock (moq-dev/moq#8272) - #144

Merged
kixelated merged 8 commits into
mainfrom
quest/m0/pin-sync
Nov 12, 2026
Merged

kixelated merged 8 commits into
mainfrom
quest/m0/pin-sync

Conversation

@kixelated

@kixelated kixelated commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Crosses the moq submodule from 79960a9dd to 644d48996 (moq-dev/moq#8272) and moves a broadcast source's playout onto moq_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

  • Pin: just upgrade-moq 644d48996, with Cargo.lock. bun.lock did 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 was rind 0.3.2 → 0.3.1, held back by rind-rcgen 0.14.3 and rind-rustls 0.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: the Speaker (which owned the clock's anchor, skipped bursts, and deferred reports), Place, heard, and SLACK are gone. What replaces them:
    • Hold wraps a sync::Track. It folds in arrivals, holds the audio's jitter target plus WRITE_LEAD (capped at MAX_DELAY), resets at a discontinuity, and sends on a watch whenever 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, because Writer trims what the bus has already mixed; dropped once it has fully played out.
  • rs/moq-scene/src/source.rs:
    • play_video and play_audio each hold the clock for their kind. Video shows whatever present says is due, with no wait for sound.
    • Audio recomputes each queued frame's moment from the current clock at write time, so the late part of a post-stall backlog is dropped or trimmed and the sound comes back on the clock.
    • A decoder Gap or End resets 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 at due(start), since playhead is gone.
  • Other upstream breaks this pin crosses:
    • moq-dev/moq#8269: encode::Producer::new takes a track. The studio names it .avc3 or .hev1 from the probed codec and fails on any other codec. with_track became new, and Exporter::new returns the config.
    • moq-dev/moq#8274: 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::heard is removed. Read clock.due(..) instead.
  • Feed::clock() returns moq_mux::sync::Consumer, not timeline::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

  • The audio's target on the clock is its jitter target plus WRITE_LEAD, capped at MAX_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.
  • A decoder Gap or End resets that track's hold, because the decoder reports a timeline that does not continue as a Gap. 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.
  • A layer's followed sound still rechecks another broadcast's clock every RECHECK (10 ms) while it waits, since that clock does not notify its readers. The broadcast's own tracks wake on the watch instead.

Validation

  • just ci passes: 305 tests. source::tests::a_broken_off_sound_keeps_its_tail fails with an immediate reset and passes with the deferred one. The new and rewritten tests ran 30 times under --stress-count 30 with no failures.
  • New test: 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 old heard gate would hold every picture during the stall.
  • New playout unit tests: the lead, trim, and drop rule; late arrivals not moving the schedule; and the MAX_DELAY cap.
  • Real surface:
    • Setup: a moq listener (built from the submodule) imported an ffmpeg MPEG-TS of testsrc (H.264) and a sine tone (Opus), with the audio cut for 3 s (aselect='not(between(t,15,18))'). The moq-studio binary (--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.
    • Result: video showed 30 pictures every second throughout, including the three seconds with no audio. Pictures were at most 24 ms late on the software renderer, and typically 2–6 ms late. Audio resumed at 50 frames per second, each written its lead ahead (0 late). The tune-in burst of 147 audio frames played from where the clock was, dropping what had already passed.
    • Also observed: with AAC audio, the broadcast source fails outright, on main too (screenshot of main's build showed failed). This is unrelated to this change.

Decisions

  • How the program's sound follows a jitter-target step (opencode round 2 finding, answered by the maintainer on 2026-11-12): ✅ keep the rule as decided, each audio frame is written at the bus position the clock gives it, so a target step costs one audible gap or skip and sound and picture stay exactly in sync (rejected: writing continuously and using due only 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

  • moq-dev/moq (for /quest-plan there): 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 review after 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 3

  • Score: 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 1

  • Score: 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:

    • Low, rs/moq-studio/src/ui.rs:1970: Sources hover text still says the picture waits for its sound
  • The last round's findings, at e100a3b:

    • Refuted: high, 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.

kixelated and others added 3 commits November 12, 2026 10:12
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>
@kixelated kixelated changed the title moq-mux's decoupled playout clock is pinned feat(scene)!: pin moq-mux's decoupled playout clock (moq-dev/moq#8272) Nov 12, 2026
kixelated and others added 2 commits November 12, 2026 10:46
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>
@kixelated
kixelated marked this pull request as ready for review November 12, 2026 21:22
@pullfrog

pullfrog Bot commented Nov 12, 2026

Copy link
Copy Markdown

Pullfrog's workflow isn't active on your default branch, so this run didn't start. .github/workflows/pullfrog.yml was deleted or disabled there, and Pullfrog couldn't restore it automatically — a direct push to that branch was refused. Merge the file back yourself, or re-add it in one click from the Pullfrog console and merge the change.

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Broadcast 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: Minimal · up to 7c50e

The change moves broadcast playout to a shared clock and updates the supporting publishing and upgrade-script code. No concrete merge-blocking defect was established. The author reports that CI and the new tests passed.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the feature scope (scene), a breaking change, and the moq pin with its upstream PR. The main change is moving broadcast playout onto moq_mux::sync's decoupled clock, and the pin is the…
Description check Passed The description covers the moq submodule pin, the move of broadcast-source playout onto moq_mux::sync, the upgrade script change, the playout and source rewrites, the watch.rs layout timing change, th…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the feature scope (scene), a breaking change, and the moq pin with its upstream PR. The main change is moving broadcast playout onto moq_mux::sync's decoupled clock, and the pin is the vehicle for that. The title is specific and relates directly to the changeset, though it leans on the upstream pin rather than the behavioral change (sound stalls no longer pause picture).

Full details: Description check

Explanation

The description covers the moq submodule pin, the move of broadcast-source playout onto moq_mux::sync, the upgrade script change, the playout and source rewrites, the watch.rs layout timing change, the README update, and the public API breaks. It also gives validation and review details. It is directly related to the changeset.

Finishing Touches

Simplify code

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

Clippy (1.99.1)

Clippy execution failed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 63c19f01-8135-4120-9be7-8d4d28e5eaa3
Commits

Reviewing files that changed from the base of the PR and between 5b6c2c8 and 7c50ed0.

Files ignored due to path filters: 1

  • Cargo.lock is excluded by !**/*.lock
Files selected for processing: 14

  • README.md
  • moq
  • quest/m0/pin-main.md
  • quest/m0/pin-sync.md
  • rs/moq-scene/src/playout.rs
  • rs/moq-scene/src/publish.rs
  • rs/moq-scene/src/source.rs
  • rs/moq-studio/src/engine.rs
  • rs/moq-studio/src/feed.rs
  • rs/moq-studio/src/replay.rs
  • rs/moq-studio/src/setup.rs
  • rs/moq-studio/src/ui.rs
  • rs/moq-studio/src/watch.rs
  • sh/upgrade-moq.sh
Files with no reviewable changes: 1

  • quest/m0/pin-sync.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@kixelated

Copy link
Copy Markdown
Contributor Author

@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)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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: 644d48996 is a commit on moq's main (0 ahead, behind only). The quest file is deleted and pin-main.md drops its Required link to it, as the convention expects.
  • playout.rs: Hold wraps a sync::Track and signals moved whenever the schedule shifts. set_target caps the audio target plus WRITE_LEAD at MAX_DELAY. follow now keeps a frame that is partly due, and Writer::write already trims the part the bus has mixed.
  • source.rs: video presents on present with no gate on the sound, and audio works out each frame's moment again when it writes it. An audio Gap or End freezes the moments of the queued tail before the reset that comes with the next frame. That matters because State::reset clears the anchor, and with no video anchor due would become None and drop the tail. Video resets as soon as it sees a gap.
  • watch.rs: take_due and the keyframe start now read only due, since playhead is gone.
  • Upstream API breaks: encode::Producer::new(track, ..) uses the shared publish::suffix table, Exporter::new now returns the config, and the studio calls mix.minus([strip]).
  • sh/upgrade-moq.sh: each pass now tries every drifted crate. Since drift() prints name@version, -p names one package exactly. The script only fails when a whole pass moves nothing.
  • Docs: the README.md broadcast-source paragraph and the Sources tooltip now describe the decoupled clock.

Check was still running when this review was submitted.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@kixelated
kixelated merged commit 187515e into main Nov 12, 2026
3 checks passed
@kixelated
kixelated deleted the quest/m0/pin-sync branch November 12, 2026 23:12
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.

1 participant