Files
jellytau/src/lib/utils/backgroundAudio.ts
T
dtourolle d54d8cc7c4 refactor(logging): route frontend console calls through the logger
TRACES: | DR-204

484 ungated `console.*` calls across 63 non-test frontend files shipped to
end users with no way to turn them off. Mechanical substitution, no control
flow, error handling or message semantics changed:

  console.log / console.debug -> log.debug
  console.info                -> log.info
  console.warn                -> log.warn
  console.error               -> log.error

Hand-written `"[Scope] …"` prefixes are dropped where the logger's scope
now carries them; scope names that already existed are preserved verbatim
(`[Auth]`, `[VideoPlayer]`, `[PiP]`, …) and inferred from the filename
where a file had none. `src/routes/player/[id]/+page.svelte` keeps its
`NextEpisode` and `AutoPlay` sub-scopes as separate loggers rather than
flattening them into the page scope.

`grep -rn 'console\.' src/` now matches nothing outside the tests and the
facade itself.
2026-08-20 19:29:59 +02:00

81 lines
2.9 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.
*/
import { createLogger } from "$lib/utils/logger";
const log = createLogger("BgAudio");
interface AndroidBackgroundAudioBridge {
setEnabled(enabled: boolean): void;
}
declare global {
interface Window {
AndroidBackgroundAudio?: AndroidBackgroundAudioBridge;
}
}
function bridge(): AndroidBackgroundAudioBridge | undefined {
if (typeof window === "undefined") return undefined;
return window.AndroidBackgroundAudio;
}
/**
* 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): boolean {
const b = bridge();
if (!b) {
// The button is gated on platform(), not on this bridge, so it can render
// before/without the bridge existing. Silently no-oping here leaves the UI
// showing "armed" while native never learns — and the handoff then never
// fires on lock. Report it so callers can retry.
log.warn("setEnabled: bridge missing, native NOT armed");
return false;
}
try {
b.setEnabled(enabled);
log.debug("setEnabled ->", enabled);
return true;
} catch (err) {
log.warn("Failed to set enabled:", err);
return false;
}
}
/**
* 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);
}