fix(player): decide transport in Rust for webview media (DR-097)
Video on Android/Linux renders in a webview <video> element, and the frontend facade short-circuited play/pause/toggle straight into the adapter whenever one was registered. Html5PlayerAdapter.toggle() then decided play-vs-pause by reading el.paused off the DOM, so the Rust controller never saw the intent and could not serialise competing ones. el.paused flips transiently while an element buffers or settles a seek. Two intents ~150ms apart therefore read *different* values and performed *opposing* actions — one playing, one pausing — which self-sustained a play/pause loop that needed no further input. On device this showed up as a fully healthy element (readyState=4, networkState=1, not seeking, not buffering, not ended) pausing itself roughly once a second, so unpausing or skipping ahead bounced straight back to paused. The root cause was that Rust held NO state for webview-rendered media: report_html5_state only re-emitted its argument, despite the comment above it claiming the controller was the single source of truth. It had nothing to decide a toggle from. Now report_html5_state tracks the reported state, and play/pause/toggle consult it and drive the element by emitting a ControlCommand — the same "backend decides, adapter executes the primitive" split player_seek_video already uses. A stopped/idle report clears the tracking so MPV/ExoPlayer regain authority for music playback. Tests cover the loop signature directly (repeated toggles must alternate, never repeat or oppose) plus a guard that one intent yields exactly one ControlCommand — which matters on Windows, where the backend is itself webview-based and could otherwise be driven twice.
This commit is contained in:
+15
-4
@@ -12,7 +12,7 @@
|
||||
* derived + merged (remote-session-aware) stores so UI can import state and
|
||||
* actions from one place, in both local and remote modes.
|
||||
*
|
||||
* TRACES: UR-005 | DR-001, DR-009
|
||||
* TRACES: UR-005 | DR-001, DR-009, DR-097 | UT-089
|
||||
*/
|
||||
|
||||
import { get } from "svelte/store";
|
||||
@@ -83,18 +83,29 @@ function requireHandle(): string {
|
||||
// Transport controls (no repository handle required)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Transport intents ALWAYS go to the backend, in both native and HTML5 modes.
|
||||
//
|
||||
// These used to short-circuit into the active video adapter, which made the
|
||||
// webview the decider: `adapter.toggle()` read `el.paused` off the DOM and
|
||||
// flipped the element, so Rust never saw the intent. `el.paused` flips
|
||||
// transiently while an element buffers or settles a seek, so two intents
|
||||
// ~150ms apart could read different values and take opposing actions — a
|
||||
// self-sustaining play/pause loop.
|
||||
//
|
||||
// Now Rust decides from PlayerController state and drives the element back
|
||||
// through a `ControlCommand` event (handled in playerEvents.ts), the same
|
||||
// "backend decides, adapter executes the primitive" split used by
|
||||
// player_seek_video. Do NOT reintroduce an adapter short-circuit here.
|
||||
|
||||
async function play() {
|
||||
if (activeAdapter) return void (await activeAdapter.play());
|
||||
await commands.playerPlay();
|
||||
}
|
||||
|
||||
async function pause() {
|
||||
if (activeAdapter) return void (await activeAdapter.pause());
|
||||
await commands.playerPause();
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
if (activeAdapter) return void (await activeAdapter.toggle());
|
||||
await commands.playerToggle();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user