Repository navigation
Drop browser audio that arrives too late to mix - #7
Merged
Merged
Conversation
5 of 7 tasks
zackees
approved these changes
Oct 25, 2026
zackees
left a comment
Member
There was a problem hiding this comment.
Coordinator review: approved.
AudioTooLatefollows libobs's placement: smoothing within 70 ms snaps toaudio_next_ts, a gap of 3+ blocks counts as a restart, and anything more than 2 s off is left to OBS to re-anchor. It drops only packets that land behindaudio_mix_endand later than their own duration, plus a grace run for packets up to one block late, which still lets OBS buffer once for normal latency.- I checked that
blockuses OBS's rate:OnAudioStreamStartedsetssample_ratefromparams.sample_rate = audio_output_get_sample_rate(obs_get_audio()), so it matches the mixer's block length. - Lifetime: the raw callback is added in the constructor and removed in
Destroy(), which is the only free path (info.destroy).obs_remove_raw_audio_callbacktakes the callback lock, so no callback can run after it returns.audio_mix_endis atomic across the audio and CEF threads. - The measurements show the trade-off clearly (a longer browser gap during a stall, instead of permanent latency for every source). The behaviour change is spelled out in the description. RED → GREEN is documented.
Nit (non-blocking): the register in the constructor is unconditional, so sources without "Control audio via OBS" also take a per-block callback. That's cheap, but it could be skipped when reroute_audio is off.
tests/audio_buffering.py plays a WebAudio alert page (a 1 s tone every 5 s) through a real browser source with "Control audio via OBS", loads every CPU in bursts and requires that OBS's global audio buffering stays at its value from before the load, that packets keep flowing and that each tone starts in the mix within one audio block of its capture timestamp. On master a burst stalls the capture for up to 0.9 s; the backlog then arrives with its real timestamps and OBS raises the buffering of every source to fit it in, for good (106 ms to 874 ms in one burst). Refs obs-rust/obs-studio#2335
With "Control audio via OBS", CEF hands each packet over with its capture time on OBS's clock. Under load its delivery can stall for up to a second and then deliver the backlog at once. OBS places the backlog at its capture time, behind the block it is mixing, and raises its global audio buffering to fit it in. That buffering delays every source and never comes back down: one stall could add up to 960 ms of latency to the whole stream until OBS restarts. Drop such packets instead. Each source now tracks where OBS's mix is (a raw audio callback on mix 1), and OnAudioStreamPacket drops a packet that OBS would place by its timestamp (a new stream, a pause in delivery, a gap) behind the mixed block. Packets that continue the stream are appended by OBS whatever their timestamp and are kept, as are packets delivered within their own duration, so OBS still buffers once for the capture's normal latency. Kept packets keep their timestamps, so the browser audio stays in sync with video. Each run of dropped audio is logged. The cost: during a stall, the browser source is silent for about the length of the stall, instead of the whole stream's audio staying that much later for good. tests/audio_buffering.py: buffering stays at 21 ms through 13 load bursts and three delivery stalls (0.5, 0.9 and 0.3 s), every tone starts in the mix within one audio block of its capture timestamp. Fixes obs-rust/obs-studio#2335
zackees
force-pushed
the
fix/audio-stream-start
branch
from
October 25, 2026 07:15
2aa2b34 to
1ca296b
Compare
Member
Merged
6 of 7 tasks
chriscoveries
pushed a commit
to chriscoveries/obsrust-fork
that referenced
this pull request
Oct 25, 2026
…s) (obs-rust#2378) Moves plugins/obs-browser from 7bab463c9 to 4583599fe, with: - obs-rust/obs-browser#3: the CEF sandbox where it can work on Linux (obs-rust#1092) - obs-rust/obs-browser#5: os_event_init results checked in module load and ExecuteOnBrowser - obs-rust/obs-browser#6: WebAudio resumes after the audio service dies (obs-rust#2328) - obs-rust/obs-browser#7: browser audio that arrives too late to mix is dropped (obs-rust#2335) - obs-rust/obs-browser#8: the fallback warning when the sandbox can't work (obs-rust#2218) One bump instead of three (obs-rust#2333, obs-rust#2351 and ours), as agreed on obs-rust#2351. docs/features.md gets the two audio lines from obs-rust#2333 and obs-rust#2351 as they are, a line for the sandbox, and the browser on the fallback warning line, which obs-rust#2238 merged on 2026-10-25. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs obs-rust/obs-studio#2335
What and why
With "Control audio via OBS", CEF hands each audio packet over with its capture time, on OBS's clock. Under load, CEF's delivery to OBS can stall for hundreds of ms and then hand over the backlog all at once. During the stall the source's buffer runs short, and OBS treats it as stopped. When the backlog arrives, OBS places it at its capture time, which is behind the block it is mixing, and raises its global audio buffering to fit it in. That buffering applies to every source and never comes back down. A few stalls take a stream to the 960 ms maximum, and it stays there until OBS restarts.
The stalls come in the middle of a stream, not only at its start. In the runs that showed this, the first late packet was packet #14 or #24 of a stream. A page that plays a continuous tone hits the same rise as one that plays alerts.
The fix:
BrowserSourceregisters a raw audio callback on mix 1 that records the end of the block OBS mixed last. It is added in the constructor and removed inDestroy().OnAudioStreamPacketdrops a packet that OBS would place by its timestamp behind that point. OBS places a packet by its timestamp when it starts a stream, comes after a pause in delivery of at least 3 blocks, or leaves a gap beyond OBS's 70 ms timestamp smoothing. The check follows OBS's smoothing to find where OBS would put the packet.[obs-browser: '<source>'] Dropped N ms of audio that arrived too late to mix.Behaviour change
Recording measurements
Made with a scratch harness, not part of this PR. It used the real module and libobs, 150 s, isolated, load bursts as in the test, and two delivery stalls made by the harness: 0.5 s into alert 8 and 0.8 s into alert 20, each 0.3 s after the tone starts. It records MKV through
ffmpeg_muxerwith x264 and two AAC tracks: track 1 has the browser alert page, which shows white for its 1 s tone; track 2 has a reference source with a constant 1 kHz tone and a 100 ms beep every second, with a white video frame for each beep, both on OBS's clock. The analysis reads the MKV with ffmpeg.RED → GREEN
test: browser audio buffering under load(8f04654).tests/audio_buffering.pycompilesaudio-buffering.cagainst the real libobs. It playsaudio-buffering.html(WebAudio: a 1 s 440 Hz tone every 5 s, or--mode continuous) through a real browser source withreroute_audio. After 20 quiet seconds it loads every CPU in bursts: 32 busy processes that it starts and kills by PID, 4 s on and 6 s off, until 150 s.obs_source_output_audio, so it blocks the module's audio thread before libobs while holding no libobs lock. It drops the one packet the module had already passed, in both builds.Drop browser audio that arrives too late to mix(2aa2b34): passes in 5 of 5 alert runs and 1 continuous run. Buffering stays at 21 ms. Each run drops 1771 ms of late audio in 3 runs of up to 960 ms, and every tone (31 checked) starts in the mix within 1 ms of its capture timestamp, except one tone in one run that was 21.0 ms off. That was a one-block shift at a stream start with no drop nearby; libobs did the same once on master (+20.6 ms, no load), so the test allows one block.tests/harnesses it needs a CEF build and an X display. The command is intests/README.md.Notes
obs_add_raw_audio_callback,AUDIO_OUTPUT_FRAMES). CEF's packet timestamps are on the same monotonic clock asos_gettime_ns()on each platform. If they ever weren't, the 2 s check leaves the packets to OBS's own re-anchoring, as before.TS_SMOOTHING_THRESHOLD(70 ms) andMAX_TS_VAR(2 s) fromobs-internal.h, which plugins cannot include.master.