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:
@@ -1,6 +1,7 @@
|
||||
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026, UR-040, UR-061 | DR-010, DR-023, DR-024, DR-051, DR-052, DR-092, DR-098, DR-099 -->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, untrack } from "svelte";
|
||||
import { get } from "svelte/store";
|
||||
import { goto } from "$app/navigation";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import type { JRayActor } from "$lib/api/bindings";
|
||||
@@ -14,7 +15,7 @@
|
||||
import CachedImage from "../common/CachedImage.svelte";
|
||||
import { videoFitClass } from "./videoFit";
|
||||
import { sleepTimerActive, sleepTimerExpiredSignal } from "$lib/stores/sleepTimer";
|
||||
import { playbackPosition } from "$lib/stores/player";
|
||||
import { playbackPosition, playerState } from "$lib/stores/player";
|
||||
import * as html5Adapter from "$lib/player/html5Adapter";
|
||||
import { playerController } from "$lib/player";
|
||||
import { Html5PlayerAdapter, type Html5ElementBridge } from "$lib/player/adapters";
|
||||
@@ -42,6 +43,7 @@
|
||||
initialHandoffState,
|
||||
shouldEnterBackgroundAudio,
|
||||
shouldExitBackgroundAudio,
|
||||
shouldResumeOnForeground,
|
||||
type BackgroundAudioState,
|
||||
} from "./backgroundAudioHandoff";
|
||||
|
||||
@@ -1333,7 +1335,11 @@
|
||||
// the position native reached, and restore play/pause.
|
||||
async function exitBackgroundAudioHandoff() {
|
||||
if (!shouldExitBackgroundAudio(handoffState)) return;
|
||||
const wasPlaying = handoffState.wasPlaying;
|
||||
// Read the native player's state BEFORE exiting — the exit stops it. If the
|
||||
// user hit pause on the lockscreen while backgrounded, that pause must
|
||||
// survive the return to video rather than being overwritten by whatever the
|
||||
// <video> was doing when we handed off.
|
||||
const wasPlaying = shouldResumeOnForeground(handoffState.wasPlaying, get(playerState).kind);
|
||||
handoffState = { ...initialHandoffState };
|
||||
try {
|
||||
// Absolute position the native audio reached (base offset applied in Rust).
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
initialHandoffState,
|
||||
shouldEnterBackgroundAudio,
|
||||
shouldExitBackgroundAudio,
|
||||
shouldResumeOnForeground,
|
||||
type BackgroundAudioState,
|
||||
} from "./backgroundAudioHandoff";
|
||||
|
||||
@@ -58,4 +59,27 @@ describe("backgroundAudioHandoff", () => {
|
||||
expect(shouldExitBackgroundAudio(active)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldResumeOnForeground", () => {
|
||||
it("resumes when it was playing and the native audio still is", () => {
|
||||
expect(shouldResumeOnForeground(true, "playing")).toBe(true);
|
||||
});
|
||||
|
||||
it("stays paused when the lockscreen paused the native audio", () => {
|
||||
// The whole point of the lockscreen pause: coming back to the app must not
|
||||
// undo it just because the video was playing when we handed off.
|
||||
expect(shouldResumeOnForeground(true, "paused")).toBe(false);
|
||||
});
|
||||
|
||||
it("stays paused when the video was already paused at handoff", () => {
|
||||
expect(shouldResumeOnForeground(false, "playing")).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to the captured state when native state is unknown", () => {
|
||||
// Loading/seeking/idle say nothing about intent — the handoff snapshot is
|
||||
// the best evidence we have, so a playing video still resumes.
|
||||
expect(shouldResumeOnForeground(true, "loading")).toBe(true);
|
||||
expect(shouldResumeOnForeground(true, undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,3 +55,22 @@ export function shouldEnterBackgroundAudio(
|
||||
export function shouldExitBackgroundAudio(state: BackgroundAudioState): boolean {
|
||||
return state.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the `<video>` should start playing again once it reloads on foreground.
|
||||
*
|
||||
* `wasPlaying` is what the video was doing when we handed off, but the native
|
||||
* audio player kept going after that — and the lockscreen/notification can pause
|
||||
* it while backgrounded. The player is the authoritative source of play/pause,
|
||||
* so an explicit `paused` from it overrides the handoff snapshot; anything less
|
||||
* definite (loading, seeking, already-stopped, no state at all) falls back to
|
||||
* the snapshot.
|
||||
*
|
||||
* TRACES: UR-040, UR-005 | DR-052 | UT-060
|
||||
*/
|
||||
export function shouldResumeOnForeground(
|
||||
wasPlaying: boolean,
|
||||
nativeStateKind: string | undefined
|
||||
): boolean {
|
||||
return wasPlaying && nativeStateKind !== "paused";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user