Skip to content

fix(watch,play): audio snaps onto the clock past a 200 ms stretch - #8675

Merged
kixelated merged 13 commits into
mainfrom
quest/m2/audio-playout/congestion-catch-up
Nov 14, 2026
Merged

kixelated merged 13 commits into
mainfrom
quest/m2/audio-playout/congestion-catch-up

Conversation

@kixelated

@kixelated kixelated commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

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.

  • Threshold: stretch now defaults to 200 ms (js/watch STRETCH, <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.
  • Snap onto the clock: audio trailing the clock past the threshold now skips onto the clock instead of back to the bound (ring-buffer.ts read, ring.rs skip). The test is now "past the threshold, or past one window when that is larger", where it used to be "threshold plus a window".
  • Snap both ways: audio leading the clock past the threshold now pads silence until the clock reaches it, with the stretch on too. Before, it padded only when stretch was 0 (ring-buffer.ts, ring.rs pad). 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.
  • stretch sizes nothing else:
    • The audio subscription reaches the estimator's 2 s ceiling past the delay, whatever the stretch (latency.ts maxDelay, moq play's REACH).
    • Natively, the decode cap and the hole fill (fill_max) follow that reach.
    • Native video waits its own delay on a missing group. video_max_delay is gone.
    • This also fixes --delay auto --stretch 0, which cut the budget to 50 ms and dropped the sink on any hole longer than that.
  • Harness: recover_ms now 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 a stretch.
  • Spec and docs: 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.
  • Quests: deletes the quest file and its links, and notes in lag rule's Plan that both rings already snap both ways.

Decisions

