Summary
FAO: @Falco ![]()
AI disclosure: This bug was investigated agentically and the cause and remedy were suggested by Opus 5.5.
In Brave Browser, turning on background blur in a Live room always shows an error toast: “Background blur couldn’t be started, so it was turned off.” The same user, forum and room work fine in Google Chrome on the same machine.

The cause is the hidden <video> that BackgroundBlurManager#setup() creates but never inserts into the document. Brave rejects play() on it with:
AbortError: The play() request was interrupted because the media was removed from the document.
Inserting the element into the document (invisibly) before play() makes it work in Brave.
Environment
- Discourse
2026.10.0-latest.1(568a006ad8), bundled Live plugin,discourse_voice_assets0.2.0 - Brave (Chromium 155) on Linux, Wayland, Intel GPU; WebGL2 and WebGPU hardware accelerated
- Google Chrome on the same machine: works
Steps to reproduce
- Open a Live room in Brave and turn the camera on.
- Enable background blur.
- The toast appears and blur is switched off.
Diagnosis
I ran the plugin’s own blur pipeline step by step in the Brave console (same asset path, model, delegate, and running mode as background-blur.js), with Brave’s fingerprinting protection on and then off:
| Step | Brave, fingerprinting protection on | Brave, fingerprinting protection off |
|---|---|---|
vision_bundle.js import, WASM load |
ok | ok |
ImageSegmenter.createFromOptions, GPU and CPU delegates |
ok | ok |
getUserMedia({ video: true }), track state |
ok, live |
ok, live |
play() on a detached muted <video> with the camera stream (what the plugin does) |
AbortError | AbortError |
play() on the same kind of <video> appended to document.body (1px, transparent) |
ok | ok |
So MediaPipe, WebGL and the camera are all fine. Only the detached play() fails, and Brave’s fingerprinting protection makes no difference. Headless Chromium plays a detached <video> without error, which matches Chrome working.
Suggested fix
In plugins/live/assets/javascripts/discourse/lib/live/background-blur.js, setup(): append the hidden video to the document before video.play(), and remove it in teardown(). For example:
video.setAttribute("aria-hidden", "true");
video.style.cssText =
"position:fixed;width:1px;height:1px;opacity:0;pointer-events:none";
document.body.appendChild(video);
// ... await video.play() as now ...
// teardown():
this.#video?.remove();
As far as I can see, this is the only place in the plugin that plays a detached media element.
Two related observations
-
The real error is swallowed.
#createBackgroundBlur()inlocal-video-manager.jsuses a barecatch {}and callsliveLog.warn("[live] failed to start background blur")without the error. Even withlive_verbose_loggingenabled, the console never says why blur failed. Passing the caught error toliveLog.warn(which already sanitises it) would have msurfaced the error more rapidly. -
Blur is offered where it can never work.
BackgroundBlurManager.isSupported()checks onlycanvas.captureStreamandWebAssembly. In a browser without WebGL, MediaPipe’s segmenter fails with both the GPU and CPU delegates (Service "kGpuService" ... cannot be created: emscripten_webgl_create_context() returned error 0), because the segmentation post-processor needs a GL context either way. Adding a WebGL2 check toisSupported()would hide or disable the blur control instead of failing after the user turns it on. (Reproduced in headless Chromium by stubbinggetContext("webgl*"), not seen in the wild.)
Thanks for Live - apart from this, it’s working nicely for us.