Live: background blur always fails in Brave Browser ("Background blur couldn't be started") - detached `<video>`.play() is aborted

AI-generated summary

pacharanero reported a bug where background blur always fails in Brave Browser with the error: “Background blur couldn’t be started, so it was turned off.” The issue does not occur in Google Chrome on the same machine. After agentic investigation, the cause was identified: BackgroundBlurManager#setup() creates a hidden <video> element but never inserts it into the document. Brave aborts the play() request on this detached element with an AbortError, whereas Chrome (and headless Chromium) permits it. Diagnosis confirmed that MediaPipe, WebGL, and camera access function correctly; only the detached playback fails, regardless of Brave’s fingerprinting protection settings.

Proposed Fix:
Append the <video> element to the document.body (styled as invisible/1px) before calling play(), and remove it during teardown(). pacharanero submitted a pull request implementing this fix at github.com/discourse/discourse/pull/45366 after pmusaraj invited the contribution.

Additional Observations:

  1. Error Swallowing: The current code uses a bare catch {} in local-video-manager.js, hiding the specific error from logs even when verbose logging is enabled. Passing the caught error to liveLog.warn would improve debugging.
  2. Feature Detection: BackgroundBlurManager.isSupported() currently checks for canvas.captureStream and WebAssembly but not WebGL. Since MediaPipe’s segmentation requires a GL context (even for CPU delegates in some configurations), users without WebGL support are offered the feature only to encounter failures later. Adding a WebGL2 check to isSupported() would prevent this by hiding or disabling the control when incompatible.

Summary

FAO: @Falco :smiling_face_with_sunglasses:
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.

error: "Background blur couldn't be started, so it was turned off."

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_assets 0.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

  1. Open a Live room in Brave and turn the camera on.
  2. Enable background blur.
  3. 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

  1. The real error is swallowed. #createBackgroundBlur() in local-video-manager.js uses a bare catch {} and calls liveLog.warn("[live] failed to start background blur") without the error. Even with live_verbose_logging enabled, the console never says why blur failed. Passing the caught error to liveLog.warn (which already sanitises it) would have msurfaced the error more rapidly.

  2. Blur is offered where it can never work. BackgroundBlurManager.isSupported() checks only canvas.captureStream and WebAssembly. 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 to isSupported() would hide or disable the blur control instead of failing after the user turns it on. (Reproduced in headless Chromium by stubbing getContext("webgl*"), not seen in the wild.)

Thanks for Live - apart from this, it’s working nicely for us.

1 Like

This seems good from a quick look. Did you want to submit a PR with the fix?

1 Like

Incoming!

1 Like