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.
146 lines
6.0 KiB
TypeScript
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();
|