fix(player): re-entering a video no longer opens the audio player (DR-100)
Leaving a video and returning to it rendered the movie/episode in AudioPlayer. Closing a webview-rendered video deliberately emits no "stopped" state (that would break the autoplay handoff), and the direct-play path does not stop the backend on unmount, so the Rust controller still reported that item as its loaded media. Re-entering the route therefore took the "already playing, just show the UI" shortcut, which returns before a stream URL is fetched, and the render fell through to the audio surface. Mostly visible on Android, where video direct-plays; Linux transcodes and stops the backend on unmount. Both decisions move into playerSurface.ts as pure functions: shouldReuseActivePlayback excludes video, so video always takes the full load path and gets its stream URL and resume position; resolvePlayerSurface maps video-without-a-stream-URL to "pending" (spinner) rather than falling through to audio.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Regression tests for the `/player/[id]` surface decision.
|
||||
*
|
||||
* The bug these pin down: a video that was left and re-entered rendered in the
|
||||
* AUDIO player. Exiting a webview-rendered video does not stop the Rust
|
||||
* controller (`onReportStop` deliberately emits no `stopped` state, so the
|
||||
* autoplay handoff survives), so the backend still reports that episode/movie as
|
||||
* the loaded media. Re-entering the route therefore took the "already playing,
|
||||
* just show the UI" shortcut, which returns *before* a stream URL is fetched —
|
||||
* and the render then fell through to `<AudioPlayer>` because it treated
|
||||
* "video without a stream URL" as audio.
|
||||
*
|
||||
* TRACES: UR-005 | DR-100 | UT-092, UT-093
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { shouldReuseActivePlayback, resolvePlayerSurface } from "./playerSurface";
|
||||
|
||||
describe("shouldReuseActivePlayback", () => {
|
||||
it("reuses playback when the same audio track is already loaded", () => {
|
||||
expect(
|
||||
shouldReuseActivePlayback({
|
||||
requestedId: "track-1",
|
||||
activeMediaId: "track-1",
|
||||
isVideo: false,
|
||||
forceRestart: false,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does NOT reuse playback for video, even when the backend reports it loaded", () => {
|
||||
// Video needs a full load: the shortcut skips fetching the stream URL, and
|
||||
// <VideoPlayer> cannot render without one.
|
||||
expect(
|
||||
shouldReuseActivePlayback({
|
||||
requestedId: "episode-1",
|
||||
activeMediaId: "episode-1",
|
||||
isVideo: true,
|
||||
forceRestart: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not reuse playback for a different item", () => {
|
||||
expect(
|
||||
shouldReuseActivePlayback({
|
||||
requestedId: "track-2",
|
||||
activeMediaId: "track-1",
|
||||
isVideo: false,
|
||||
forceRestart: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not reuse playback when nothing is loaded", () => {
|
||||
expect(
|
||||
shouldReuseActivePlayback({
|
||||
requestedId: "track-1",
|
||||
activeMediaId: null,
|
||||
isVideo: false,
|
||||
forceRestart: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not reuse playback when an explicit start position is requested", () => {
|
||||
expect(
|
||||
shouldReuseActivePlayback({
|
||||
requestedId: "track-1",
|
||||
activeMediaId: "track-1",
|
||||
isVideo: false,
|
||||
startPosition: 42,
|
||||
forceRestart: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not reuse playback when restarting (next-episode advance)", () => {
|
||||
expect(
|
||||
shouldReuseActivePlayback({
|
||||
requestedId: "episode-2",
|
||||
activeMediaId: "episode-2",
|
||||
isVideo: true,
|
||||
forceRestart: true,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePlayerSurface", () => {
|
||||
it("renders the video surface for video with a stream URL", () => {
|
||||
expect(resolvePlayerSurface({ isVideo: true, streamUrl: "http://s/master.m3u8" })).toBe(
|
||||
"video"
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the audio surface for audio content", () => {
|
||||
expect(resolvePlayerSurface({ isVideo: false, streamUrl: null })).toBe("audio");
|
||||
});
|
||||
|
||||
it("never renders video content in the audio surface when the stream URL is missing", () => {
|
||||
// A video whose stream URL has not resolved yet is pending, not audio —
|
||||
// otherwise the movie/episode shows up in the audio player.
|
||||
expect(resolvePlayerSurface({ isVideo: true, streamUrl: null })).toBe("pending");
|
||||
expect(resolvePlayerSurface({ isVideo: true, streamUrl: "" })).toBe("pending");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user