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
//