/** * Native video surface compositing, Android only. * * TRACES: UR-003, UR-004 | DR-150, DR-151, DR-183 * * On Android, ExoPlayer renders video into a SurfaceView that sits *behind* the * Tauri WebView (`setZOrderMediaOverlay(false)`, added at index 0 of the content * view by VideoOverlayManager). For that video to be visible, two independent * opaque layers have to be cleared: * * 1. The **WebView widget's own background** — reachable only from Kotlin, via * the `AndroidVideoSurface` @JavascriptInterface installed by MainActivity. * 2. The **web page's backgrounds** — the `html`/`body` colour in app.css and * the app shell's `bg-[var(--color-background)]`. Handled by the * `data-native-video` attribute, which $lib/stores/nativeVideo.ts sets and * app.css keys its transparency rules off. * * Clearing only one leaves a black screen with audio, which is exactly the * failure mode the old INTERIM override in VideoPlayer.svelte was working * around. Both must be toggled together, so this module owns both halves. * * Everything here is a no-op off Android — the bridge is simply absent. */ import { nativeVideoActive } from "$lib/stores/nativeVideo"; import { createLogger } from "$lib/utils/logger"; const log = createLogger("videoSurface"); interface AndroidVideoSurfaceBridge { setTransparent(transparent: boolean): void; isSupported(): boolean; } declare global { interface Window { AndroidVideoSurface?: AndroidVideoSurfaceBridge; } } function bridge(): AndroidVideoSurfaceBridge | undefined { if (typeof window === "undefined") return undefined; return window.AndroidVideoSurface; } /** * Whether the native-surface bridge exists on this platform. This reports only * that the *plumbing* is present; whether native video should actually be used * is Rust's decision (`player_get_capabilities`) gated by the user's * `experimentalNativeVideo` flag. */ export function isNativeSurfaceBridgeAvailable(): boolean { try { return bridge()?.isSupported() ?? false; } catch (err) { log.warn("isSupported check failed:", err); return false; } } /** * Make the webview transparent so the video surface behind it shows through. * * MUST be paired with {@link disableNativeVideoCompositing} on teardown — a * transparent window left behind shows the launcher through the whole app. */ export function enableNativeVideoCompositing(): void { // Page layer first: if the Kotlin call succeeded but this threw, the user // would see through the app to the home screen. nativeVideoActive.set(true); const androidVideoSurface = bridge(); if (!androidVideoSurface) { // Say so loudly. Every bridge call in this file is optional-chained, so a // missing bridge is silent — and a silently-skipped setTransparent(true) is // indistinguishable on screen from a compositing failure: ExoPlayer renders // correctly behind a WebView that never stopped painting its own opaque // background. That ambiguity is what DR-172 was left holding. MainActivity's // console bridge forwards this to logcat under the JellyTauWeb tag. log.error( "AndroidVideoSurface bridge is MISSING - the webview will " + "stay opaque and native video will play as audio with no picture" ); return; } try { androidVideoSurface.setTransparent(true); log.debug("compositing enabled (setTransparent(true) sent)"); } catch (err) { log.warn("setTransparent(true) failed:", err); nativeVideoActive.set(false); } } /** Restore the opaque webview background. Safe to call unconditionally. */ export function disableNativeVideoCompositing(): void { try { bridge()?.setTransparent(false); } catch (err) { log.warn("setTransparent(false) failed:", err); } // Always clear the page layer, even if the bridge call failed, so the app is // never left rendering over a transparent window. nativeVideoActive.set(false); }