Skip to content

Resume WebAudio after the audio service dies - #6

Merged
zackees merged 2 commits into
obs-rust:masterfrom
rdoupe:fix/audiocontext-resume
Oct 25, 2026
Merged

zackees merged 2 commits into
obs-rust:masterfrom
rdoupe:fix/audiocontext-resume

Conversation

@rdoupe

@rdoupe rdoupe commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Refs obs-rust/obs-studio#2328 (upstream obsproject/obs-studio#10586)

What and why

When Chromium's audio service process dies (a crash, a device change, or on Windows the audio hang monitor ending it after 3 minutes), every running AudioContext fires error and is suspended. Chromium never resumes it, so a browser source that plays through WebAudio (alert boxes) stays silent until the page reloads or "Control audio via OBS" is toggled. Pages that use new Audio() recover by themselves.

BrowserApp::OnContextCreated now evaluates a small script in every frame, before the page's own scripts, next to the obsstudio bindings:

  • AudioContext (and webkitAudioContext where present) is wrapped in a Proxy, so pages still see the native constructor, prototype, name, length, property descriptors and [native code]; instanceof, subclassing and ctx.constructor === AudioContext behave as before.
  • On a context's error event, a context the page wanted running is resumed. One the page suspended itself (suspend() is wrapped, the same way, to track that) is left alone.
  • OfflineAudioContext is untouched.
  • The script only uses functions it captured before the page ran, so pages that replace or freeze Reflect, Promise.prototype.then, EventTarget.prototype.addEventListener, WeakMap, Function.prototype.call, setTimeout or AudioContext.prototype.resume do not break it.
  • The first resume waits 3 s. With "Control audio via OBS", CEF keeps the capture stream of the dead audio service until the page has been silent for 2 s (kRecentlyAudibleTimeout in AlloyBrowserHostImpl), and only starts a new one after that; a context resumed sooner plays into the dead stream for as long as it stays audible. Later attempts retry every 1 to 2 s, and a new error starts a new chain.

RED → GREEN

  • RED commit test: WebAudio after the audio service dies (9e1be0d): tests/audio_recovery.py plays a local WebAudio tone through real browser sources with reroute_audio, SIGKILLs only this run's audio service process (found through the harness's process tree) and requires packets in the 5th second after the kill. On master all 4 scenarios fail with 0 packets: plain, repeated (3 kills, on a page that replaces and freezes globals), page-suspend, two-sources.
  • GREEN Resume WebAudio after the audio service dies (654d2b9): 4 of 4 pass; audio is back 3.1 to 3.2 s after every kill. In page-suspend the context the page suspended stays suspended; plain also checks that the page sees an unchanged AudioContext (passes before and after).
  • Runs: Linux, CEF 8078, isolated (loopback-only network namespace, private Xvfb and dbus, fresh HOME, no audio server). clang-format 22.1.3 clean.
  • CI: the test is not run in CI. Like the other tests/ harnesses it needs a CEF build and an X display, and it kills a Chromium process. See tests/README.md for the command.

Notes

  • Verified on Linux only. The script is platform independent; the 3 s delay is tied to CEF's 2 s audible timeout, which is the same on all platforms.
  • Sources without "Control audio via OBS" get the same resume (there is no capture stream to wait for; the delay is harmless there).

When Chromium's audio service process dies, a page's AudioContext fires
error and is suspended. Nothing resumes it, so a browser source with
"Control audio via OBS" delivers no audio until the page reloads.

tests/audio_recovery.py plays a local WebAudio tone through real browser
sources, SIGKILLs only this run's audio service process and requires
packets again in the fifth second after the kill: one kill, three kills
on a page that replaces and freezes globals, a context the page
suspended itself (must stay suspended), and two sources.

All four scenarios fail on master: 0 packets after the kill.

Refs obs-rust/obs-studio#2328
When Chromium's audio service process dies (a crash, or on Windows the
hang monitor ending it), every running AudioContext fires "error" and
is suspended. Nothing resumes it, so the source stays silent until the
page reloads (obsproject/obs-studio#10586).

OnContextCreated now runs a script in every frame before the page's
own scripts. It wraps AudioContext (and webkitAudioContext, where
present) in a Proxy, so pages still see the native constructor,
prototype, name and methods, and watches each context's error event. A
context the page wanted running is resumed; one the page suspended
itself is left alone (suspend() is wrapped to track that).
OfflineAudioContext is untouched. The script uses only what it
captured before the page ran, so pages that replace or freeze globals
do not break it.

The first resume waits 3 s: with "Control audio via OBS", CEF keeps the
dead capture stream until the page has been silent for 2 s, and a
context resumed sooner keeps playing into it. Later attempts retry
every 1 to 2 s.

tests/audio_recovery.py: 4 of 4 scenarios pass; audio is back 3.1 to
3.2 s after each kill.

Fixes obs-rust/obs-studio#2328

@zackees zackees left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Coordinator review: approved.

  • The injected script uses only functions it captured before the page runs (Reflect, Promise#then, addEventListener, WeakMap/Map methods, setTimeout), so pages that replace or freeze globals can't break it. Object spread in the descriptor copies can't be hooked by the page either.
  • A suspend the page started itself turns off the resume through wanted/pendingSuspends; closed ends a chain; each error starts a new chain and ends the old one. That covers the repeated-kill case.
  • constructor and suspend are replaced only when the descriptor still holds the native value, and aliases share one proxy through wrapped. instanceof and identity hold.
  • An Eval failure leaves the page with the native AudioContext. RED → GREEN is documented, and the harness lives in tests/ like the others; it can't run in CI (needs CEF and X).

@zackees
zackees merged commit 4b88708 into obs-rust:master Oct 25, 2026
1 check passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants