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.
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
/**
|
|
* Pure decisions for the `/player/[id]` route: which player surface to render,
|
|
* and whether a load can be skipped because the backend is already playing the
|
|
* requested item.
|
|
*
|
|
* Kept free of Svelte so both can be unit-tested without mounting the route.
|
|
*
|
|
* TRACES: UR-005 | DR-100 | UT-092, UT-093
|
|
*/
|
|
|
|
/** Which player component the route should render. */
|
|
export type PlayerSurface = "video" | "audio" | "pending";
|
|
|
|
export interface ReuseActivePlaybackInput {
|
|
/** Item id the route was asked to play. */
|
|
requestedId: string;
|
|
/** Id of the media the backend currently reports as loaded, if any. */
|
|
activeMediaId: string | null | undefined;
|
|
/** Whether the requested item is video content. */
|
|
isVideo: boolean;
|
|
/** Explicit start position, if the caller asked for one. */
|
|
startPosition?: number;
|
|
/** Advancing to a next episode always restarts from the beginning. */
|
|
forceRestart: boolean;
|
|
}
|
|
|
|
/**
|
|
* Whether the route can show its UI over the backend's existing playback
|
|
* instead of reloading the item (e.g. expanding the audio mini player).
|
|
*
|
|
* Never for video. The shortcut returns before a stream URL is fetched, which
|
|
* is fine for audio (the backend owns the stream and the UI only mirrors it)
|
|
* but leaves `<VideoPlayer>` with nothing to render. Leaving a webview-rendered
|
|
* video does not clear the Rust controller's media — closing the route emits no
|
|
* `stopped` state by design — so re-entering the same movie/episode hit this
|
|
* shortcut and rendered the audio player instead.
|
|
*/
|
|
export function shouldReuseActivePlayback(input: ReuseActivePlaybackInput): boolean {
|
|
return (
|
|
!input.isVideo &&
|
|
input.activeMediaId === input.requestedId &&
|
|
!input.startPosition &&
|
|
!input.forceRestart
|
|
);
|
|
}
|
|
|
|
export interface PlayerSurfaceInput {
|
|
isVideo: boolean;
|
|
streamUrl: string | null;
|
|
}
|
|
|
|
/**
|
|
* Which surface to render for the loaded item.
|
|
*
|
|
* Video without a stream URL is `pending`, never `audio` — falling through to
|
|
* the audio player is how a movie/episode ended up in it.
|
|
*/
|
|
export function resolvePlayerSurface(input: PlayerSurfaceInput): PlayerSurface {
|
|
if (input.isVideo) {
|
|
return input.streamUrl ? "video" : "pending";
|
|
}
|
|
return "audio";
|
|
}
|