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.
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
/**
|
|
* Background-audio support, Android only.
|
|
*
|
|
* TRACES: UR-040 | IR-025, DR-051
|
|
*
|
|
* Keeps a video's *audio* playing when the app is backgrounded or the screen is
|
|
* locked, while video decode stops. This is a HANDOFF: the WebView `<video>`
|
|
* element (which decodes video) is torn down and the same item is played back
|
|
* audio-only through the native ExoPlayer foreground service. It is NOT the
|
|
* WebView staying alive — an Android WebView `<video>` does not keep audio
|
|
* playing once the app is backgrounded.
|
|
*
|
|
* The `AndroidBackgroundAudio` @JavascriptInterface (installed by MainActivity)
|
|
* carries the toggle state to native; native signals background/foreground back
|
|
* to the frontend as DOM CustomEvents (`jellytau-background` /
|
|
* `jellytau-foreground`) — see subscribeAppBackgrounded/Foregrounded below.
|
|
*
|
|
* Unsupported (no-op) on every non-Android platform.
|
|
*/
|
|
|
|
interface AndroidBackgroundAudioBridge {
|
|
setEnabled(enabled: boolean): void;
|
|
isSupported(): boolean;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
AndroidBackgroundAudio?: AndroidBackgroundAudioBridge;
|
|
}
|
|
}
|
|
|
|
function bridge(): AndroidBackgroundAudioBridge | undefined {
|
|
if (typeof window === "undefined") return undefined;
|
|
return window.AndroidBackgroundAudio;
|
|
}
|
|
|
|
/** Whether background audio is available — used to decide if the toggle renders. */
|
|
export function isBackgroundAudioSupported(): boolean {
|
|
try {
|
|
return bridge()?.isSupported() ?? false;
|
|
} catch (err) {
|
|
console.warn("[BgAudio] isSupported check failed:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Arm/disarm background-audio mode for the current video. When armed, the native
|
|
* side runs the audio handoff on background instead of entering PiP.
|
|
*/
|
|
export function setBackgroundAudioEnabled(enabled: boolean): void {
|
|
try {
|
|
bridge()?.setEnabled(enabled);
|
|
} catch (err) {
|
|
console.warn("[BgAudio] Failed to set enabled:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribe to the native "app backgrounded" signal (Home/app-switch/lock).
|
|
* Returns an unsubscribe function. No-op where unsupported (the event never
|
|
* fires on non-Android platforms).
|
|
*/
|
|
export function subscribeAppBackgrounded(handler: () => void): () => void {
|
|
if (typeof window === "undefined") return () => {};
|
|
window.addEventListener("jellytau-background", handler);
|
|
return () => window.removeEventListener("jellytau-background", handler);
|
|
}
|
|
|
|
/** Subscribe to the native "app foregrounded" signal. Returns an unsubscribe fn. */
|
|
export function subscribeAppForegrounded(handler: () => void): () => void {
|
|
if (typeof window === "undefined") return () => {};
|
|
window.addEventListener("jellytau-foreground", handler);
|
|
return () => window.removeEventListener("jellytau-foreground", handler);
|
|
}
|