Files
pmomusic/pmoapp/webapp/src/composables/useWebRenderer.ts
Eric Coissac 19451068f1 [webapp] Add retry logic and improve renderer registration
- 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)
2026-04-06 00:22:03 +02:00

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,
};
}