Repository navigation
fix(watch,play): audio snaps onto the clock past a 200 ms stretch - #8675
Conversation
Quest-Claim: 867c4e303d20d055
…lock The stretch setting now sizes only that threshold: the audio subscription, the native decode cap and hole fill reach the estimator's 2 s ceiling past the delay, and native video waits its own delay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the stretch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the clock arrives A target rise right after a snap left native AAC congestion 1.78 s ahead for 8.5 s; the threshold now holds both ways. 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>
…bance leaves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 20 minutes. View limit details
No actionable comments were generated in the recent review. WalkthroughThe changes set the default audio stretch to 200 ms and separate subscription reach from stretch. Audio rings now skip trailing audio onto the clock and pad silence when audio leads the clock beyond the applicable threshold. The CLI and browser documentation and tests reflect these playback rules. The audio-quality analyzer now measures recovery against recent resting lag, and the related measurement budgets have changed. Merge Risk:
|
There was a problem hiding this comment.
ℹ️ No critical issues — one rough edge inline.
Reviewed changes
Reviewed the full diff: the 200 ms snap both ways in both rings, the subscription reach decoupled from stretch, and the harness's new recover_ms.
- 200 ms default:
STRETCHandmoq play --stretchnow default to 200 ms; zero still turns the stretch off. - Skip onto the clock:
ring-buffer.tsreadandring.rsskiptrigger pastmax(bound, window)and skip the whole lag, not back to the bound. - Pad when ahead: both rings now pad silence past
max(bound, window)with the stretch on, holding until the clock arrives; Rust gates it onclockedinstead of!stretching(). - Reach decoupled:
latency.tsmaxDelayandargs.rsmax_delayuse a local 2 sREACH,video_max_delayis gone, and native video subscribes at its own delay. - Harness:
longestAwaybecomesrecovery(), which measures against the median lag of the 5 s before each disturbance and seeds from the warmup; capture, probe, and ptt dropstretch. - Budgets and docs: replay rows re-measured, congestion trim ceilings raised (provisional), and docs, rustdoc, and quests updated to the new rule.
claude-opus-5-5 | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@pullfrog review this pull request and submit an approving review if it is ready to merge; the maintainer approved merging it. (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
ℹ️ Not approving yet: one earlier Pullfrog thread is still open and unaddressed. Nothing else came up.
Reviewed changes
Reviewed the full diff: the stretch threshold becomes a 200 ms snap in both rings, the audio subscription and native budgets no longer depend on it, and the audio-quality harness gains a disturbance-anchored recover_ms.
- 200 ms default:
STRETCH,<moq-watch stretch>, andmoq play --stretchnow default to 200 ms, with the docs, READMEs, and rustdoc updated to match. - Skip onto the clock: on the clock,
ring-buffer.tsreadandring.rsskipskip the whole lag once it passesmax(bound, window). - Pad when ahead: both rings pad silence once the lead passes
max(bound, window), whether the stretch is on or off, and pad until the lead reaches zero. JS readsthis.#boundand Rust readsinputs.bound, and each is that ring's own stretch bound. stretchsizes nothing else:latency.tsmaxDelayandmoq play'smax_delay/set_max_delayloop now reachdelay + 2 s(REACH).bound()is nowmax(stretch, lookahead).video_max_delayis gone, so native video waits onlyvideo_delay().- Harness:
recovery()replaceslongestAway. It times each disturbance until audio is back within one codec frame of its median lag over the 5 s before, leaves earlier disturbances out of that median, and takes the starting median from the warmup. - Budgets and quests: the replay rows are re-measured and the congestion rows' trim ceilings are raised provisionally, with the note explaining both. The finished quest file and its links are removed.
Rust and JS match on the skip and pad thresholds, on the join (a zero bound before the first clocked read), and in their tests.
ℹ️ The open thread on the unclocked native skip is still unaddressed
The earlier thread on rs/moq-audio/src/ring.rs still holds at head 2155336. The unclocked branch now skips once lag > max(bound, window), which is just bound, where it used to wait for bound + window. Each skip is still lag - bound, so this ring can now skip a few frames on almost every callback. That contradicts Decision 3's claim that the unclocked ring keeps its old behavior. Either apply the suggestion in that thread or reply there with why the new threshold is intended. Then resolve the thread and this PR can be approved.
claude-opus-5-5 | 𝕏
Restores main's unclocked threshold, which the snap onto the clock had narrowed to the bound alone, so a level hovering there skipped a few frames nearly every callback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@pullfrog review this pull request and submit an approving review if it is ready to merge (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
✅ No new issues found. The open thread on the unclocked skip is addressed.
Reviewed changes
Reviewed dbfd700, the only commit since the prior review at 2155336.
- Unclocked skip threshold restored:
ring.rsskipnow splits onclocked. On the clock it still skips the whole lag once it passesmax(bound, window). Without a clock it waits forbound + windowand skips back to the bound, as onmain, so a level hovering at the bound no longer skips a few frames every callback. - Regression test:
without_a_clock_only_a_window_past_the_bound_skipschecks that half a window past the bound plays on, and that two windows past skips once back to within one read of the bound. - Rustdoc:
skip's doc comment now describes the unclocked rule.
claude-opus-5-5 | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
I reviewed the whole branch diff: the 200 ms snap in both rings, the subscription and native budgets no longer sized by stretch, the reworked recover_ms analyzer, the re-measured budgets, and the docs and quest updates.
- Default threshold:
STRETCHandmoq play --stretchnow default to 200 ms. Zero still turns the stretch off, and a buffered player still uses the larger of the stretch and its lookahead (latency.tsbound). - Skip onto the clock: trailing audio now skips when
lag > max(bound, window)and lands on the clock. Before, it skipped back to the bound.ring-buffer.tsreadandring.rsskipagree, and only the unclocked native ring keeps the old rule of skipping back to the bound once it is a window past it. - Pad ahead with the stretch on:
padnow applies at the same threshold whenever the ring is clocked, where before it applied only with the stretch off. Withstretch0 the threshold is one window, the same as before. stretchno longer sizes the subscription:maxDelayand nativemax_delayreach the delay plus a 2 sREACH. The native decode cap andfill_maxfollow that, andvideo_max_delayis removed, so native video now waits only its own delay.recover_ms:recovery()reads where audio rested from the median lag over the 5 s before each disturbance, leaving earlier disturbances out. The tolerance is one codec frame and both directions count. The warmup sets the rest for a disturbance at the start of the window.- Budgets and docs: the replay rows are re-measured exactly, and the congestion trim ceilings are raised and marked provisional. The concept doc, rustdoc, README, and element docs describe the new rule.
claude-opus-5-5 | 𝕏
…stall rule together Both #8675 and this branch re-measured the relay-mic-stall replay row for their own change alone; the row now holds what the two measure together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Finishes quest/m2/audio-playout/congestion-catch-up.md, the first step of the audio playout epic planned in moq-dev/moq#8663.
Summary
After a congestion stall, audio is back on the shared clock about 2.5 s after the outage starts, where it used to trail by up to 1.4 s for 15 to 26 s. This holds in the browser and natively.
stretchnow defaults to 200 ms (js/watchSTRETCH,<moq-watch stretch>,moq play --stretch). Zero still turns the stretch off. A buffered player still uses the larger of its lookahead and the threshold.ring-buffer.tsread,ring.rsskip). The test is now "past the threshold, or past one window when that is larger", where it used to be "threshold plus a window".stretchwas 0 (ring-buffer.ts,ring.rspad). Without this, native AAC congestion snapped onto the clock at 27.0 s, the target then rose 243 → 2023 ms, and audio sat 1.78 s ahead for 8.5 s.stretchsizes nothing else:latency.tsmaxDelay,moq play'sREACH).fill_max) follow that reach.video_max_delayis gone.--delay auto --stretch 0, which cut the budget to 50 ms and dropped the sink on any hole longer than that.recover_msnow runs from a disturbance to audio back within one codec frame of where it rested in the 5 s before (that rest leaves out earlier disturbances), in either direction. Before, it compared against the window's p50 and wasn't tied to any disturbance. The capture no longer takes astretch.doc/concept/audio-jitter.md's observation point and skip rules,doc/lib/js/watch.md,doc/bin/cli.md,js/watch/README.md, and the rustdoc all describe the new rule.Decisions
Taken without asking (headless):
lag > max(bound, window), where it wasbound + window. The threshold is the threshold, and a zero bound still waits one window.latency.tsREACH,play/args.rsREACH), each documented as the estimator'sBUCKET * BUCKETS. Exporting it from the estimator would add public API in@moq/hangandmoq-audiofor one consumer each.bus::MAX_STRETCHstays 2 s. It caps a positioned channel's bound and was never the players' default.recover_ms: the rest is read over 5 s, the tolerance is one codec frame (Opus 20 ms, AAC 1024 samples), it counts both directions, and it leaves earlier disturbances out of the rest. A row whose resting point moves for good, like native Opusstep, never "returns" and reads to the end of the window, the same onmain.trimmed_ms_per_min(and native Opus skips): raised by the note's rule (worst run × 1.25 + one hiccup) from two local runs, marked provisional until Nightly re-measures.Validation
just checkpasses on the branch, includingmoq-audio,@moq/watch, the audio-quality client, and thejust rs playtests (277).without_a_clock_only_a_window_past_the_bound_skips). It failed with a 720-frame skip before the unclocked threshold was restored.stretchmoves (200 ms → 1.5 s → 0).recovery()cases.just test audio-quality, one 60 s run per row on a loaded x86_64 Linux developer host (debug build, seed 7),main(d6195e8) against this branch. Both sides used this branch's analyzer sorecover_mscompares like with like.main. It is the output latency that output latency owns.--replays --enforce) pass against the re-measured budgets:relay-mic-stallA/V lag p95 484.9 → 118.6 ms. It now skips the outage's trail and the target falls, and pads the rise.rtt40skips its 320 → 80 ms target fall once.bursty-aacskips a 225 ms trail once.Before and after rows
chromium (main → this branch)
native (main → this branch)
Public API
Behavior only, no
!:stretchdefaults to 200 ms in@moq/watch(Sync,Player,<moq-watch>) andmoq play --stretch.stretch.moq_audio::playback::Sink::duekeeps its signature; only its documented skip and pad behavior changes.Wire: none.
Caused by
stretchdefault, which also sized the subscription, the native decode cap, and native video's waitFollow-ups
None.
Models
Size: M
Implementer: Claude, claude-opus-5-5, unknown effort
Implementer: Claude Code, claude-opus-5-5, unknown effort (iterate, XS)
Review
Rewritten by
quest reviewafter each round.Head: dbfd700
Base: origin/main at 6f2f3ff
Reviewer: Codex, gpt-6.1-sol, high effort
Session:
01a1d82e-bb6a-7b61-8049-4270ea0e68b0, round 1Score: 9/10,
Code inspection found no actionable regressions in the reviewed branch diff; builds and tests were not run, as instructed.Rounds: 1; findings 0: fixed 0, refuted 0, open 0
Findings: none