On iPad Brave, a shared video stopped the moment the browser left the foreground, even with Brave's "Enable Background Audio" on, while an mp3 kept playing. Brave iOS keeps a web <video> going by re-playing one the OS paused (brave-core ios/browser/web/media/resources/media_backgrounding.ts), but it only hooks a <video> present at document start or added as a MutationObserver addedNode that IS a video. React inserts a player's <video> inside its freshly mounted wrapper in one go, so Brave never saw it. lib/browserBackgroundPlay re-inserts a mounted <video> at its own position, once per element: it becomes an added node, the DOM React manages does not change, and playback is not interrupted. The watch page, the downloads library player and the Plex player hand their <video> to it. Measured on the owner's iPad (Brave) with a prototype of the same idea: the video kept playing in another app and with the screen locked. The YouTube modal is not ours to change: its <video> lives in YouTube's cross-origin iframe, and nothing of ours pauses it on backgrounding. The prototype's two audio-element variants (a muted video following an <audio>, and an <audio> taking over on hide) went silent in the background on the iPad and are removed. The watch page gets a Repeat toggle, on by default and remembered per browser (LS.watchRepeat; the page has no account): it sets `loop` on the <video> or the audio-only <audio>.
24 lines
1.5 KiB
TypeScript
24 lines
1.5 KiB
TypeScript
// Brave iOS keeps a web <video> playing when the browser leaves the foreground (Settings → Media → "Enable
|
|
// Background Audio") by re-playing a video the OS paused. But it only hooks the <video> elements it SEES:
|
|
// those present at document start, or a MutationObserver addedNode that IS a <video> (brave-core
|
|
// ios/browser/web/media/resources/media_backgrounding.ts). React inserts a player's <video> inside its
|
|
// freshly mounted wrapper in one go, so the video itself is never an added node and Brave never hooks it —
|
|
// measured on iPad Brave 2026-09-14: the shared video stopped the moment the app was left; inserted as a
|
|
// node of its own it kept playing, locked screen included.
|
|
//
|
|
// Re-inserting the element at its own position makes it an added node without changing the DOM React
|
|
// manages. Playback is not interrupted: a media element removed and re-inserted within the same task is
|
|
// back in the document by the next stable state, so the spec's pause-on-removal never runs.
|
|
//
|
|
// Only a <video> needs this: an <audio> already keeps playing in the background on its own.
|
|
|
|
const exposed = new WeakSet<HTMLVideoElement>();
|
|
|
|
/** Make a mounted <video> visible to the browser's background-play hook. Idempotent per element, so it is
|
|
* safe from a ref callback that React calls on every render. */
|
|
export function exposeVideoToBackgroundPlay(video: HTMLVideoElement | null): void {
|
|
if (!video || exposed.has(video) || !video.parentNode) return;
|
|
exposed.add(video);
|
|
video.parentNode.insertBefore(video, video.nextSibling);
|
|
}
|