import { getItem, streamUrl, session } from '../embyApi.js'; import { getParty, leaveParty, connectParty } from '../party.js'; import { navigate } from '../router.js'; const TICKS_PER_SECOND = 10000000; // Emby/.NET convention: 1 tick = 100ns const DRIFT_TOLERANCE_SECONDS = 1.5; export function renderPartyPlayer(root, params) { const partyId = params.id; root.innerHTML = `
Code:

L'hôte contrôle la lecture.

`; const video = root.querySelector('#video'); const codeEl = root.querySelector('#code'); const membersEl = root.querySelector('#members'); const followerNotice = root.querySelector('#followerNotice'); let isHost = false; let ws = null; let suppressLocalEvents = false; // avoid feeding our own correction back as a new event let heartbeat = null; function applyRemoteState(msg) { suppressLocalEvents = true; const targetSeconds = msg.positionTicks / TICKS_PER_SECOND; if (Math.abs(video.currentTime - targetSeconds) > DRIFT_TOLERANCE_SECONDS) { video.currentTime = targetSeconds; } if (msg.isPaused && !video.paused) video.pause(); if (!msg.isPaused && video.paused) video.play().catch(() => {}); setTimeout(() => { suppressLocalEvents = false; }, 250); } function sendLocalState() { if (!isHost || suppressLocalEvents || !ws) return; ws.sendPlaystate(video.paused, Math.round(video.currentTime * TICKS_PER_SECOND)); } async function init() { let party; try { party = await getParty(partyId); } catch (err) { alert(err.message); navigate('/browse'); return; } isHost = party.hostUserId === session.userId; codeEl.textContent = party.joinCode; video.controls = isHost; followerNotice.classList.toggle('hide', isHost); const item = await getItem(party.itemId); const src = streamUrl(item.Id); if (window.Hls && window.Hls.isSupported()) { const hls = new window.Hls(); hls.loadSource(src); hls.attachMedia(video); } else { video.src = src; } ws = connectParty(partyId, { onMembers: renderMembers, onPlaystate: applyRemoteState, onError: (message) => alert(message), onClose: () => {} }); if (isHost) { video.addEventListener('play', sendLocalState); video.addEventListener('pause', sendLocalState); video.addEventListener('seeked', sendLocalState); heartbeat = setInterval(sendLocalState, 3000); } } function renderMembers(members) { membersEl.textContent = members .map((m) => m.userName + (m.isHost ? ' (hôte)' : '')) .join(', '); } root.querySelector('#leaveBtn').addEventListener('click', async () => { if (heartbeat) clearInterval(heartbeat); if (ws) ws.leave(); await leaveParty(partyId).catch(() => {}); navigate('/browse'); }); init(); }