Taken without asking (headless):

  1. Padding ahead is in scope. The epic's Plan sets the 200 ms threshold "both ways", but the quest's Goal names only the trailing skip. The measured native AAC regression above says the two halves have to land together, so this PR adds the pad instead of leaving it to the lag rule, which keeps it.
  2. Skip test on the clock: lag > max(bound, window), where it was bound + window. The threshold is the threshold, and a zero bound still waits one window.
  3. Unclocked native ring keeps its old rule: past its bound (three quarters of the ring's headroom) plus one window, it skips back to the bound. With no clock there is nothing to land on, and the window of slack keeps a level hovering at the bound from skipping a few frames every callback.
  4. The 2 s reach is a local constant in each player (latency.ts REACH, play/args.rs REACH), each documented as the estimator's BUCKET * BUCKETS. Exporting it from the estimator would add public API in @moq/hang and moq-audio for one consumer each.
  5. bus::MAX_STRETCH stays 2 s. It caps a positioned channel's bound and was never the players' default.
  6. 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 Opus step, never "returns" and reads to the end of the window, the same on main.
  7. Budgets:
    • Replay rows: re-measured exactly, with the reasons in the note.
    • Congestion 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.
    • The note says a rise in trimmed audio "is a regression to fix rather than budget". Here the trim is the snap the epic decided, so I budgeted it and said so in the note.

Validation

  • just check passes on the branch, including moq-audio, @moq/watch, the audio-quality client, and the just rs play tests (277).
  • New and updated unit tests in both languages:
    • Without a clock, a level half a window past the bound plays on and two windows past skips back to the bound (without_a_clock_only_a_window_past_the_bound_skips). It failed with a 720-frame skip before the unclocked threshold was restored.
    • A skip lands on the clock past the threshold and not before it.
    • A playhead ahead past the bound pads until the clock arrives and drops nothing; within the bound it keeps playing.
    • The subscription's max delay stays at delay + 2 s as stretch moves (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 so recover_ms compares like with like.
    • Congestion rows, A/V lag p95 / time to return:
      • Chromium AAC: 1396.8 → 61.6 ms / 26.3 → 2.5 s
      • Chromium Opus: 1424.8 → 64.3 ms / 20.3 → 2.8 s
      • Native AAC: 1246.3 → 116.7 ms / 14.5 → 2.5 s
      • Native Opus: 826.9 → 117.5 ms / 24.0 → 3.8 s
    • The other rows move within single-run noise. The chromium A/V lag p50 of about 30 ms on every row, which fails its ceilings, is the same on main. It is the output latency that output latency owns.
  • Replays (--replays --enforce) pass against the re-measured budgets:
    • relay-mic-stall A/V lag p95 484.9 → 118.6 ms. It now skips the outage's trail and the target falls, and pads the rise.
    • rtt40 skips its 320 → 80 ms target fall once.
    • bursty-aac skips a 225 ms trail once.
Before and after rows

chromium (main → this branch)

row skips/min trimmed ms/min underruns/min stalled A/V lag p50 A/V lag p95 recover ms target p95
aac-44100-bursty 1.1 → 0 36.3 → 0 0 0 56.3 → 61.6 82.1 → 86.8 750.9 → 1501.4 343.2 → 323.2
aac-44100-congestion 1.1 → 3.3 114.3 → 2498.7 0 0.041 → 0.05 55.5 → 29.8 1396.8 → 61.6 26268.1 → 2505.7 643.2 → 663.2
aac-44100-fixed-250 0 0 0 0 28.5 → 27.7 48.5 → 48.2 501.6 → 500.4 250
aac-44100-mild 0 0 5.5 → 0 0 27.7 → 28.8 48.6 → 49.1 500.5 → 500.6 203.2 → 186.2
aac-44100-near-zero 0 0 1.1 → 0 0 29.5 → 28.1 52.4 → 48.5 1530.8 → 500.8 223.2 → 203.2
aac-44100-step 0 0 5.5 → 2.2 0.005 → 0 33.3 → 31.9 58.2 → 54.5 1001 → 750.6 223.2
aac-44100-wide 0 0 0 → 6.5 0 → 0.005 33.5 → 39.3 56.2 → 69.7 750.5 → 1751.3 283.2
opus-48000-bursty 0 0 0 0 36.1 → 35.4 50.9 → 53.3 750.8 → 1000.8 160 → 240
opus-48000-congestion 0 → 5.5 0 → 2697.2 0 0.041 → 0.05 46.8 → 34.5 1424.8 → 64.3 20262.4 → 2751.7 600 → 560
opus-48000-fixed-250 0 0 0 0 36.1 → 32.2 53.2 → 52.7 752.1 → 751.1 250
opus-48000-mild 0 0 0 0 32.7 → 33.6 54.7 → 55.9 501.3 → 750.4 80
opus-48000-near-zero 0 0 0 0 33.3 → 34.6 56.2 → 57.8 752.2 → 1001.4 60 → 120
opus-48000-step 0 0 10.9 → 7.6 0 40.2 → 42.8 67.7 → 69.7 1252.7 → 1000.5 100
opus-48000-wide 0 0 0 0 43.2 → 40.2 66.9 → 69.4 751 → 2503.4 280 → 360

native (main → this branch)

row skips/min trimmed ms/min underruns/min stalled A/V lag p50 A/V lag p95 recover ms target p95
aac-44100-bursty 0 0 0 0 50.1 → 46.1 58.8 → 53.1 4249.8 → 0 343.2 → 323.2
aac-44100-congestion 4.3 → 7.6 271.7 → 2870.4 912.4 → 961 0 -4.8 → -4.2 1246.3 → 116.7 14499.9 → 2500.2 2023.2 → 923.2
aac-44100-fixed-250 0 0 0 0 -6.9 → -9.5 -1.5 → -4.2 0 250
aac-44100-mild 0 0 0 0 -17.3 → -11.4 -11.3 → -5.4 0 186.2
aac-44100-near-zero 0 0 7.6 → 0 0 -8.7 → -11.8 -2.4 → -5.1 0 186.2
aac-44100-step 0 0 11.9 → 22.8 0 -7 → -13.4 32.9 → 14.8 13750.6 → 499.7 223.2
aac-44100-wide 0 0 4.3 → 0 0 25 → 14.6 35.1 → 20.6 0 283.2 → 263.2
opus-48000-bursty 0 0 16.3 → 0 0 -4.6 → -0.9 2 → 2.5 0 140 → 180
opus-48000-congestion 2.2 → 7.6 403.3 → 3532.3 995.8 → 1096.8 0 -0.6 → -3.2 826.9 → 117.5 24000.6 → 3750.1 1040 → 820
opus-48000-fixed-250 0 0 0 0 -7.3 → -6.4 -2.5 → -1.7 0 250
opus-48000-mild 0 0 0 0 -0.6 → 0 5.1 → 8.1 0 → 249.2 60 → 80
opus-48000-near-zero 6.5 → 0 108.1 → 0 32.4 → 0 0 -4.9 → -4.6 5.8 → -2.6 501 → 0 100 → 60
opus-48000-step 0 0 25 → 39.1 0 30.4 → 24.8 54.7 → 50.7 30505.5 → 30504.2 100
opus-48000-wide 0 0 0 0 28.3 → 19.8 39.3 → 25.8 249.2 → 1499.5 200 → 180

Public API

Behavior only, no !:

  • stretch defaults to 200 ms in @moq/watch (Sync, Player, <moq-watch>) and moq play --stretch.
  • Past the threshold, audio skips onto the clock and pads when ahead.
  • The audio subscription no longer follows stretch.
  • moq_audio::playback::Sink::due keeps its signature; only its documented skip and pad behavior changes.

Wire: none.

Caused by

  • moq-dev/moq#8507 - stopped trimming audio that trails the clock, so a stall's backlog was caught up at the accelerate's pace
  • moq-dev/moq#8532 - bounded that trail at a 2 s stretch default, which also sized the subscription, the native decode cap, and native video's wait

Follow-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 review after each round.

  • Head: dbfd700

  • Base: origin/main at 6f2f3ff

  • Reviewer: Codex, gpt-6.1-sol, high effort

  • Session: 01a1d82e-bb6a-7b61-8049-4270ea0e68b0, round 1

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

kixelated and others added 9 commits November 13, 2026 13:47
…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>
@kixelated kixelated changed the title Audio recovers from a congestion stall quickly fix(watch,play): audio snaps onto the clock past a 200 ms stretch Nov 13, 2026
kixelated and others added 2 commits November 13, 2026 15:55
…bance leaves

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 14, 2026 00:01
@coderabbitai

coderabbitai Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

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: 1e5640c1-2f1e-4803-8e07-8af643dbcbf7
Commits

Reviewing files that changed from the base of the PR and between 2155336 and dbfd700.

Files selected for processing: 1

  • rs/moq-audio/src/ring.rs

No actionable comments were generated in the recent review.

Walkthrough

The 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: Minimal · previous review 21553

This change sets the default audio stretch to 200 ms and makes audio skip onto the clock or pad silence past that threshold. The subscription reach stays at 2 s past the delay. The supplied summaries show consistent code, tests, docs and budgets, and no remaining merge-blocking risk was identified.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title "fix(watch,play): audio snaps onto the clock past a 200 ms stretch" matches the main change. The PR sets the default stretch to 200 ms in js/watch and moq play. Audio that trails the clock p…
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.
Description check Passed The description is specific and on-topic. It explains the congestion catch-up quest, the 200 ms stretch default, the skip-onto-clock and pad-when-ahead rules, the decoupling of the audio subscription …
Full details: Title check

Explanation

The title "fix(watch,play): audio snaps onto the clock past a 200 ms stretch" matches the main change. The PR sets the default stretch to 200 ms in js/watch and moq play. Audio that trails the clock past the threshold now skips onto the clock, and audio that leads it pads silence. The title names the skip behavior and the stretch threshold, and it covers both packages the changes touch.

Full details: Description check

Explanation

The description is specific and on-topic. It explains the congestion catch-up quest, the 200 ms stretch default, the skip-onto-clock and pad-when-ahead rules, the decoupling of the audio subscription from stretch, the recover_ms harness change, the budget updates, and the validation runs. It matches the changeset summaries for the docs, watch, audio, CLI, and audio-quality harness files.

Finishing Touches

Simplify code

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

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: f7f93546-dfd4-410b-8aa9-bb8a5c7c25b9
Commits

Reviewing files that changed from the base of the PR and between 9c4b4df and 2155336.

Files selected for processing: 33

  • doc/bin/cli.md
  • doc/concept/audio-jitter.md
  • doc/lib/js/watch.md
  • js/watch/README.md
  • js/watch/src/audio/decoder.test.ts
  • js/watch/src/audio/decoder.ts
  • js/watch/src/audio/latency.ts
  • js/watch/src/audio/playout.test.ts
  • js/watch/src/audio/replay.ts
  • js/watch/src/audio/ring-buffer.test.ts
  • js/watch/src/audio/ring-buffer.ts
  • js/watch/src/element.ts
  • js/watch/src/player.ts
  • js/watch/src/sync.ts
  • quest/m0/nightly/README.md
  • quest/m2/audio-playout/README.md
  • quest/m2/audio-playout/congestion-catch-up.md
  • quest/m2/audio-playout/lag-rule.md
  • rs/moq-audio/src/bus.rs
  • rs/moq-audio/src/playback/sink.rs
  • rs/moq-audio/src/ring.rs
  • rs/moq-cli/src/play/args.rs
  • rs/moq-cli/src/play/headless.rs
  • rs/moq-cli/src/play/media.rs
  • test/audio-quality/README.md
  • test/audio-quality/budgets.json
  • test/audio-quality/clients/js/src/analyze.test.ts
  • test/audio-quality/clients/js/src/analyze.ts
  • test/audio-quality/clients/js/src/capture.test.ts
  • test/audio-quality/clients/js/src/capture.ts
  • test/audio-quality/clients/js/src/probe.ts
  • test/audio-quality/clients/js/src/ptt.ts
  • test/audio-quality/clients/js/src/schema.ts
Files with no reviewable changes: 4

  • quest/m2/audio-playout/congestion-catch-up.md
  • test/audio-quality/clients/js/src/capture.ts
  • test/audio-quality/clients/js/src/probe.ts
  • test/audio-quality/clients/js/src/ptt.ts

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ 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: STRETCH and moq play --stretch now default to 200 ms; zero still turns the stretch off.
  • Skip onto the clock: ring-buffer.ts read and ring.rs skip trigger past max(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 on clocked instead of !stretching().
  • Reach decoupled: latency.ts maxDelay and args.rs max_delay use a local 2 s REACH, video_max_delay is gone, and native video subscribes at its own delay.
  • Harness: longestAway becomes recovery(), which measures against the median lag of the 5 s before each disturbance and seeds from the warmup; capture, probe, and ptt drop stretch.
  • Budgets and docs: replay rows re-measured, congestion trim ceilings raised (provisional), and docs, rustdoc, and quests updated to the new rule.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread rs/moq-audio/src/ring.rs Resolved
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

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

@kixelated
kixelated enabled auto-merge November 14, 2026 02:36

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ 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>, and moq play --stretch now default to 200 ms, with the docs, READMEs, and rustdoc updated to match.
  • Skip onto the clock: on the clock, ring-buffer.ts read and ring.rs skip skip the whole lag once it passes max(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 reads this.#bound and Rust reads inputs.bound, and each is that ring's own stretch bound.
  • stretch sizes nothing else: latency.ts maxDelay and moq play's max_delay/set_max_delay loop now reach delay + 2 s (REACH). bound() is now max(stretch, lookahead). video_max_delay is gone, so native video waits only video_delay().
  • Harness: recovery() replaces longestAway. 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.

Pullfrog  | Fix it ➔ | View workflow run | Using 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>
@kixelated
kixelated disabled auto-merge November 14, 2026 03:50
@kixelated

Copy link
Copy Markdown
Collaborator Author

@pullfrog review this pull request and submit an approving review if it is ready to merge

(Written by Claude Opus 5.5)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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.rs skip now splits on clocked. On the clock it still skips the whole lag once it passes max(bound, window). Without a clock it waits for bound + window and skips back to the bound, as on main, 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_skips checks 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.

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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

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 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: STRETCH and moq play --stretch now 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.ts bound).
  • 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.ts read and ring.rs skip agree, 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: pad now applies at the same threshold whenever the ring is clocked, where before it applied only with the stretch off. With stretch 0 the threshold is one window, the same as before.
  • stretch no longer sizes the subscription: maxDelay and native max_delay reach the delay plus a 2 s REACH. The native decode cap and fill_max follow that, and video_max_delay is 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.

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

@kixelated
kixelated added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 97a7267 Nov 14, 2026
5 checks passed
@kixelated
kixelated deleted the quest/m2/audio-playout/congestion-catch-up branch November 14, 2026 05:29
kixelated added a commit that referenced this pull request Nov 14, 2026
…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>
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