Files
jellytau/src/lib/utils/pictureInPicture.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

125 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.
*/
import { createLogger } from "$lib/utils/logger";
const log = createLogger("PiP");
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) {
log.warn("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) {
log.warn("canEnterPip check failed:", err);
return false;
}
}
/** Enter picture-in-picture. No-op where unsupported. */
export function enterPip(): void {
try {
bridge()?.enterPip();
} catch (err) {
log.error("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) {
log.warn("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 is back to defaulting **off** (DR-172, after native video shipped as
* audio with no picture), so this is once again the path Android normally takes —
* which is why PiP does not depend on that flag being on.
*
* 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) {
log.warn("Failed to report HTML5 video state:", err);
}
}