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.
This commit is contained in:
2026-08-16 22:14:43 +02:00
parent 5e8efa252e
commit ab95f5013d
2 changed files with 29 additions and 21 deletions
+27 -18
View File
@@ -50,30 +50,37 @@ const NATIVE_VIDEO_ATTR = "data-native-video";
* *
* The picture is genuinely fixed and device-verified — `WebView transparent = * The picture is genuinely fixed and device-verified — `WebView transparent =
* true` and `Marking media ready` now appear in logcat with video on screen, * 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 * the pair DR-172 went looking for and could not find. The default nonetheless
* off**, because turning it on surfaced a different gap: the background-audio * stayed **off** for a further release, because turning it on surfaced a
* handoff (UR-040) can only *return* through the HTML5 element. * different gap: the background-audio handoff (UR-040) could only *return*
* `applyPendingForegroundSeek` bails on `!videoElement`, the HLS re-init effect * through the HTML5 element, so coming back from background audio left playback
* bails on `!useHtml5Element`, and `handleCanPlay` — the event that owns the * dead. That was the same shape of mistake as DR-161 — a verified sub-path
* post-handoff position and play state — is an element event that never fires * shipped as a default over an unverified one — so the flip waited (DR-190).
* 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 * - **on** now. The two defects that were holding it back are fixed and
* default over an unverified one. The evidence standard this branch set for the * verified on a device: the handoff return restarts the renderer that is
* picture applies to the handoff too, so the flip waits for it (DR-190). * 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 * An explicit stored choice still wins in both directions, so anyone who turned
* it on keeps it on. * 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 * TRACES: UR-003, UR-004 | DR-188
*/ */
function load(): boolean { function load(): boolean {
if (typeof localStorage === "undefined") return false; if (typeof localStorage === "undefined") return true;
try { try {
return localStorage.getItem(STORAGE_KEY) === "true"; const stored = localStorage.getItem(STORAGE_KEY);
// Never chosen → on. Chosen → honour it, in both directions.
return stored === null ? true : stored === "true";
} catch { } catch {
// Private-mode / disabled storage — default to the path whose handoff works. // Private-mode / disabled storage — same default as a fresh install.
return false; return true;
} }
} }
@@ -101,9 +108,11 @@ function createExperimentalNativeVideoStore() {
} }
/** /**
* User opt-in for the native Android video path. **Defaults to off** — see * User preference for the native Android video path. **Defaults to on** — see
* `load()`. The name says "experimental" because the flag remains a suppressor * `load()`. The name still says "experimental" because the flag remains a
* of Rust's backend choice, not a promoter of it. * 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(); export const experimentalNativeVideo = createExperimentalNativeVideoStore();
+2 -3
View File
@@ -746,9 +746,8 @@
Decode video with the device's hardware decoder instead of the Decode video with the device's hardware decoder instead of the
built-in web player, for better performance and battery life, built-in web player, for better performance and battery life,
and so picture-in-picture shows the video rather than the app. and so picture-in-picture shows the video rather than the app.
The picture works, but background audio does not come back from On by default. Turn it off to fall back to the built-in web
the lockscreen on this path yet — leave it off unless you are player if a video misbehaves.
helping test it.
</p> </p>
</div> </div>
<button <button