2026-02-21 00:15:38 +01:00
|
|
|
/**
|
|
|
|
|
* Composable pour gérer le WebRenderer navigateur.
|
|
|
|
|
*
|
2026-04-05 10:52:54 +02:00
|
|
|
* Utilise PMOPlayer pour le controle remote.
|
|
|
|
|
* S'enregistre via POST /api/webrenderer/register
|
|
|
|
|
* Utilise polling HTTP pour les commands
|
2026-02-21 00:15:38 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { ref, onMounted, onUnmounted, readonly } from "vue";
|
2026-04-05 10:52:54 +02:00
|
|
|
import { PMOPlayer } from "@/services/PMOPlayer";
|
2026-02-21 20:54:22 +01:00
|
|
|
|
|
|
|
|
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)}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
const INSTANCE_ID_KEY = "pmomusic_webrenderer_instance_id";
|
|
|
|
|
|
|
|
|
|
// 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>(null);
|
|
|
|
|
|
2026-02-21 20:54:22 +01:00
|
|
|
function getOrCreateInstanceId(): string {
|
|
|
|
|
try {
|
2026-02-28 14:10:38 +01:00
|
|
|
let id = sessionStorage.getItem(INSTANCE_ID_KEY);
|
2026-02-21 20:54:22 +01:00
|
|
|
if (!id) {
|
|
|
|
|
id = generateUUID();
|
2026-02-28 14:10:38 +01:00
|
|
|
sessionStorage.setItem(INSTANCE_ID_KEY, id);
|
2026-02-21 20:54:22 +01:00
|
|
|
}
|
|
|
|
|
return id;
|
|
|
|
|
} catch {
|
|
|
|
|
return generateUUID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 19:58:37 +01:00
|
|
|
export function useWebRenderer() {
|
2026-04-05 10:52:54 +02:00
|
|
|
const connected = sharedConnected;
|
|
|
|
|
const streamUrl = sharedStreamUrl;
|
|
|
|
|
const rendererUdn = sharedRendererUdn;
|
2026-02-21 10:16:49 +01:00
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
let player: PMOPlayer | null = null;
|
2026-02-26 19:58:37 +01:00
|
|
|
let onConnectedCallback: (() => void) | null = null;
|
2026-02-28 12:45:55 +01:00
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
async function register(): Promise<void> {
|
|
|
|
|
// Prevent concurrent registrations (race condition → double player)
|
|
|
|
|
if (globalPlayer || registering) {
|
|
|
|
|
if (globalPlayer) {
|
|
|
|
|
player = globalPlayer;
|
|
|
|
|
connected.value = true;
|
2026-02-21 10:16:49 +01:00
|
|
|
}
|
2026-04-05 10:52:54 +02:00
|
|
|
return;
|
2026-02-21 00:15:38 +01:00
|
|
|
}
|
2026-04-05 10:52:54 +02:00
|
|
|
registering = true;
|
2026-02-21 00:15:38 +01:00
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
const instanceId = getOrCreateInstanceId();
|
|
|
|
|
console.log('[WebRenderer] registering with instanceId:', instanceId);
|
2026-02-21 00:15:38 +01:00
|
|
|
|
2026-02-26 19:58:37 +01:00
|
|
|
try {
|
|
|
|
|
const resp = await fetch("/api/webrenderer/register", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
2026-04-05 10:52:54 +02:00
|
|
|
body: JSON.stringify({
|
|
|
|
|
instance_id: instanceId,
|
|
|
|
|
user_agent: navigator.userAgent,
|
|
|
|
|
}),
|
2026-02-21 10:16:49 +01:00
|
|
|
});
|
2026-02-21 00:15:38 +01:00
|
|
|
|
2026-02-26 19:58:37 +01:00
|
|
|
if (!resp.ok) {
|
|
|
|
|
console.error("[WebRenderer] register failed:", resp.status);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-21 00:15:38 +01:00
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
const data = await resp.json();
|
2026-02-26 19:58:37 +01:00
|
|
|
streamUrl.value = data.stream_url;
|
|
|
|
|
rendererUdn.value = data.udn;
|
|
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
player = new PMOPlayer(instanceId);
|
|
|
|
|
globalPlayer = player;
|
|
|
|
|
globalInstanceId = instanceId;
|
|
|
|
|
player.setDebug(true);
|
2026-02-26 19:58:37 +01:00
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
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?.();
|
2026-02-26 19:58:37 +01:00
|
|
|
} catch (e) {
|
|
|
|
|
console.error("[WebRenderer] register error:", e);
|
2026-04-05 10:52:54 +02:00
|
|
|
} finally {
|
|
|
|
|
registering = false;
|
2026-02-21 00:15:38 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 19:58:37 +01:00
|
|
|
async function unregister(): Promise<void> {
|
2026-04-05 10:52:54 +02:00
|
|
|
// 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;
|
|
|
|
|
|
2026-02-21 00:15:38 +01:00
|
|
|
try {
|
2026-02-26 19:58:37 +01:00
|
|
|
await fetch(`/api/webrenderer/${instanceId}`, { method: "DELETE" });
|
2026-02-21 00:15:38 +01:00
|
|
|
} catch {
|
2026-04-05 10:52:54 +02:00
|
|
|
// Ignored
|
2026-02-21 00:15:38 +01:00
|
|
|
}
|
2026-04-05 10:52:54 +02:00
|
|
|
player?.destroy();
|
|
|
|
|
player = null;
|
2026-02-21 00:15:38 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
function setVolume(_v: number): void {
|
|
|
|
|
// PMOPlayer doesn't control volume directly - handled by backend
|
2026-02-21 00:15:38 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 10:52:54 +02:00
|
|
|
function setMute(_m: boolean): void {
|
|
|
|
|
// PMOPlayer doesn't control mute directly - handled by backend
|
2026-02-21 00:15:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-02-26 19:58:37 +01:00
|
|
|
void register();
|
|
|
|
|
window.addEventListener("beforeunload", () => void unregister());
|
2026-02-21 00:15:38 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2026-02-26 19:58:37 +01:00
|
|
|
void unregister();
|
|
|
|
|
window.removeEventListener("beforeunload", () => void unregister());
|
2026-02-21 00:15:38 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
connected: readonly(connected),
|
2026-02-26 19:58:37 +01:00
|
|
|
streamUrl: readonly(streamUrl),
|
|
|
|
|
rendererUdn: readonly(rendererUdn),
|
2026-02-21 00:15:38 +01:00
|
|
|
onConnected(fn: () => void) {
|
|
|
|
|
onConnectedCallback = fn;
|
|
|
|
|
},
|
2026-02-26 19:58:37 +01:00
|
|
|
setVolume,
|
|
|
|
|
setMute,
|
2026-02-21 00:15:38 +01:00
|
|
|
};
|
2026-04-05 10:52:54 +02:00
|
|
|
}
|