Hand video playback off to a native audio-only stream when the app is backgrounded or locked, with no on-device video decode (UR-040). Adds player_enter/exit_background_audio commands, an audio-only stream URL for video items across the repository layer, and the frontend handoff state machine wired into VideoPlayer. Includes accompanying repository/offline/player refactoring and regenerates the traceability matrix.
87 lines
2.7 KiB
TypeScript
87 lines
2.7 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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|