Establish a decoupled player boundary so UI and backend interact with video through one contract, with the HTML5 (Linux/interim-Android) and native (ExoPlayer) providers as interchangeable primitive-executor adapters. - PlayerAdapter interface + AdapterHost callback bag (adapters/types.ts): the adapter owns only decision-free element PRIMITIVES (seekElement, reloadSource, play/pause, setVolume, selectSubtitle); it never branches on strategy. - Seek/audio-track DECISIONS stay in Rust (player_seek_video / _switch_audio_track return a strategy); the facade dispatches the chosen primitive to the active adapter. Both providers share the one decision path — logic lives once, in Rust. - Facade holds the active adapter; a new ControlCommand PlayerStatusEvent lets backend control (lockscreen/remote/sleep) drive the webview <video> element. - Html5PlayerAdapter resolves the LIVE element via the bridge (fixes play/pause silently no-opping when the element was re-bound). - Do not emit a "stopped" player state on natural end-of-video: it flipped the player/mode to idle mid-handoff and suppressed next-episode auto-advance under a sleep timer. Jellyfin progress reporting is preserved; the backend's on_video_playback_ended owns the transition. - VideoPlayer net -300 lines (strategy/HLS-reload logic relocated to the adapter). - Adds 20 adapter unit tests; existing suites stay green (vitest 457, cargo 416). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
/**
|
|
* An {@link AdapterHost} implementation that forwards a player adapter's outward
|
|
* lifecycle events into the Rust `PlayerController` via the `player_report_*`
|
|
* commands. The controller re-emits the same `PlayerStatusEvent`s the native
|
|
* backends emit, so the frontend `player` store is fed from ONE pipeline
|
|
* (playerEvents.ts) in both native and HTML5 modes — keeping Rust the single
|
|
* source of truth.
|
|
*
|
|
* This is the sole place that talks to the report commands; adapters depend only
|
|
* on the {@link AdapterHost} interface, never on `commands` directly, which keeps
|
|
* them unit-testable with a mock host.
|
|
*
|
|
* TRACES: UR-003, UR-005 | DR-001, DR-028
|
|
*/
|
|
|
|
import { commands } from "$lib/api/bindings";
|
|
import type { AdapterHost } from "./types";
|
|
|
|
const POSITION_REPORT_INTERVAL_MS = 250;
|
|
|
|
/** Report options that let a caller bypass throttling for discrete events. */
|
|
export interface ReportPositionOptions {
|
|
force?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Low-level report helpers, exported so the legacy `$lib/player/html5Adapter`
|
|
* shim can keep its function-style API while there are still direct callers.
|
|
* Prefer {@link createRustReportHost} for new adapter code.
|
|
*/
|
|
let lastPositionReport = 0;
|
|
|
|
export async function reportState(
|
|
state: "playing" | "paused" | "loading" | "stopped" | "idle",
|
|
mediaId: string | null
|
|
): Promise<void> {
|
|
try {
|
|
await commands.playerReportState(state, mediaId);
|
|
} catch (err) {
|
|
console.warn("[rustReportHost] Failed to report state:", err);
|
|
}
|
|
}
|
|
|
|
export async function reportPosition(
|
|
position: number,
|
|
duration: number,
|
|
{ force = false }: ReportPositionOptions = {}
|
|
): Promise<void> {
|
|
const now = Date.now();
|
|
if (!force && now - lastPositionReport < POSITION_REPORT_INTERVAL_MS) {
|
|
return;
|
|
}
|
|
lastPositionReport = now;
|
|
try {
|
|
await commands.playerReportPosition(position, Number.isFinite(duration) ? duration : 0);
|
|
} catch (err) {
|
|
console.warn("[rustReportHost] Failed to report position:", err);
|
|
}
|
|
}
|
|
|
|
export async function reportMediaLoaded(duration: number): Promise<void> {
|
|
try {
|
|
await commands.playerReportMediaLoaded(Number.isFinite(duration) ? duration : 0);
|
|
} catch (err) {
|
|
console.warn("[rustReportHost] Failed to report media loaded:", err);
|
|
}
|
|
}
|
|
|
|
export function resetReporting(): void {
|
|
lastPositionReport = 0;
|
|
}
|
|
|
|
/**
|
|
* Build an {@link AdapterHost} bound to a specific media id that forwards adapter
|
|
* events to Rust. `onStreamUrlChanged`, `onBuffering`, and `onReady` are wired by
|
|
* the owning view (they affect the `<video src>` / spinner), so this host accepts
|
|
* optional view callbacks and defaults them to no-ops.
|
|
*/
|
|
export function createRustReportHost(
|
|
mediaId: string,
|
|
view: Partial<Pick<AdapterHost, "onStreamUrlChanged" | "onBuffering" | "onReady" | "onEnded" | "onError">> = {}
|
|
): AdapterHost {
|
|
return {
|
|
onState: (state) => void reportState(state, mediaId),
|
|
onPosition: (position, duration) => void reportPosition(position, duration),
|
|
onMediaLoaded: (duration) => void reportMediaLoaded(duration),
|
|
onEnded: view.onEnded ?? (() => {}),
|
|
onError: view.onError ?? ((message) => console.warn("[rustReportHost] adapter error:", message)),
|
|
onStreamUrlChanged: view.onStreamUrlChanged ?? (() => {}),
|
|
onBuffering: view.onBuffering ?? (() => {}),
|
|
onReady: view.onReady ?? (() => {}),
|
|
};
|
|
}
|