Files
jellytau/src/lib/stores/nativeVideo.ts
T
dtourolle ab95f5013d feat(player): make native Android video the default
The two defects that were holding the flip back are fixed and verified on a
device, which is the standard this default has been held to since DR-161 shipped
a verified sub-path over an unverified one:

  - returning from background audio restarts the renderer that is actually on
    screen, instead of only ever reloading the <video> element (DR-196)
  - the letterbox bars are painted, instead of retaining whatever was last in
    the framebuffer (DR-194)

Evidence: handoff to audio-only at 69:54 returning to video playing at 70:18,
and clean bars across playback, the control bar and a rotation round-trip.

An explicit stored choice still wins in both directions, so anyone who turned the
flag off keeps it off — hence the null check on the stored value rather than a
bare === "true", which would silently re-enable it for people who opted out.

The Settings copy no longer tells users to leave it off; it now describes the
toggle as the fallback to the built-in web player.

The flag keeps its "experimental" name because it remains a suppressor of Rust's
backend choice, never a promoter: turning it on cannot produce a native backend
where Rust says HTML5.
2026-08-16 22:14:43 +02:00

146 lines
6.0 KiB
TypeScript

// Native-video compositing state.
//
// TRACES: UR-003, UR-004 | DR-150, DR-152
//
// Two separate concerns live here, deliberately:
//
// 1. `experimentalNativeVideo` — the user-facing opt-in flag. Rust already
// decides *which backend this platform has* (`useHtml5Element` from
// `player_play_item`); this flag only *suppresses* that decision so a
// half-working spike cannot ship as a regression. It never turns native on
// where Rust says HTML5.
//
// 2. `nativeVideoActive` — whether a native surface is on screen right now.
// Setting it toggles `data-native-video` on <html>, which is what the CSS in
// app.css keys off to clear the app's opaque backgrounds so the SurfaceView
// behind the WebView is visible. It is deliberately NOT derived from the
// flag: the backgrounds must come back the moment the player unmounts.
//
// Frontend-only preference, stored in localStorage per the `jellytau-view-mode`
// precedent in library.ts — no Rust settings command backs this.
import { writable } from "svelte/store";
const STORAGE_KEY = "jellytau-experimental-native-video";
/** The attribute app.css keys its transparency rules off. */
const NATIVE_VIDEO_ATTR = "data-native-video";
/**
* Whether the native path is on, defaulting to **on** when the user has never
* chosen.
*
* This default has moved three times, so the history is the documentation:
*
* - **off** while the path was a spike (DR-150).
* - **on** for picture-in-picture (DR-161), which shipped as *audio with no
* picture* — ExoPlayer decoded correctly into a live SurfaceView while the
* page stayed opaque over it.
* - **off** again (DR-172), which named the compositing as the suspect but did
* not find it.
* - **on** now, because the four defects behind that symptom were found and
* each is fixed and verified on a device: the app shell painted over the
* surface through a CSS rule targeting an attribute nothing set (DR-185); the
* poster card had no way to lift on a path with no `<video>` element
* (DR-182); the JS bridges raced the page load, so `setTransparent(true)`
* could never arrive (DR-183); and the SurfaceView was never detached
* (DR-184). Two further UI defects that only this path could show — the play
* overlay never clearing (DR-186) and the system bars staying over the player
* (DR-187) — are fixed with it.
*
* The picture is genuinely fixed and device-verified — `WebView transparent =
* true` and `Marking media ready` now appear in logcat with video on screen,
* the pair DR-172 went looking for and could not find. The default nonetheless
* stayed **off** for a further release, because turning it on surfaced a
* different gap: the background-audio handoff (UR-040) could only *return*
* through the HTML5 element, so coming back from background audio left playback
* dead. That was the same shape of mistake as DR-161 — a verified sub-path
* shipped as a default over an unverified one — so the flip waited (DR-190).
*
* - **on** now. The two defects that were holding it back are fixed and
* verified on a device: the handoff return restarts the renderer that is
* actually on screen rather than only ever reloading the `<video>` element
* (DR-196), and the letterbox bars are painted instead of retaining whatever
* was last in the framebuffer (DR-194). The evidence standard this default
* has been held to since DR-161 is met for both: audio handoff at 69:54
* returning to video playing at 70:18, and clean bars across playback, the
* control bar and a rotation round-trip.
*
* An explicit stored choice still wins in both directions, so anyone who turned
* it off keeps it off — hence the `null` check rather than a bare `=== "true"`,
* which would silently re-enable it for people who opted out.
*
* TRACES: UR-003, UR-004 | DR-188
*/
function load(): boolean {
if (typeof localStorage === "undefined") return true;
try {
const stored = localStorage.getItem(STORAGE_KEY);
// Never chosen → on. Chosen → honour it, in both directions.
return stored === null ? true : stored === "true";
} catch {
// Private-mode / disabled storage — same default as a fresh install.
return true;
}
}
function persist(enabled: boolean) {
if (typeof localStorage === "undefined") return;
try {
localStorage.setItem(STORAGE_KEY, String(enabled));
} catch {
// Quota or private-mode failure — keep the in-memory value.
}
}
function createExperimentalNativeVideoStore() {
const { subscribe, set } = writable<boolean>(load());
return {
subscribe,
set(enabled: boolean) {
persist(enabled);
set(enabled);
},
/** Read the current value without subscribing (init-time decisions). */
current: load,
};
}
/**
* User preference for the native Android video path. **Defaults to on** — see
* `load()`. The name still says "experimental" because the flag remains a
* suppressor of Rust's backend choice, not a promoter of it: turning it off
* forces the webview element, turning it on never produces a native backend
* where Rust says HTML5.
*/
export const experimentalNativeVideo = createExperimentalNativeVideoStore();
function createNativeVideoActiveStore() {
const { subscribe, set } = writable<boolean>(false);
return {
subscribe,
/**
* Mark a native video surface as visible (or gone) and sync the <html>
* attribute that app.css uses to clear opaque backgrounds.
*/
set(active: boolean) {
if (typeof document !== "undefined") {
if (active) {
document.documentElement.setAttribute(NATIVE_VIDEO_ATTR, "active");
} else {
document.documentElement.removeAttribute(NATIVE_VIDEO_ATTR);
}
}
set(active);
},
};
}
/**
* Whether a native video surface is currently on screen. Must be cleared on
* player teardown, or the rest of the app renders over a transparent window.
*/
export const nativeVideoActive = createNativeVideoActiveStore();