fix(player): make lockscreen transport reach background audio (DR-097)

Pausing from the lockscreen did nothing while a video's audio played in
the background. The handoff starts native ExoPlayer audio and only then
tears the WebView <video> down, and that teardown fires a DOM `pause`
the frontend reports like any other — leaving html5_playing = Some(false).
Transport therefore stayed aimed at the element: the lockscreen pause
emitted a ControlCommand into a <video> that no longer existed while the
native player carried on.

The controller now tracks a background-audio handoff explicitly. Entering
one hands transport authority to the native backend and drops the dying
element's state/position/media-loaded reports, which also stop flipping
the UI to paused and dragging the position backwards. Exiting restores
the element as the player.

A lockscreen pause also has to survive the return to the foreground: the
video used to resume from a snapshot taken at handoff time, undoing the
pause on the way back in. shouldResumeOnForeground() lets an explicit
`paused` from the player override that snapshot.

TRACES: UR-040, UR-005 | DR-052, DR-097
This commit is contained in:
2026-08-05 12:26:25 +02:00
parent 6aaa80ff92
commit 878ac5fa59
5 changed files with 196 additions and 4 deletions
+2 -2
View File
@@ -714,7 +714,7 @@ pub async fn player_enter_background_audio(
// this base to the native player's relative position to get the absolute one.
// The controller owns it so a backend-driven advance to the next episode
// clears it along with the stream it described.
controller.set_background_audio_base(position_seconds);
controller.enter_background_audio(position_seconds);
controller
.play_item(media_item)
.map_err(|e| e.to_string())?;
@@ -753,7 +753,7 @@ pub async fn player_exit_background_audio(
// The base offset (handoff position) + native player's relative position =
// the absolute position to resume the video at. Zero after a backend-driven
// episode advance, whose stream already starts at its own zero.
let base = controller.take_background_audio_base();
let base = controller.exit_background_audio();
// Capture position into a `let` BEFORE stop() — never hold work across a lock
// re-entrant call (deadlock discipline, CLAUDE.md).
let relative = controller.position();