Adds WebviewAudioBackend, used on non-Linux/non-Android targets (e.g. Windows) where there is no libmpv/ExoPlayer. Instead of decoding, it emits a WebviewAudioLoad event with the stream URL; a frontend <audio> element (WebviewAudioAdapter + webviewAudio service) plays it and reports state/position back through the existing player_report_* round-trip, so the Rust PlayerController stays the single source of truth. Play/pause/ seek reach the element via the existing ControlCommand event. All video already renders in the webview on every platform, so this completes audio-only playback for Windows (video via WebView2, audio via <audio>). Pure Rust + Tauri events, so it still cross-compiles from Linux. Regenerates bindings.ts (adds webview_audio_load; also carries the equalizer EQ bindings). TRACES: UR-003, UR-004, UR-005 | DR-004
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
/**
|
|
* Webview audio controller — the frontend half of audio-only playback on
|
|
* platforms with no native audio backend (currently Windows).
|
|
*
|
|
* The Rust `WebviewAudioBackend` emits a `webview_audio_load` event carrying the
|
|
* stream URL whenever a track loads. This controller owns a single hidden
|
|
* `<audio>` element, plays that URL through a {@link WebviewAudioAdapter}, and
|
|
* registers the adapter with the player facade so backend `control_command`
|
|
* events (play/pause/seek — routed by playerEvents.ts) reach the element. The
|
|
* adapter reports state/position back through the standard `player_report_*`
|
|
* round-trip, keeping the Rust controller the single source of truth.
|
|
*
|
|
* No-op on platforms with a native audio backend (Linux/Android): the backend
|
|
* never emits `webview_audio_load` there, so even if initialized this listener
|
|
* stays idle. We still gate initialization on platform to avoid mounting a stray
|
|
* element.
|
|
*
|
|
* TRACES: UR-003, UR-005 | DR-004
|
|
*/
|
|
|
|
import { type UnlistenFn } from "@tauri-apps/api/event";
|
|
import { events } from "$lib/api/bindings";
|
|
import { playerController } from "$lib/player";
|
|
import { createRustReportHost } from "$lib/player/adapters/rustReportHost";
|
|
import { WebviewAudioAdapter } from "$lib/player/adapters/webviewAudioAdapter";
|
|
|
|
let unlisten: UnlistenFn | null = null;
|
|
let audioEl: HTMLAudioElement | null = null;
|
|
let adapter: WebviewAudioAdapter | null = null;
|
|
|
|
/** Platforms whose Rust backend renders audio in the webview rather than natively. */
|
|
function usesWebviewAudio(): boolean {
|
|
// Native audio backends exist only for Linux (mpv) and Android (ExoPlayer).
|
|
// Everything else (Windows, and any future desktop) uses the webview element.
|
|
// We detect "not linux/android" rather than "is windows" so new desktop
|
|
// targets are covered automatically, matching the Rust cfg gate.
|
|
if (typeof navigator === "undefined") return false;
|
|
const ua = navigator.userAgent.toLowerCase();
|
|
const isAndroid = ua.includes("android");
|
|
const isLinux = ua.includes("linux") && !isAndroid;
|
|
return !isAndroid && !isLinux;
|
|
}
|
|
|
|
/**
|
|
* Initialize the webview audio controller. Safe to call unconditionally from the
|
|
* root layout; it self-gates on platform and is idempotent.
|
|
*/
|
|
export async function initWebviewAudio(): Promise<void> {
|
|
if (unlisten) return;
|
|
if (!usesWebviewAudio()) return;
|
|
|
|
audioEl = document.createElement("audio");
|
|
audioEl.hidden = true;
|
|
audioEl.preload = "auto";
|
|
// Kept in the DOM so the browser keeps decoding it when not focused.
|
|
document.body.appendChild(audioEl);
|
|
|
|
unlisten = await events.playerStatusEvent.listen((event) => {
|
|
const p = event.payload;
|
|
if (p.type !== "webview_audio_load") return;
|
|
void handleLoad(p.url, p.media_id, p.position, p.autoplay);
|
|
});
|
|
}
|
|
|
|
async function handleLoad(
|
|
url: string,
|
|
mediaId: string | null,
|
|
position: number,
|
|
autoplay: boolean
|
|
): Promise<void> {
|
|
if (!audioEl) return;
|
|
|
|
// Fresh host/adapter per load so reporting targets the current media id.
|
|
const host = createRustReportHost(mediaId ?? "", {});
|
|
adapter = new WebviewAudioAdapter(audioEl, host);
|
|
playerController.setActiveAdapter(adapter);
|
|
|
|
await adapter.load(url, {
|
|
mediaId: mediaId ?? "",
|
|
mediaSourceId: null,
|
|
needsTranscoding: false,
|
|
initialPosition: position,
|
|
isLive: false,
|
|
audioTrackIndex: null,
|
|
knownDuration: 0,
|
|
subtitleTracks: [],
|
|
});
|
|
|
|
if (!autoplay) {
|
|
await adapter.pause();
|
|
}
|
|
}
|
|
|
|
/** Tear down the controller (idempotent). */
|
|
export function cleanupWebviewAudio(): void {
|
|
if (unlisten) {
|
|
unlisten();
|
|
unlisten = null;
|
|
}
|
|
if (adapter) {
|
|
playerController.clearActiveAdapter(adapter);
|
|
void adapter.dispose();
|
|
adapter = null;
|
|
}
|
|
if (audioEl) {
|
|
audioEl.remove();
|
|
audioEl = null;
|
|
}
|
|
}
|