fix(player): make Android native video actually visible, and usable
DR-172 reverted native video to opt-in after it shipped as audio with no picture, naming the compositing as the suspect. The compositing was fine. Five separate defects sat between ExoPlayer and the screen, each able to produce that exact symptom on its own, and each invisible to the others. DR-185 — the app shell painted over the surface. app.css clears the page's opaque layers through three selectors, one of which targets `[data-app-shell]`, an attribute NO component has ever set, in any commit. The shell paints --color-background across the whole viewport and VideoPlayer stacks above it, so the WebView composited opaque no matter what else was cleared. Invisible three ways over: the CSS is valid, the selector is plausible, and a rule matching nothing looks exactly like a rule matching something already transparent. DR-182 — nothing could lift the poster card. Every markMediaReady() call site is an HTML5 <video> event, and the native branch renders no element, so the black title card covered the surface for the entire session. The first fix hooked `player://position-update` / `player://state-changed`; those channels are never emitted by the backend, so it passed a test that fired them by hand and did nothing on a device. Driven from the player store now, as the seek bar already was. DR-183 — the JS bridges raced the page load. Installed 500ms after onCreate by walking the view tree, while WebView binds injected objects at page-load time, and the identity guard then declined to re-inject forever. setTransparent(true) could never arrive. Installed from WryActivity.onWebViewCreate instead, which wry calls immediately before the first loadUrl. DR-184 — the SurfaceView was never detached. detachVideoSurface had no callers anywhere, mirroring the DR-151 defect: every native video left its surface parented to the content view and the next one stacked another beneath it. DR-191 — the overlay stopped repainting. Incremental damage (the clock's text, the control bar's opacity) never reached the screen while structural changes did, so the progress bar froze, the controls would not fade, and the play overlay appeared to work because it is added and removed from the DOM. Driven from the Activity via postInvalidateOnAnimation while compositing is on. Two UI defects only this path could reveal came with them: isPlaying froze at its initial value, leaving the play overlay dimming and covering the video (DR-186), and the control bar's auto-hide was armed solely by mousemove, which a touchscreen never fires (DR-189). Immersive mode now applies on entering the player rather than only via the fullscreen button (DR-187). Verified on a device (Honor ROD2-W09, Android 16): logcat carries `WebView transparent = true` and `Marking media ready` with video on screen — the pair DR-172 went looking for and could not find — and skip, seek, rotation and subtitle rendering were exercised by hand. The default stays OFF (DR-188). Turning it on surfaced a further unverified sub-path: returning from background audio is HTML5-only, so playback stays dead (DR-190, proposed). Shipping it would have repeated DR-161 exactly — a verified sub-path made default over an unverified one.
This commit is contained in:
@@ -27,31 +27,52 @@ const STORAGE_KEY = "jellytau-experimental-native-video";
|
||||
const NATIVE_VIDEO_ATTR = "data-native-video";
|
||||
|
||||
/**
|
||||
* Whether the native path is on. **Off** unless the user turned it on.
|
||||
* Whether the native path is on, defaulting to **on** when the user has never
|
||||
* chosen.
|
||||
*
|
||||
* DR-161 briefly made this default to on, so picture-in-picture could shrink a
|
||||
* real video surface. On a device that shipped as **audio with no picture**:
|
||||
* ExoPlayer decoded correctly and fed its SurfaceView, but the SurfaceView sits
|
||||
* *behind* the WebView and the compositing that clears the opaque layers above it
|
||||
* never took effect — logcat showed `WebView transparent = false` and never
|
||||
* `= true`. So the video was rendering the whole time, behind the page.
|
||||
* This default has moved three times, so the history is the documentation:
|
||||
*
|
||||
* That is the defect the flag existed to contain, and it is why the default is
|
||||
* back off: video working matters more than PiP showing the native surface, and
|
||||
* PiP still works without it via the HTML5 path (DR-160). Native video remains
|
||||
* available in Settings for anyone testing it.
|
||||
* - **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 is still
|
||||
* off**, because turning it on surfaced a different gap: the background-audio
|
||||
* handoff (UR-040) can only *return* through the HTML5 element.
|
||||
* `applyPendingForegroundSeek` bails on `!videoElement`, the HLS re-init effect
|
||||
* bails on `!useHtml5Element`, and `handleCanPlay` — the event that owns the
|
||||
* post-handoff position and play state — is an element event that never fires
|
||||
* natively. So coming back from background audio leaves playback dead.
|
||||
*
|
||||
* That is the same shape of mistake as DR-161: a verified sub-path shipped as a
|
||||
* default over an unverified one. The evidence standard this branch set for the
|
||||
* picture applies to the handoff too, so the flip waits for it (DR-190).
|
||||
*
|
||||
* An explicit stored choice still wins in both directions, so anyone who turned
|
||||
* it on keeps it on.
|
||||
*
|
||||
* TRACES: UR-003, UR-004 | DR-172
|
||||
* TRACES: UR-003, UR-004 | DR-188
|
||||
*/
|
||||
function load(): boolean {
|
||||
if (typeof localStorage === "undefined") return false;
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) === "true";
|
||||
} catch {
|
||||
// Private-mode / disabled storage — default to the safe (HTML5) path.
|
||||
// Private-mode / disabled storage — default to the path whose handoff works.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -80,9 +101,9 @@ function createExperimentalNativeVideoStore() {
|
||||
}
|
||||
|
||||
/**
|
||||
* User opt-in for the native Android video path. **Defaults to off** again since
|
||||
* DR-172 — see `load()`. The name says "experimental" because the flag
|
||||
* remains a suppressor of Rust's backend choice, not a promoter of it.
|
||||
* User opt-in for the native Android video path. **Defaults to off** — see
|
||||
* `load()`. The name says "experimental" because the flag remains a suppressor
|
||||
* of Rust's backend choice, not a promoter of it.
|
||||
*/
|
||||
export const experimentalNativeVideo = createExperimentalNativeVideoStore();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user