/** * NativePlayerAdapter — the Android/ExoPlayer PlayerAdapter implementation. * * ExoPlayer is driven entirely by the Rust backend (JNI), which already emits * PlayerStatusEvents and handles seek/audio-track internally. So this adapter is * a thin delegate to backend commands; there is no DOM element to touch and no * hls.js. State reporting is unnecessary here because the native backend emits * events directly — the adapter's job is only to forward control intents. * * NOTE: On current Tauri, native Android video rendering is blocked upstream * (transparent webview / SurfaceView compositing — tauri#10152), so video on * Android currently runs through the HTML5 adapter via the interim override in * the factory. This adapter exists for the audio/native path and for when that * upstream limitation is resolved. * * TRACES: UR-003, UR-005 | DR-004, DR-028 */ import { commands } from "$lib/api/bindings"; import type { AdapterHost, PlayerAdapter, PlayerLoadOptions } from "./types"; export class NativePlayerAdapter implements PlayerAdapter { readonly kind = "native" as const; // Kept for symmetry / future reporting needs; the native backend emits events. private host: AdapterHost; private position = 0; constructor(host: AdapterHost) { this.host = host; } // The native surface is owned by the backend; nothing to attach in the DOM. attach(_element: HTMLVideoElement | null): void {} async load(_streamUrl: string, options: PlayerLoadOptions): Promise { // player_play_item already initiated native playback before this adapter is // created; nothing further to do. Seed a resume position if requested (the // native backend performs the actual seek internally). if (options.initialPosition > 0) { this.position = options.initialPosition; } } async play(): Promise { await commands.playerPlay(); } async pause(): Promise { await commands.playerPause(); } async toggle(): Promise { const response = (await commands.playerToggle()) as any; return response?.state === "playing"; } /** * PRIMITIVE: in-place seek. For the native backend, the backend drives * ExoPlayer's seek internally, so this simply records the target position. * (The decision to seek-in-place vs reload was already made by the backend.) */ async seekElement(positionSeconds: number, _offset: number): Promise { this.position = positionSeconds; } /** * PRIMITIVE: reload source. For the native backend the backend already * performed the reload+seek internally as part of the seek decision; nothing * to do on the frontend beyond recording position. */ async reloadSource(_url: string, offset: number): Promise { this.position = offset; } setVolume(volume: number): void { void commands.playerSetVolume(Math.max(0, Math.min(1, volume))); } setMuted(_muted: boolean): void { void commands.playerToggleMute(); } async selectSubtitle(streamIndex: number | null, arrayIndex?: number): Promise { const indexToUse = streamIndex === null ? null : arrayIndex ?? streamIndex; await commands.playerSetSubtitleTrack(indexToUse); } getPosition(): number { return this.position; } async dispose(): Promise { // The backend is stopped via player_stop by the owning view; nothing to free. } }