- Added automatic retries with exponential backoff to fetchRenderers - Improved WebRenderer registration: handle stale players, avoid duplicate registrations, and force renderers refresh after successful connection - Bumped version to v0.3.37 (Cargo: 0.3.36 → Cargo.lock mismatch fixed by updating to final version)
214 lines
6.6 KiB
TypeScript
214 lines
6.6 KiB
TypeScript
/**
|
|
* Composable pour gérer le WebRenderer navigateur.
|
|
*
|
|
* Utilise PMOPlayer pour le controle remote.
|
|
* S'enregistre via POST /api/webrenderer/register
|
|
* Utilise polling HTTP pour les commands
|
|
*/
|
|
|
|
import { ref, onMounted, onUnmounted, readonly } from "vue";
|
|
import { PMOPlayer } from "@/services/PMOPlayer";
|
|
|
|
function generateUUID(): string {
|
|
if (typeof crypto.randomUUID === "function") {
|
|
return crypto.randomUUID();
|
|
}
|
|
const bytes = new Uint8Array(16);
|
|
crypto.getRandomValues(bytes);
|
|
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
|
|
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
|
|
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
}
|
|
|
|
const INSTANCE_ID_KEY = "pmomusic_webrenderer_instance_id";
|
|
const RENDERER_UDN_KEY = "pmomusic_webrenderer_udn";
|
|
|
|
// Load persisted UDN on module load
|
|
function loadPersistedUdn(): string | null {
|
|
try {
|
|
return sessionStorage.getItem(RENDERER_UDN_KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function savePersistedUdn(udn: string) {
|
|
try {
|
|
sessionStorage.setItem(RENDERER_UDN_KEY, udn);
|
|
} catch {
|
|
// ignore storage errors
|
|
}
|
|
}
|
|
|
|
// Module-level singleton for PMOPlayer to prevent duplicate instances
|
|
let globalPlayer: PMOPlayer | null = null;
|
|
let globalInstanceId: string | null = null;
|
|
let registering = false;
|
|
|
|
// Module-level reactive state shared across all composable invocations
|
|
const sharedConnected = ref(false);
|
|
const sharedStreamUrl = ref<string | null>(null);
|
|
const sharedRendererUdn = ref<string | null>(loadPersistedUdn());
|
|
|
|
function getOrCreateInstanceId(): string {
|
|
try {
|
|
let id = sessionStorage.getItem(INSTANCE_ID_KEY);
|
|
if (!id) {
|
|
id = generateUUID();
|
|
sessionStorage.setItem(INSTANCE_ID_KEY, id);
|
|
}
|
|
return id;
|
|
} catch {
|
|
return generateUUID();
|
|
}
|
|
}
|
|
|
|
export function useWebRenderer() {
|
|
const connected = sharedConnected;
|
|
const streamUrl = sharedStreamUrl;
|
|
const rendererUdn = sharedRendererUdn;
|
|
|
|
let player: PMOPlayer | null = null;
|
|
let onConnectedCallback: (() => void) | null = null;
|
|
|
|
async function register(): Promise<void> {
|
|
// Allow re-registration if we have a stale player but no connection
|
|
if (globalPlayer && !connected.value) {
|
|
console.log('[WebRenderer] stale globalPlayer, clearing');
|
|
globalPlayer = null;
|
|
}
|
|
|
|
if (globalPlayer) {
|
|
player = globalPlayer;
|
|
connected.value = true;
|
|
console.log('[WebRenderer] reusing existing player');
|
|
return;
|
|
}
|
|
|
|
if (registering) {
|
|
console.log('[WebRenderer] already registering, waiting...');
|
|
// Wait a bit and try once
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
if (globalPlayer) {
|
|
player = globalPlayer;
|
|
connected.value = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
registering = true;
|
|
|
|
let instanceId;
|
|
try {
|
|
instanceId = getOrCreateInstanceId();
|
|
} catch (e) {
|
|
console.error('[WebRenderer] failed to get instanceId:', e);
|
|
registering = false;
|
|
return;
|
|
}
|
|
|
|
console.log('[WebRenderer] registering with instanceId:', instanceId);
|
|
|
|
try {
|
|
const resp = await fetch("/api/webrenderer/register", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
instance_id: instanceId,
|
|
user_agent: navigator.userAgent,
|
|
}),
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
console.error("[WebRenderer] register failed:", resp.status);
|
|
return;
|
|
}
|
|
|
|
const data = await resp.json();
|
|
streamUrl.value = data.stream_url;
|
|
rendererUdn.value = data.udn;
|
|
savePersistedUdn(data.udn);
|
|
|
|
player = new PMOPlayer(instanceId);
|
|
globalPlayer = player;
|
|
globalInstanceId = instanceId;
|
|
player.setDebug(true);
|
|
|
|
player.on('play', () => console.log('[WebRenderer] playing'));
|
|
player.on('pause', () => console.log('[WebRenderer] paused'));
|
|
|
|
// Si le backend est déjà en lecture (reconnexion après reload), démarrer immédiatement
|
|
if (data.should_play && data.stream_url) {
|
|
console.log('[WebRenderer] backend already playing, starting stream');
|
|
player.playStream(data.stream_url);
|
|
}
|
|
|
|
connected.value = true;
|
|
onConnectedCallback?.();
|
|
|
|
// Force refresh renderers list after registration
|
|
// The SSE might miss the initial event
|
|
setTimeout(() => {
|
|
console.log('[WebRenderer] forcing renderers refresh');
|
|
window.dispatchEvent(new CustomEvent('webrenderer-registered'));
|
|
}, 500);
|
|
} catch (e) {
|
|
console.error("[WebRenderer] register error:", e);
|
|
} finally {
|
|
registering = false;
|
|
}
|
|
}
|
|
|
|
async function unregister(): Promise<void> {
|
|
// Only unregister if this is the global player - prevent duplicate unregister calls
|
|
if (player !== globalPlayer || !player) {
|
|
return;
|
|
}
|
|
|
|
const instanceId = globalInstanceId || getOrCreateInstanceId();
|
|
|
|
// Clear global first to prevent other components from using it
|
|
globalPlayer = null;
|
|
globalInstanceId = null;
|
|
|
|
try {
|
|
await fetch(`/api/webrenderer/${instanceId}`, { method: "DELETE" });
|
|
} catch {
|
|
// Ignored
|
|
}
|
|
player?.destroy();
|
|
player = null;
|
|
}
|
|
|
|
function setVolume(_v: number): void {
|
|
// PMOPlayer doesn't control volume directly - handled by backend
|
|
}
|
|
|
|
function setMute(_m: boolean): void {
|
|
// PMOPlayer doesn't control mute directly - handled by backend
|
|
}
|
|
|
|
const beforeUnloadHandler = () => void unregister();
|
|
|
|
onMounted(() => {
|
|
void register();
|
|
window.addEventListener("beforeunload", beforeUnloadHandler);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
void unregister();
|
|
window.removeEventListener("beforeunload", beforeUnloadHandler);
|
|
});
|
|
|
|
return {
|
|
connected: readonly(connected),
|
|
streamUrl: readonly(streamUrl),
|
|
rendererUdn: readonly(rendererUdn),
|
|
onConnected(fn: () => void) {
|
|
onConnectedCallback = fn;
|
|
},
|
|
setVolume,
|
|
setMute,
|
|
};
|
|
} |