DR-161 made `experimentalNativeVideo` default to on, but three comments still described the pre-flip world and one of them was load-bearing: - `nativeVideo.ts` labelled the store "Default off" directly above a `load()` that returns true when nothing is stored. - The two PiP comments explained themselves as "what makes PiP work in the shipping configuration", which stopped being true when Android started shrinking the real ExoPlayer surface. They still describe the Linux path and the flag-off case, so they say that instead. - `video_audio_codecs` justified its narrow codec list with "video does not play through ExoPlayer", which is no longer so on Android. The narrow list is still right, for a different reason now recorded: the flag is a user setting and a download outlives it, so only the intersection holds on both sides of the switch. DR-171 carries the same caveat. No behaviour change.
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
/**
|
|
* Picture-in-picture support, Android only.
|
|
*
|
|
* TRACES: UR-041 | IR-026 | DR-053
|
|
*
|
|
* Video on Android renders into a native ExoPlayer SurfaceView behind the
|
|
* WebView, so PiP is driven by the Activity (which shrinks into a floating
|
|
* window) rather than the HTML5 `requestPictureInPicture()` API. The bridge is
|
|
* the `AndroidPictureInPicture` @JavascriptInterface installed by MainActivity.
|
|
*
|
|
* On every other platform this module reports unsupported. Notably WebKitGTK
|
|
* (the Linux webview) does not implement the Picture-in-Picture Web API at all,
|
|
* so there is no HTML5 fallback to reach for.
|
|
*/
|
|
|
|
interface AndroidPictureInPictureBridge {
|
|
enterPip(): void;
|
|
isSupported(): boolean;
|
|
canEnterPip(): boolean;
|
|
setAutoEnterEnabled(enabled: boolean): void;
|
|
setHtml5VideoState(active: boolean, width: number, height: number, playing: boolean): void;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
AndroidPictureInPicture?: AndroidPictureInPictureBridge;
|
|
}
|
|
}
|
|
|
|
function bridge(): AndroidPictureInPictureBridge | undefined {
|
|
if (typeof window === "undefined") return undefined;
|
|
return window.AndroidPictureInPicture;
|
|
}
|
|
|
|
/**
|
|
* Whether the device can do PiP at all - used to decide if the button should
|
|
* be rendered. False on desktop, and on Android devices where the user has
|
|
* disabled the feature.
|
|
*/
|
|
export function isPipSupported(): boolean {
|
|
try {
|
|
return bridge()?.isSupported() ?? false;
|
|
} catch (err) {
|
|
console.warn("[PiP] isSupported check failed:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether entering PiP would succeed right now: a native video must be playing
|
|
* locally. False during audio playback and while casting to a remote session.
|
|
*/
|
|
export function canEnterPip(): boolean {
|
|
try {
|
|
return bridge()?.canEnterPip() ?? false;
|
|
} catch (err) {
|
|
console.warn("[PiP] canEnterPip check failed:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Enter picture-in-picture. No-op where unsupported. */
|
|
export function enterPip(): void {
|
|
try {
|
|
bridge()?.enterPip();
|
|
} catch (err) {
|
|
console.error("[PiP] Failed to enter picture-in-picture:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable/disable auto-entering PiP when the user backgrounds the app.
|
|
*
|
|
* This is a coarse frontend override; the authoritative gate is the native
|
|
* `canEnterPip` guard, which already refuses PiP unless a local video surface
|
|
* is actively rendering (so audio playback, menu/library browsing, and
|
|
* remote/cast sessions never enter PiP regardless of this flag). The only
|
|
* caller today is the background-audio toggle, which disarms auto-PiP so the
|
|
* two background behaviours stay mutually exclusive.
|
|
*/
|
|
export function setAutoEnterEnabled(enabled: boolean): void {
|
|
try {
|
|
bridge()?.setAutoEnterEnabled(enabled);
|
|
} catch (err) {
|
|
console.warn("[PiP] Failed to set auto-enter:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tell native that a WebView `<video>` is (or is no longer) the playback surface.
|
|
*
|
|
* This is what makes PiP work on the HTML5 path. The native side only ever knew
|
|
* about the ExoPlayer surface, and that path is behind `experimentalNativeVideo`,
|
|
* which defaulted to off when this was written — so `canEnterPip` was always
|
|
* false and pressing the button did nothing. Reporting the element's state gives
|
|
* native a surface it can legitimately shrink into, plus the intrinsic size it
|
|
* needs for the PiP window's aspect ratio and the play state for its play/pause
|
|
* action.
|
|
*
|
|
* The flag defaults to **on** now (DR-161), so Android normally shrinks the real
|
|
* ExoPlayer surface instead; this remains the path for Linux and for anyone who
|
|
* turned the flag off.
|
|
*
|
|
* Pass `active: false` when the element goes away, or PiP would be offered over a
|
|
* video that is no longer there.
|
|
*
|
|
* TRACES: UR-041 | DR-160
|
|
*/
|
|
export function setHtml5VideoState(
|
|
active: boolean,
|
|
width: number,
|
|
height: number,
|
|
playing: boolean
|
|
): void {
|
|
try {
|
|
bridge()?.setHtml5VideoState(active, Math.round(width), Math.round(height), playing);
|
|
} catch (err) {
|
|
console.warn("[PiP] Failed to report HTML5 video state:", err);
|
|
}
|
|
}
|