import { streamUrl, getPlaybackOptions } from '../embyApi.js'; import { getParty, leaveParty, connectParty } from '../party.js'; import { navigate } from '../router.js'; import { escapeHtml } from '../tiles.js'; const TICKS_PER_SECOND = 10000000; // Emby/.NET convention: 1 tick = 100ns const DRIFT_TOLERANCE_SECONDS = 1.5; // "Quality" is really "max transcoding bitrate", the same simplification // Emby's/Jellyfin's own web clients use - not true adaptive-bitrate // switching. See docs/architecture.md. const QUALITY_PRESETS = [ { label: 'Auto (qualité source)', value: '' }, { label: '1080p (~8 Mbps)', value: '8000000' }, { label: '720p (~4 Mbps)', value: '4000000' }, { label: '480p (~1.5 Mbps)', value: '1500000' } ]; export function renderPartyPlayer(root, params) { const partyId = params.id; root.innerHTML = `

Chargement du flux...

Code:

L'hôte contrôle la lecture.

`; const video = root.querySelector('#video'); const loadingNotice = root.querySelector('#loadingNotice'); const errorBox = root.querySelector('#playerError'); const errorText = root.querySelector('#playerErrorText'); const itemTitleEl = root.querySelector('#itemTitle'); const codeEl = root.querySelector('#code'); const membersEl = root.querySelector('#members'); const nextBtn = root.querySelector('#nextBtn'); const followerNotice = root.querySelector('#followerNotice'); const settingsWrap = root.querySelector('#settingsWrap'); const settingsBtn = root.querySelector('#settingsBtn'); const settingsPanel = root.querySelector('#settingsPanel'); const audioSelect = root.querySelector('#audioSelect'); const subtitleSelect = root.querySelector('#subtitleSelect'); const qualitySelect = root.querySelector('#qualitySelect'); qualitySelect.innerHTML = QUALITY_PRESETS.map((q) => ``).join(''); settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); settingsPanel.classList.toggle('hide'); }); document.addEventListener('click', (e) => { if (!settingsWrap.contains(e.target)) settingsPanel.classList.add('hide'); }); let isHost = false; let ws = null; let hls = null; let currentItemId = null; let suppressLocalEvents = false; // avoid feeding our own correction back as a new event let heartbeat = null; let hasNextQueued = false; function showError(message, detail) { loadingNotice.classList.add('hide'); errorText.textContent = message; errorBox.classList.remove('hide'); console.error('[watchparty player]', message, detail || ''); } function clearError() { errorBox.classList.add('hide'); } 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 loadTrackOptions(itemId) { try { const options = await getPlaybackOptions(itemId); audioSelect.innerHTML = options.audioTracks.length ? options.audioTracks.map((t) => ``).join('') : ''; subtitleSelect.innerHTML = '' + options.subtitleTracks.map((t) => ``).join(''); } catch (err) { console.error('[watchparty player] playback-options failed', err); // Non-fatal: playback can still work with Emby's own defaults, just without a picker. } } function loadVideo(itemId, resumeSeconds, wasPlaying) { currentItemId = itemId; clearError(); loadingNotice.classList.remove('hide'); if (hls) { hls.destroy(); hls = null; } const src = streamUrl(itemId, { AudioStreamIndex: audioSelect.value, SubtitleStreamIndex: subtitleSelect.value, MaxStreamingBitrate: qualitySelect.value }); const resume = () => { loadingNotice.classList.add('hide'); if (resumeSeconds) video.currentTime = resumeSeconds; if (wasPlaying) video.play().catch(() => {}); }; if (window.Hls && window.Hls.isSupported()) { hls = new window.Hls(); hls.on(window.Hls.Events.MANIFEST_PARSED, resume); hls.on(window.Hls.Events.ERROR, (event, data) => { if (!data.fatal) return; showError( "Erreur de lecture (HLS) - le flux n'a pas pu être chargé depuis Emby.", data ); }); hls.loadSource(src); hls.attachMedia(video); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // Safari plays HLS natively - no hls.js involved, so wire the plain //