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
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
computeHandoffPosition,
|
|
initialHandoffState,
|
|
shouldEnterBackgroundAudio,
|
|
shouldExitBackgroundAudio,
|
|
shouldResumeOnForeground,
|
|
type BackgroundAudioState,
|
|
} from "./backgroundAudioHandoff";
|
|
|
|
// TRACES: UR-040 | DR-052 | UT-060
|
|
|
|
describe("backgroundAudioHandoff", () => {
|
|
describe("computeHandoffPosition", () => {
|
|
it("sums element time and transcode seekOffset (absolute position)", () => {
|
|
// Transcoded HLS resets element time to 0 after a reload; seekOffset carries
|
|
// the cumulative offset. The audio stream must resume at the absolute pos.
|
|
expect(computeHandoffPosition(12, 180)).toBe(192);
|
|
});
|
|
|
|
it("handles a direct stream with no offset", () => {
|
|
expect(computeHandoffPosition(45, 0)).toBe(45);
|
|
});
|
|
|
|
it("never returns a negative position", () => {
|
|
expect(computeHandoffPosition(-5, 0)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("shouldEnterBackgroundAudio", () => {
|
|
it("enters when toggle is on and not already handed off", () => {
|
|
expect(shouldEnterBackgroundAudio(true, initialHandoffState)).toBe(true);
|
|
});
|
|
|
|
it("does not enter when the toggle is off", () => {
|
|
expect(shouldEnterBackgroundAudio(false, initialHandoffState)).toBe(false);
|
|
});
|
|
|
|
it("does not double-enter when already active", () => {
|
|
const active: BackgroundAudioState = { active: true, wasPlaying: true };
|
|
expect(shouldEnterBackgroundAudio(true, active)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("shouldExitBackgroundAudio", () => {
|
|
it("exits when a handoff is active", () => {
|
|
const active: BackgroundAudioState = { active: true, wasPlaying: false };
|
|
expect(shouldExitBackgroundAudio(active)).toBe(true);
|
|
});
|
|
|
|
it("does not exit when no handoff happened", () => {
|
|
expect(shouldExitBackgroundAudio(initialHandoffState)).toBe(false);
|
|
});
|
|
|
|
it("exits even if the toggle was turned off while backgrounded", () => {
|
|
// shouldExit ignores the toggle by design, so turning it off mid-background
|
|
// still returns cleanly to video on foreground.
|
|
const active: BackgroundAudioState = { active: true, wasPlaying: true };
|
|
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);
|
|
});
|
|
});
|
|
});
|