Background-audio handoff for video + repository/player refactor
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.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user