const AnimePlayer = (function() { let _animeId = null; let _currentEpisode = 0; let _entrySource = 'anilist'; let _audioMode = 'sub'; let _isLocal = false; let _malId = null; let _skipBtn = null; let _skipIntervals = []; let _progressUpdated = false; let _animeTitle = "Anime"; let _rpcActive = false; let _rawVideoData = null; let _currentSubtitles = []; let _localEntryId = null; let _totalEpisodes = 0; let _manualExtensionId = null; let _activeSubtitleIndex = -1; let _roomMode = false; let _isRoomHost = false; let _roomWebSocket = null; let hlsInstance = null; let subtitleRenderer = null; let cursorTimeout = null; let settingsPanelActive = false; let _settingsView = 'main'; const els = { wrapper: null, playerWrapper: null, playerContainer: null, video: null, loader: null, loaderText: null, serverSelect: null, extSelect: null, subDubToggle: null, epTitle: null, prevBtn: null, nextBtn: null, mpvBtn: null, downloadBtn: null, downloadModal: null, dlQualityList: null, dlAudioList: null, dlSubsList: null, dlConfirmBtn: null, dlCancelBtn: null, manualMatchBtn: null, // Custom Controls playPauseBtn: null, volumeBtn: null, volumeSlider: null, timeDisplay: null, settingsBtn: null, settingsPanel: null, fullscreenBtn: null, progressContainer: null, progressPlayed: null, progressBuffer: null, progressHandle: null, subtitlesCanvas: null }; function init(animeId, initialSource, isLocal, animeData, roomMode = false) { _roomMode = roomMode; _animeId = animeId; _entrySource = initialSource || 'anilist'; _isLocal = isLocal; _totalEpisodes = animeData.episodes || 1000; if (animeData.title) { _animeTitle = animeData.title.romaji || animeData.title.english || "Anime"; } initElements(); setupEventListeners(); if (_roomMode) { if(els.playerWrapper) { els.playerWrapper.style.display = 'block'; els.playerWrapper.classList.add('room-mode'); } } else { loadExtensionsList(); } } function initElements() { els.wrapper = document.getElementById('hero-wrapper'); els.playerWrapper = document.getElementById('player-wrapper'); els.playerContainer = els.playerWrapper?.querySelector('.player-container'); els.video = document.getElementById('player'); els.loader = document.getElementById('player-loading'); els.loaderText = document.getElementById('player-loading-text'); // Header controls els.downloadBtn = document.getElementById('download-btn'); els.downloadModal = document.getElementById('download-modal'); els.dlQualityList = document.getElementById('dl-quality-list'); els.dlAudioList = document.getElementById('dl-audio-list'); els.dlSubsList = document.getElementById('dl-subs-list'); els.dlConfirmBtn = document.getElementById('confirm-dl-btn'); els.dlCancelBtn = document.getElementById('cancel-dl-btn'); els.manualMatchBtn = document.getElementById('manual-match-btn'); els.mpvBtn = document.getElementById('mpv-btn'); els.serverSelect = document.getElementById('server-select'); els.extSelect = document.getElementById('extension-select'); els.subDubToggle = document.getElementById('sd-toggle'); els.epTitle = document.getElementById('player-episode-title'); els.prevBtn = document.getElementById('prev-ep-btn'); els.nextBtn = document.getElementById('next-ep-btn'); // Custom controls els.playPauseBtn = document.getElementById('play-pause-btn'); els.volumeBtn = document.getElementById('volume-btn'); els.volumeSlider = document.getElementById('volume-slider'); els.timeDisplay = document.getElementById('time-display'); els.settingsBtn = document.getElementById('settings-btn'); els.settingsPanel = document.getElementById('settings-panel'); els.fullscreenBtn = document.getElementById('fullscreen-btn'); els.progressContainer = document.querySelector('.progress-container'); els.progressPlayed = document.querySelector('.progress-played'); els.progressBuffer = document.querySelector('.progress-buffer'); els.progressHandle = document.querySelector('.progress-handle'); els.subtitlesCanvas = document.getElementById('subtitles-canvas'); if (!document.getElementById('skip-overlay-btn')) { const btn = document.createElement('button'); btn.id = 'skip-overlay-btn'; if(els.playerContainer) els.playerContainer.appendChild(btn); _skipBtn = btn; } else { _skipBtn = document.getElementById('skip-overlay-btn'); } } function setupEventListeners() { if(!_roomMode) { const closeBtn = document.getElementById('close-player-btn'); if(closeBtn) closeBtn.addEventListener('click', closePlayer); } if(els.prevBtn) els.prevBtn.onclick = () => playEpisode(_currentEpisode - 1); if(els.nextBtn) els.nextBtn.onclick = () => playEpisode(_currentEpisode + 1); // Skip button if(_skipBtn) _skipBtn.onclick = () => handleOverlayClick(); // Audio mode toggle if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode); // Server/Extension changes if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream()); if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true)); // Manual match if (els.manualMatchBtn) { els.manualMatchBtn.addEventListener('click', openMatchModal); } // Download if (els.downloadBtn) { els.downloadBtn.addEventListener('click', downloadEpisode); } if (els.dlConfirmBtn) els.dlConfirmBtn.onclick = executeDownload; if (els.dlCancelBtn) els.dlCancelBtn.onclick = closeDownloadModal; const closeDlModalBtn = document.getElementById('close-download-modal'); if (closeDlModalBtn) closeDlModalBtn.onclick = closeDownloadModal; if (els.downloadModal) { els.downloadModal.addEventListener('click', (e) => { if (e.target === els.downloadModal) closeDownloadModal(); }); } // MPV if(els.mpvBtn) els.mpvBtn.addEventListener('click', openInMPV); // Custom controls setupCustomControls(); // Cursor management setupCursorManagement(); // Keyboard shortcuts setupKeyboardShortcuts(); } function loadVideoFromRoom(videoData) { console.log('AnimePlayer.loadVideoFromRoom called with:', videoData); if (!videoData || !videoData.url) { console.error('Invalid video data provided to loadVideoFromRoom'); return; } if (videoData.malId) _malId = videoData.malId; if (videoData.episode) _currentEpisode = parseInt(videoData.episode); _skipIntervals = []; if (els.progressContainer) { els.progressContainer.querySelectorAll('.skip-range, .skip-cut').forEach(e => e.remove()); } if (_skipBtn) _skipBtn.classList.remove('visible'); _currentSubtitles = videoData.subtitles || []; if (els.loader) els.loader.style.display = 'none'; initVideoPlayer(videoData.url, videoData.type || 'm3u8', videoData.subtitles || []); } function setupCustomControls() { // ELIMINADO: if (_roomMode && !_isRoomHost) return; // Ahora permitimos que el código fluya para habilitar volumen y ajustes a todos // 1. Play/Pause (SOLO HOST) if(els.playPauseBtn) { els.playPauseBtn.onclick = togglePlayPause; // La validación de permiso se hará dentro de togglePlayPause } if(els.video) { els.video.onclick = togglePlayPause; // Click en video para pausar els.video.ondblclick = toggleFullscreen; // Doble click siempre permitido } // 2. Volume (TODOS) if(els.volumeBtn) { els.volumeBtn.onclick = toggleMute; } if(els.volumeSlider) { els.volumeSlider.oninput = (e) => { setVolume(e.target.value / 100); }; } // 3. Settings (TODOS - Aquí están los subtítulos y audio) if(els.settingsBtn) { els.settingsBtn.onclick = (e) => { e.stopPropagation(); settingsPanelActive = !settingsPanelActive; if (settingsPanelActive) { _settingsView = 'main'; buildSettingsPanel(); els.settingsPanel?.classList.add('active'); } else { els.settingsPanel?.classList.remove('active'); } }; } // Close settings when clicking outside (TODOS) document.onclick = (e) => { if (settingsPanelActive && els.settingsPanel && !els.settingsPanel.contains(e.target) && !els.settingsBtn.contains(e.target)) { settingsPanelActive = false; els.settingsPanel.classList.remove('active'); } }; // 4. Fullscreen (TODOS) if(els.fullscreenBtn) { els.fullscreenBtn.onclick = toggleFullscreen; } // 5. Progress bar (SOLO HOST para buscar, TODOS para ver) if(els.progressContainer) { // El listener se añade, pero seekToPosition bloqueará a los invitados els.progressContainer.onclick = seekToPosition; } // 6. Video events (TODOS - Necesarios para actualizar la UI localmente) if(els.video) { els.video.onplay = onPlay; els.video.onpause = onPause; els.video.ontimeupdate = onTimeUpdate; els.video.onprogress = onProgress; els.video.onloadedmetadata = onLoadedMetadata; els.video.onended = onEnded; els.video.onvolumechange = onVolumeChange; els.video.onseeked = onSeeked; } } function setupCursorManagement() { if (!els.playerContainer) return; const showCursor = () => { els.playerContainer.classList.add('show-cursor'); clearTimeout(cursorTimeout); if (!els.video?.paused) { cursorTimeout = setTimeout(() => { els.playerContainer.classList.remove('show-cursor'); }, 3000); } }; els.playerContainer.addEventListener('mousemove', showCursor); els.playerContainer.addEventListener('mouseenter', showCursor); els.video?.addEventListener('pause', () => { clearTimeout(cursorTimeout); els.playerContainer.classList.add('show-cursor'); }); els.video?.addEventListener('play', () => { showCursor(); }); } function setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { if (!els.playerWrapper || els.playerWrapper.style.display === 'none') return; if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; // En room mode, solo el host puede usar shortcuts de control if (_roomMode && !_isRoomHost) { // Permitir fullscreen y volumen para todos if (e.key.toLowerCase() === 'f') { e.preventDefault(); toggleFullscreen(); } else if (e.key.toLowerCase() === 'm') { e.preventDefault(); toggleMute(); } else if (e.key === 'ArrowUp') { e.preventDefault(); adjustVolume(0.1); } else if (e.key === 'ArrowDown') { e.preventDefault(); adjustVolume(-0.1); } return; } switch(e.key.toLowerCase()) { case ' ': case 'k': e.preventDefault(); togglePlayPause(); break; case 'f': e.preventDefault(); toggleFullscreen(); break; case 'm': e.preventDefault(); toggleMute(); break; case 'arrowleft': e.preventDefault(); seekRelative(-10); break; case 'arrowright': e.preventDefault(); seekRelative(10); break; case 'j': e.preventDefault(); seekRelative(-10); break; case 'l': e.preventDefault(); seekRelative(10); break; case 'arrowup': e.preventDefault(); adjustVolume(0.1); break; case 'arrowdown': e.preventDefault(); adjustVolume(-0.1); break; case 'n': e.preventDefault(); if (_currentEpisode < _totalEpisodes) playEpisode(_currentEpisode + 1); break; case 'p': e.preventDefault(); if (_currentEpisode > 1) playEpisode(_currentEpisode - 1); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': e.preventDefault(); const percent = parseInt(e.key) / 10; seekToPercent(percent); break; } }); } // Control functions function togglePlayPause() { if (_roomMode && !_isRoomHost) { console.log('Guests cannot control playback'); return; } if (!els.video) return; if (els.video.paused) { els.video.play().catch(() => {}); if (_roomMode && _isRoomHost) { sendRoomEvent('play', { currentTime: els.video.currentTime }); } } else { els.video.pause(); if (_roomMode && _isRoomHost) { sendRoomEvent('pause', { currentTime: els.video.currentTime }); } } } function toggleMute() { if (!els.video) return; els.video.muted = !els.video.muted; } function setVolume(vol) { if (!els.video) return; els.video.volume = Math.max(0, Math.min(1, vol)); els.video.muted = vol === 0; } function adjustVolume(delta) { if (!els.video) return; setVolume(els.video.volume + delta); if (els.volumeSlider) { els.volumeSlider.value = els.video.volume * 100; } } function toggleFullscreen() { if (!document.fullscreenElement && !document.webkitFullscreenElement) { const elem = els.playerContainer || els.playerWrapper; if (elem.requestFullscreen) { elem.requestFullscreen(); } else if (elem.webkitRequestFullscreen) { elem.webkitRequestFullscreen(); } } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } } } function seekToPosition(e) { if (!els.video || !els.progressContainer) return; if (_roomMode && !_isRoomHost) return; const rect = els.progressContainer.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; const newTime = pos * els.video.duration; els.video.currentTime = newTime; // En room mode, enviar evento de seek if (_roomMode && _isRoomHost) { sendRoomEvent('seek', { currentTime: newTime }); } } function updateProgressHandle(e) { if (!els.progressHandle || !els.progressContainer) return; const rect = els.progressContainer.getBoundingClientRect(); const pos = (e.clientX - rect.left) / rect.width; els.progressHandle.style.left = `${pos * 100}%`; } function seekRelative(seconds) { if (!els.video) return; if (_roomMode && !_isRoomHost) return; const newTime = Math.max(0, Math.min(els.video.duration, els.video.currentTime + seconds)); els.video.currentTime = newTime; // En room mode, enviar evento de seek if (_roomMode && _isRoomHost) { sendRoomEvent('seek', { currentTime: newTime }); } } function seekToPercent(percent) { if (!els.video) return; if (_roomMode && !_isRoomHost) return; const newTime = els.video.duration * percent; els.video.currentTime = newTime; // En room mode, enviar evento de seek if (_roomMode && _isRoomHost) { sendRoomEvent('seek', { currentTime: newTime }); } } // Video event handlers function onPlay() { if (els.playPauseBtn) { els.playPauseBtn.innerHTML = ` `; } if (!els.video.duration) return; const elapsed = Math.floor(els.video.currentTime); const start = Math.floor(Date.now() / 1000) - elapsed; const end = start + Math.floor(els.video.duration); sendRPC({ startTimestamp: start, endTimestamp: end }); _rpcActive = true; } function onSeeked() { if (!els.video || els.video.paused || !els.video.duration) return; const elapsed = Math.floor(els.video.currentTime); const start = Math.floor(Date.now() / 1000) - elapsed; const end = start + Math.floor(els.video.duration); sendRPC({ startTimestamp: start, endTimestamp: end }); } function onPause() { if (els.playPauseBtn) { els.playPauseBtn.innerHTML = ` `; } if (_rpcActive) sendRPC({ paused: true }); } function onProgress() { if (!els.video || !els.progressBuffer) return; if (els.video.buffered.length > 0) { const bufferedEnd = els.video.buffered.end(els.video.buffered.length - 1); const percent = (bufferedEnd / els.video.duration) * 100; els.progressBuffer.style.width = `${percent}%`; } } function onLoadedMetadata() { if (els.video) { applyAniSkip(_malId, _currentEpisode); } } function onEnded() { if (!_roomMode && _currentEpisode < _totalEpisodes) { playEpisode(_currentEpisode + 1); } } function onVolumeChange() { if (!els.video || !els.volumeBtn || !els.volumeSlider) return; const volume = els.video.volume; const muted = els.video.muted; els.volumeSlider.value = volume * 100; let icon; if (muted || volume === 0) { icon = ''; } else if (volume < 0.5) { icon = ''; } else { icon = ''; } els.volumeBtn.innerHTML = icon; } function sendRoomEvent(eventType, data = {}) { if (!_roomMode || !_isRoomHost || !_roomWebSocket) return; if (_roomWebSocket.readyState !== WebSocket.OPEN) return; console.log('Sending room event:', eventType, data); _roomWebSocket.send(JSON.stringify({ type: eventType, ...data })); } function setWebSocket(ws) { console.log('Setting WebSocket reference in AnimePlayer'); _roomWebSocket = ws; } function formatTime(seconds) { if (!isFinite(seconds) || isNaN(seconds)) return '0:00'; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) { return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } return `${m}:${s.toString().padStart(2, '0')}`; } const Icons = { back: ``, check: ``, chevron: ``, quality: ``, audio: ``, subs: ``, speed: `` }; function buildSettingsPanel() { if (!els.settingsPanel) return; els.settingsPanel.innerHTML = ''; if (_settingsView === 'main') { buildMainMenu(); } else { buildSubMenu(_settingsView); } } function buildMainMenu() { let html = `
`; // 1. Quality if (hlsInstance && hlsInstance.levels && hlsInstance.levels.length > 1) { const currentLevel = hlsInstance.currentLevel; const label = currentLevel === -1 ? 'Auto' : (hlsInstance.levels[currentLevel]?.height + 'p'); html += createMenuItem('quality', 'Quality', label, Icons.quality); } // 2. Audio if (hlsInstance && hlsInstance.audioTracks && hlsInstance.audioTracks.length > 1) { const currentAudio = hlsInstance.audioTrack; const track = hlsInstance.audioTracks[currentAudio]; const label = track ? (track.name || track.lang || `Track ${currentAudio + 1}`) : 'Default'; html += createMenuItem('audio', 'Audio', label, Icons.audio); } // 3. Subtitles if (_currentSubtitles && _currentSubtitles.length > 0) { let label = 'Off'; const activeIndex = getActiveSubtitleIndex(); if (activeIndex !== -1 && _currentSubtitles[activeIndex]) { label = _currentSubtitles[activeIndex].label || _currentSubtitles[activeIndex].language; } html += createMenuItem('subtitle', 'Subtitles', label, Icons.subs); } // 4. Playback Speed if (els.video && (!_roomMode || _isRoomHost)) { const label = els.video.playbackRate === 1 ? 'Normal' : `${els.video.playbackRate}x`; html += createMenuItem('speed', 'Playback Speed', label, Icons.speed); } html += `
`; els.settingsPanel.innerHTML = html; // Listeners del menú principal els.settingsPanel.querySelectorAll('.settings-item').forEach(item => { item.addEventListener('click', (e) => { e.stopPropagation(); _settingsView = item.dataset.target; buildSettingsPanel(); }); }); } function createMenuItem(target, title, value, icon) { return `
${icon} ${title}
${value} ${Icons.chevron}
`; } function buildSubMenu(type) { let title = ''; let content = ''; if (type === 'quality') { title = 'Quality'; content = renderQualityOptions(); } else if (type === 'audio') { title = 'Audio Track'; content = renderAudioOptions(); } else if (type === 'subtitle') { title = 'Subtitles'; content = renderSubtitleOptions(); } else if (type === 'speed') { title = 'Playback Speed'; content = renderSpeedOptions(); } const html = `
${title}
${content}
`; els.settingsPanel.innerHTML = html; // Listener para volver atrás els.settingsPanel.querySelector('.settings-back-btn').addEventListener('click', (e) => { e.stopPropagation(); _settingsView = 'main'; buildSettingsPanel(); }); // Listeners para opciones CORREGIDO els.settingsPanel.querySelectorAll('.settings-item-option').forEach(opt => { opt.addEventListener('click', (e) => { e.stopPropagation(); const val = opt.dataset.value; applySetting(type, val); }); }); } // Funciones de renderizado de opciones function renderQualityOptions() { if (!hlsInstance) return ''; let html = ''; // Auto option const isAuto = hlsInstance.currentLevel === -1; html += `
Auto${isAuto ? Icons.check : ''}
`; // Levels desc hlsInstance.levels.forEach((level, i) => { const isSelected = hlsInstance.currentLevel === i; html += `
${level.height}p${isSelected ? Icons.check : ''}
`; }); return html; } function renderAudioOptions() { if (!hlsInstance) return ''; let html = ''; hlsInstance.audioTracks.forEach((track, i) => { const isSelected = hlsInstance.audioTrack === i; const label = track.name || track.lang || `Audio ${i + 1}`; html += `
${label}${isSelected ? Icons.check : ''}
`; }); return html; } function renderSubtitleOptions() { let html = ''; const activeIdx = getActiveSubtitleIndex(); // Off html += `
Off${activeIdx === -1 ? Icons.check : ''}
`; _currentSubtitles.forEach((sub, i) => { const isSelected = activeIdx === i; html += `
${sub.label || sub.language}${isSelected ? Icons.check : ''}
`; }); return html; } function renderSpeedOptions() { const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; let html = ''; const currentRate = els.video ? els.video.playbackRate : 1; speeds.forEach(speed => { const isSelected = Math.abs(currentRate - speed) < 0.1; html += `
${speed === 1 ? 'Normal' : speed + 'x'}${isSelected ? Icons.check : ''}
`; }); return html; } // Aplicar configuración function applySetting(type, value) { if (type === 'quality') { if (hlsInstance) hlsInstance.currentLevel = parseInt(value); } else if (type === 'audio') { if (hlsInstance) hlsInstance.audioTrack = parseInt(value); } else if (type === 'subtitle') { const idx = parseInt(value); _activeSubtitleIndex = idx; // <--- ACTUALIZAMOS EL ESTADO AQUÍ // 1. Lógica nativa (para mantener compatibilidad interna) if (els.video && els.video.textTracks) { Array.from(els.video.textTracks).forEach((track, i) => { // Si usamos JASSUB, ocultamos la nativa. Si no, mostramos la seleccionada. track.mode = (subtitleRenderer && idx !== -1) ? 'hidden' : ((i === idx) ? 'showing' : 'hidden'); }); } // 2. Lógica de JASSUB if (subtitleRenderer) { if (idx === -1) { subtitleRenderer.dispose(); } else { const sub = _currentSubtitles[idx]; if (sub) { subtitleRenderer.setTrack(sub.src); } } } } else if (type === 'speed') { if (els.video) els.video.playbackRate = parseFloat(value); } // Volvemos al menú principal para confirmar visualmente (opcional, estilo YouTube) _settingsView = 'main'; buildSettingsPanel(); } function getActiveSubtitleIndex() { return _activeSubtitleIndex; } async function initSubtitleRenderer() { if (!els.video) return; // Cleanup previous instance if (subtitleRenderer) { try { subtitleRenderer.dispose(); } catch(e) { console.warn('Error disposing renderer:', e); } subtitleRenderer = null; } // Find ASS subtitle const assSubtitle = _currentSubtitles.find(sub => (sub.src && sub.src.endsWith('.ass')) || (sub.label && sub.label.toLowerCase().includes('ass')) ); if (!assSubtitle) { console.log('No ASS subtitles found in current list'); return; } try { console.log('Initializing JASSUB for:', assSubtitle.label); // Check if JASSUB global is available if (window.SubtitleRenderer && typeof window.JASSUB !== 'undefined') { // --- CAMBIO AQUÍ: Pasamos els.subtitlesCanvas --- subtitleRenderer = new SubtitleRenderer(els.video, els.subtitlesCanvas); await subtitleRenderer.init(assSubtitle.src); } else { console.warn('JASSUB library not loaded.'); } } catch (e) { console.error('Subtitle renderer setup error:', e); subtitleRenderer = null; } } function onTimeUpdate() { if (!els.video) return; // Update progress bar const percent = (els.video.currentTime / els.video.duration) * 100; if (els.progressPlayed) { els.progressPlayed.style.width = `${percent}%`; } if (els.progressHandle) { els.progressHandle.style.left = `${percent}%`; } // Update time display if (els.timeDisplay) { const current = formatTime(els.video.currentTime); const total = formatTime(els.video.duration); els.timeDisplay.textContent = `${current} / ${total}`; } // Update progress for AniList if (!_roomMode && !_progressUpdated && els.video.duration) { const percentage = els.video.currentTime / els.video.duration; if (percentage >= 0.8) { updateProgress(); _progressUpdated = true; } } } // Player lifecycle async function playEpisode(episodeNumber) { const targetEp = parseInt(episodeNumber); if (targetEp < 1 || targetEp > _totalEpisodes) return; _currentEpisode = targetEp; _progressUpdated = false; if(els.epTitle) els.epTitle.innerText = `Episode ${targetEp}`; if(els.prevBtn) els.prevBtn.disabled = (_currentEpisode <= 1); if(els.nextBtn) els.nextBtn.disabled = (_currentEpisode >= _totalEpisodes); if(_skipBtn) { _skipBtn.classList.remove('visible'); _skipBtn.classList.remove('is-next'); } const newUrl = new URL(window.location); newUrl.searchParams.set('episode', targetEp); window.history.pushState({}, '', newUrl); if(els.playerWrapper) els.playerWrapper.style.display = 'block'; document.body.classList.add('stop-scrolling'); const trailer = document.querySelector('#trailer-player iframe'); if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); _rpcActive = false; setLoading("Checking availability..."); // Check local availability let shouldPlayLocal = false; try { const check = await fetch(`/api/library/${_animeId}/units`); const data = await check.json(); const localUnit = data.units ? data.units.find(u => u.number === targetEp) : null; if (localUnit) shouldPlayLocal = true; } catch (e) { console.warn("Availability check failed:", e); shouldPlayLocal = (els.extSelect.value === 'local'); } if (els.manualMatchBtn) { els.manualMatchBtn.style.display = shouldPlayLocal ? 'none' : 'flex'; } if (shouldPlayLocal) { let localOption = els.extSelect.querySelector('option[value="local"]'); if (!localOption) { localOption = document.createElement('option'); localOption.value = 'local'; localOption.innerText = 'Local'; els.extSelect.appendChild(localOption); } els.extSelect.value = 'local'; if(els.subDubToggle) els.subDubToggle.style.display = 'none'; if(els.serverSelect) els.serverSelect.style.display = 'none'; loadStream(); } else { if (els.extSelect.value === 'local') { const localOption = els.extSelect.querySelector('option[value="local"]'); if (localOption) localOption.remove(); let fallbackSource = (_entrySource !== 'local') ? _entrySource : 'anilist'; if (!els.extSelect.querySelector(`option[value="${fallbackSource}"]`)) { if (els.extSelect.options.length > 0) { fallbackSource = els.extSelect.options[0].value; } } els.extSelect.value = fallbackSource; handleExtensionChange(true); } else { if (els.serverSelect.options.length === 0) { handleExtensionChange(true); } else { loadStream(); } } } } function closePlayer() { if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } if (subtitleRenderer) { try { subtitleRenderer.dispose(); } catch(e) {} subtitleRenderer = null; } // Stop video if (els.video) { els.video.pause(); els.video.removeAttribute('src'); els.video.load(); } if(els.playerWrapper) els.playerWrapper.style.display = 'none'; document.body.classList.remove('stop-scrolling'); _skipIntervals = []; _rpcActive = false; sendRPC({ paused: true }); const newUrl = new URL(window.location); newUrl.searchParams.delete('episode'); window.history.pushState({}, '', newUrl); const trailer = document.querySelector('#trailer-player iframe'); if(trailer) { trailer.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); } } async function loadExtensionsList() { try { const res = await fetch('/api/extensions/anime'); const data = await res.json(); const extensions = data.extensions || []; if (_isLocal && !extensions.includes('local')) extensions.push('local'); els.extSelect.innerHTML = ''; extensions.forEach(ext => { const opt = document.createElement('option'); opt.value = ext; opt.innerText = ext.charAt(0).toUpperCase() + ext.slice(1); els.extSelect.appendChild(opt); }); if (extensions.includes(_entrySource)) { els.extSelect.value = _entrySource; } else if (extensions.length > 0) { els.extSelect.value = extensions[0]; } if (els.extSelect.value === 'local') { els.subDubToggle.style.display = 'none'; els.serverSelect.style.display = 'none'; } else if (els.extSelect.value) { handleExtensionChange(false); } } catch (e) { console.error("Error loading extensions:", e); } } async function handleExtensionChange(shouldPlay = true) { const selectedExt = els.extSelect.value; if (els.manualMatchBtn) { els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex'; } if (selectedExt === 'local') { els.subDubToggle.style.display = 'none'; els.serverSelect.style.display = 'none'; if (shouldPlay && _currentEpisode > 0) loadStream(); return; } _manualExtensionId = null; setLoading("Loading Extension Settings..."); try { const res = await fetch(`/api/extensions/${selectedExt}/settings`); const settings = await res.json(); els.subDubToggle.style.display = settings.supportsDub ? 'flex' : 'none'; setAudioMode('sub'); els.serverSelect.innerHTML = ''; if (settings.episodeServers && settings.episodeServers.length > 0) { settings.episodeServers.forEach(srv => { const opt = document.createElement('option'); opt.value = srv; opt.innerText = srv; els.serverSelect.appendChild(opt); }); els.serverSelect.value = settings.episodeServers[0]; els.serverSelect.style.display = 'block'; } else { els.serverSelect.style.display = 'none'; } if (shouldPlay && _currentEpisode > 0) { loadStream(); } else { if(els.loader) els.loader.style.display = 'none'; } } catch (error) { console.error("Failed to load settings:", error); setLoading("Failed to load extension settings."); } } function toggleAudioMode() { _audioMode = _audioMode === 'sub' ? 'dub' : 'sub'; setAudioMode(_audioMode); loadStream(); } function setAudioMode(mode) { _audioMode = mode; els.subDubToggle.setAttribute('data-state', mode); document.getElementById('opt-sub').classList.toggle('active', mode === 'sub'); document.getElementById('opt-dub').classList.toggle('active', mode === 'dub'); } function setLoading(msg) { if(els.loaderText) els.loaderText.innerText = msg; if(els.loader) els.loader.style.display = 'flex'; } async function getLocalEntryId() { if (_localEntryId) return _localEntryId; try { const res = await fetch(`/api/library/anime/${_animeId}`); if (!res.ok) return null; const data = await res.json(); _localEntryId = data.id; return _localEntryId; } catch (e) { console.error("Error fetching local ID:", e); return null; } } async function loadStream() { if (!_currentEpisode) return; _progressUpdated = false; setLoading("Fetching Stream..."); _rawVideoData = null; _currentSubtitles = []; // Cleanup before fetch to prevent ghost events if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } if (subtitleRenderer) { try { subtitleRenderer.dispose(); } catch(e) {} subtitleRenderer = null; } const currentExt = els.extSelect.value; if (currentExt !== 'local') { _isLocal = false; _rawVideoData = null; } if (currentExt === 'local') { try { const localId = await getLocalEntryId(); const check = await fetch(`/api/library/${_animeId}/units`); const data = await check.json(); const targetUnit = data.units ? data.units.find(u => u.number === parseInt(_currentEpisode)) : null; if (!targetUnit) { console.log(`Episode ${_currentEpisode} not found locally.`); const localOption = els.extSelect.querySelector('option[value="local"]'); if (localOption) localOption.remove(); const fallbackSource = (_entrySource === 'local') ? 'anilist' : _entrySource; if (els.extSelect.querySelector(`option[value="${fallbackSource}"]`)) { els.extSelect.value = fallbackSource; } else if (els.extSelect.options.length > 0) { els.extSelect.selectedIndex = 0; } handleExtensionChange(true); return; } const ext = targetUnit.format || targetUnit.name.split('.').pop().toLowerCase(); if (![''].includes(ext)) { setLoading(`Local files are not supported on the web player yet. Use MPV.`); _rawVideoData = { url: targetUnit.path, headers: {} }; if (els.mpvBtn) els.mpvBtn.style.display = 'flex'; return; } const localUrl = `/api/library/stream/${targetUnit.id}`; _rawVideoData = { url: localUrl, headers: {} }; _currentSubtitles = []; initVideoPlayer(localUrl, 'mp4'); } catch(e) { console.error(e); setLoading("Local Error: " + e.message); } return; } const server = els.serverSelect.value || ""; const extParam = `&ext=${currentExt}`; const realSource = _entrySource === 'local' ? 'anilist' : _entrySource; let url = `/api/watch/stream?animeId=${_animeId}` + `&episode=${_currentEpisode}` + `&server=${encodeURIComponent(server)}` + `&category=${_audioMode}` + `${extParam}` + `&source=${realSource}`; if (_manualExtensionId) { url += `&extensionAnimeId=${encodeURIComponent(_manualExtensionId)}`; } try { const res = await fetch(url); const data = await res.json(); if (data.error || !data.videoSources?.length) { setLoading(data.error || "No sources found."); return; } const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0]; const headers = data.headers || {}; _rawVideoData = { url: source.url, headers: headers }; let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`; if (headers['Referer'] && headers['Referer'] !== "null") { proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`; } if (headers['User-Agent']) { proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`; } const subtitles = (source.subtitles || []).map(sub => ({ label: sub.language, srclang: sub.language.toLowerCase().slice(0, 2), src: `/api/proxy?url=${encodeURIComponent(sub.url)}` })); _currentSubtitles = (source.subtitles || []).map(sub => ({ label: sub.language, srclang: sub.id, src: sub.url })); initVideoPlayer(proxyUrl, source.type, subtitles); } catch (err) { setLoading("Stream Error: " + err.message); console.error(err); } } function initVideoPlayer(url, type, subtitles = []) { console.log('initVideoPlayer called:', { url, type, subtitles }); // 1. CLEANUP FIRST if (subtitleRenderer) { try { subtitleRenderer.dispose(); } catch(e) { console.warn('Renderer dispose error (clean):', e); } subtitleRenderer = null; } if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; } const container = document.querySelector('.video-frame'); if (!container) { console.error('Video frame container not found!'); return; } // 2. Remove OLD Elements const oldVideo = container.querySelector('video'); const oldCanvas = container.querySelector('#subtitles-canvas'); if (oldVideo) { oldVideo.removeAttribute('src'); oldVideo.load(); oldVideo.remove(); } if (oldCanvas) { oldCanvas.remove(); } // 3. Create NEW Elements - CANVAS FIRST, then VIDEO const newCanvas = document.createElement('canvas'); newCanvas.id = 'subtitles-canvas'; const newVideo = document.createElement('video'); newVideo.id = 'player'; newVideo.crossOrigin = 'anonymous'; newVideo.playsInline = true; container.appendChild(newCanvas); container.appendChild(newVideo); els.video = newVideo; els.subtitlesCanvas = newCanvas; console.log('Video and canvas elements created:', { video: els.video, canvas: els.subtitlesCanvas }); // Re-setup controls with new video element setupCustomControls(); // Hide loader if (els.loader) els.loader.style.display = 'none'; // 4. Initialize Player if (Hls.isSupported() && type === 'm3u8') { console.log('Initializing HLS player'); hlsInstance = new Hls({ enableWorker: true, lowLatencyMode: false, backBufferLength: 90, debug: false }); hlsInstance.on(Hls.Events.ERROR, (event, data) => { console.error('HLS Error:', data); if (data.fatal) { if (els.loader) { els.loader.style.display = 'flex'; if (els.loaderText) els.loaderText.textContent = 'Stream error: ' + (data.details || 'Unknown'); } } }); hlsInstance.attachMedia(els.video); hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => { console.log('HLS media attached, loading source:', url); hlsInstance.loadSource(url); }); hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { console.log('HLS manifest parsed, attaching subtitles'); attachSubtitles(subtitles); buildSettingsPanel(); if (els.downloadBtn && !_roomMode) els.downloadBtn.style.display = 'flex'; // --- FIX: Inicializar el renderizador de subtítulos para HLS --- if (els.video.readyState >= 1) { initSubtitleRenderer(); } else { els.video.addEventListener('loadedmetadata', () => { initSubtitleRenderer(); }, { once: true }); } // ------------------------------------------------------------- console.log('Attempting to play video'); els.video.play().catch(err => { console.error('Play error:', err); }); }); hlsInstance.on(Hls.Events.LEVEL_SWITCHED, () => buildSettingsPanel()); hlsInstance.on(Hls.Events.AUDIO_TRACK_SWITCHED, () => buildSettingsPanel()); } else { console.log('Using native video player'); els.video.src = url; attachSubtitles(subtitles); buildSettingsPanel(); els.video.addEventListener('loadedmetadata', () => { console.log('Video metadata loaded'); initSubtitleRenderer(); }, { once: true }); console.log('Attempting to play video'); els.video.play().catch(err => { console.error('Play error:', err); }); if (els.downloadBtn && !_roomMode) els.downloadBtn.style.display = 'flex'; } } function attachSubtitles(subtitles) { if (!els.video) return; _activeSubtitleIndex = -1; Array.from(els.video.querySelectorAll('track')).forEach(t => t.remove()); if (subtitles.length === 0) return; subtitles.forEach((sub, i) => { const track = document.createElement('track'); track.kind = 'subtitles'; track.label = sub.label; track.srclang = sub.srclang; track.src = sub.src; track.default = i === 0; els.video.appendChild(track); }); setTimeout(() => { if (els.video.textTracks && els.video.textTracks.length > 0) { _activeSubtitleIndex = 0; if (!subtitleRenderer) { els.video.textTracks[0].mode = 'showing'; } else { els.video.textTracks[0].mode = 'hidden'; } } }, 100); } // AniSkip integration async function applyAniSkip(malId, episodeNumber) { if (!malId || !els.video) return; const duration = await waitForDuration(els.video); try { const url = `https://api.aniskip.com/v2/skip-times/${malId}/${episodeNumber}` + `?types[]=op&types[]=ed&episodeLength=${Math.floor(duration)}`; const res = await fetch(url); if (!res.ok) return; const data = await res.json(); if (!data.found) return; _skipIntervals = data.results.map(item => ({ startTime: item.interval.startTime, endTime: item.interval.endTime, type: item.skipType })); renderSkipMarkers(_skipIntervals); monitorSkipButton(_skipIntervals); } catch (e) { console.error('AniSkip Error:', e); } } function waitForDuration(video) { return new Promise(resolve => { if (video.duration && video.duration > 0) return resolve(video.duration); const check = () => { if (video.duration && video.duration > 0) { video.removeEventListener('loadedmetadata', check); resolve(video.duration); } }; video.addEventListener('loadedmetadata', check); }); } function renderSkipMarkers(intervals) { if (!els.progressContainer || !els.video.duration) return; els.progressContainer.querySelectorAll('.skip-range, .skip-cut').forEach(e => e.remove()); const duration = els.video.duration; intervals.forEach(skip => { const startPct = (skip.startTime / duration) * 100; const endPct = (skip.endTime / duration) * 100; const range = document.createElement('div'); range.className = `skip-range ${skip.type}`; // 'op' o 'ed' range.style.left = `${startPct}%`; range.style.width = `${endPct - startPct}%`; els.progressContainer.appendChild(range); createCut(startPct); createCut(endPct); }); } function createCut(percent) { if (percent < 0.5 || percent > 99.5) return; const cut = document.createElement('div'); cut.className = 'skip-cut'; cut.style.left = `${percent}%`; els.progressContainer.appendChild(cut); } function monitorSkipButton(intervals) { if (!_skipBtn || !els.video) return; const checkTime = () => { const ct = els.video.currentTime; const duration = els.video.duration; const activeInterval = intervals.find(i => ct >= i.startTime && ct <= i.endTime); if (activeInterval) { if (activeInterval.type === 'op') { showSkipButton('Skip Intro', activeInterval.endTime, false); return; } else if (activeInterval.type === 'ed') { if (_currentEpisode < _totalEpisodes) { showSkipButton('Next Episode', null, true); } else { showSkipButton('Skip Ending', activeInterval.endTime, false); } return; } } if (_currentEpisode < _totalEpisodes && (duration - ct) < 90 && (duration - ct) > 0) { if (!activeInterval) { showSkipButton('Next Episode', null, true); return; } } _skipBtn.classList.remove('visible'); }; els.video.addEventListener('timeupdate', checkTime); } function showSkipButton(text, seekTime, isNextAction) { if (!_skipBtn) return; _skipBtn.innerHTML = `${text} `; if (isNextAction) { _skipBtn.classList.add('is-next'); _skipBtn.dataset.seekTo = ''; } else { _skipBtn.classList.remove('is-next'); _skipBtn.dataset.seekTo = seekTime; } _skipBtn.classList.add('visible'); } function handleOverlayClick() { if (!_skipBtn) return; if (_skipBtn.classList.contains('is-next')) { playEpisode(_currentEpisode + 1); } else if (_skipBtn.dataset.seekTo) { els.video.currentTime = parseFloat(_skipBtn.dataset.seekTo); } _skipBtn.classList.remove('visible'); } // Download functionality async function downloadEpisode() { if (!_rawVideoData || !_rawVideoData.url) { alert("Stream not loaded yet."); return; } const isInFullscreen = document.fullscreenElement || document.webkitFullscreenElement; if (isInFullscreen) { try { if (document.exitFullscreen) { await document.exitFullscreen(); } else if (document.webkitExitFullscreen) { await document.webkitExitFullscreen(); } await new Promise(resolve => setTimeout(resolve, 100)); } catch (err) { console.warn("Error exiting fullscreen:", err); } } const isM3U8 = hlsInstance && hlsInstance.levels && hlsInstance.levels.length > 0; const hasMultipleAudio = hlsInstance && hlsInstance.audioTracks && hlsInstance.audioTracks.length > 1; const hasSubs = _currentSubtitles && _currentSubtitles.length > 0; if (isM3U8 || hasMultipleAudio || hasSubs) { await new Promise(resolve => requestAnimationFrame(resolve)); openDownloadModal(); } else { executeDownload(null, true); } } function openDownloadModal() { if(!els.downloadModal) return; els.dlQualityList.innerHTML = ''; els.dlAudioList.innerHTML = ''; els.dlSubsList.innerHTML = ''; let showQuality = false; let showAudio = false; let showSubs = false; // Quality options if (hlsInstance && hlsInstance.levels && hlsInstance.levels.length > 0) { showQuality = true; const levels = hlsInstance.levels.map((l, index) => ({...l, originalIndex: index})) .sort((a, b) => b.height - a.height); levels.forEach((level, i) => { const isSelected = i === 0; const div = document.createElement('div'); div.className = 'dl-item'; div.innerHTML = ` ${level.height}p ${(level.bitrate / 1000000).toFixed(1)} Mbps `; div.onclick = (e) => { if(e.target.tagName !== 'INPUT') div.querySelector('input').checked = true; }; els.dlQualityList.appendChild(div); }); } document.getElementById('dl-quality-section').style.display = showQuality ? 'block' : 'none'; // Audio tracks if (hlsInstance && hlsInstance.audioTracks && hlsInstance.audioTracks.length > 0) { showAudio = true; hlsInstance.audioTracks.forEach((track, index) => { const div = document.createElement('div'); div.className = 'dl-item'; div.innerHTML = ` ${track.name || track.lang || `Audio ${index+1}`} ${track.lang || 'unk'} `; div.onclick = (e) => { if(e.target.tagName !== 'INPUT') { const cb = div.querySelector('input'); cb.checked = !cb.checked; } }; els.dlAudioList.appendChild(div); }); } document.getElementById('dl-audio-section').style.display = showAudio ? 'block' : 'none'; // Subtitles if (_currentSubtitles && _currentSubtitles.length > 0) { showSubs = true; _currentSubtitles.forEach((sub, index) => { const div = document.createElement('div'); div.className = 'dl-item'; div.innerHTML = ` ${sub.label || sub.language || 'Unknown'} `; div.onclick = (e) => { if(e.target.tagName !== 'INPUT') { const cb = div.querySelector('input'); cb.checked = !cb.checked; } }; els.dlSubsList.appendChild(div); }); } document.getElementById('dl-subs-section').style.display = showSubs ? 'block' : 'none'; els.downloadModal.style.display = 'flex'; els.downloadModal.offsetHeight; els.downloadModal.classList.add('show'); } function closeDownloadModal() { if (els.downloadModal) { els.downloadModal.classList.remove('show'); setTimeout(() => { if(!els.downloadModal.classList.contains('show')) { els.downloadModal.style.display = 'none'; } }, 300); } } async function executeDownload(e, skipModal = false) { closeDownloadModal(); const btn = els.downloadBtn; if (!btn) return; const originalBtnContent = btn.innerHTML; btn.disabled = true; btn.innerHTML = `
`; let body = { anilist_id: parseInt(_animeId), episode_number: parseInt(_currentEpisode), stream_url: _rawVideoData.url, headers: _rawVideoData.headers || {}, chapters: _skipIntervals.map(i => ({ title: i.type === 'op' ? 'Opening' : 'Ending', start_time: i.startTime, end_time: i.endTime })), subtitles: [] }; if (skipModal) { if (_currentSubtitles) { body.subtitles = _currentSubtitles.map(sub => ({ language: sub.label || 'Unknown', url: sub.src })); } } else { const selectedSubs = Array.from(els.dlSubsList.querySelectorAll('input:checked')); body.subtitles = selectedSubs.map(cb => { const i = parseInt(cb.value); return { language: _currentSubtitles[i].label || 'Unknown', url: _currentSubtitles[i].src }; }); const isQualityVisible = document.getElementById('dl-quality-section').style.display !== 'none'; if (isQualityVisible && hlsInstance && hlsInstance.levels) { body.is_master = true; const qualityInput = document.querySelector('input[name="dl-quality"]:checked'); const qualityIndex = qualityInput ? parseInt(qualityInput.value) : 0; const level = hlsInstance.levels[qualityIndex]; if (level) { body.variant = { resolution: level.width ? `${level.width}x${level.height}` : '1920x1080', bandwidth: level.bitrate, codecs: level.attrs ? level.attrs.CODECS : '', playlist_url: level.url }; } const audioInputs = document.querySelectorAll('input[name="dl-audio"]:checked'); if (audioInputs.length > 0 && hlsInstance.audioTracks) { body.audio = Array.from(audioInputs).map(input => { const i = parseInt(input.value); const track = hlsInstance.audioTracks[i]; return { group: track.groupId || 'audio', language: track.lang || 'unk', name: track.name || `Audio ${i}`, playlist_url: track.url }; }); } } } try { const token = localStorage.getItem('token'); const res = await fetch('/api/library/download/anime', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, body: JSON.stringify(body) }); const data = await res.json(); if (res.status === 200) { btn.innerHTML = ``; } else if (res.status === 409) { btn.innerHTML = ``; } else { console.error("Download Error:", data); btn.innerHTML = ``; } } catch (err) { console.error("Request failed:", err); btn.innerHTML = ``; } finally { setTimeout(() => { if (btn) { btn.disabled = false; btn.innerHTML = ``; } }, 3000); } } // MPV functionality async function openInMPV() { if (!_rawVideoData) { alert("No video loaded yet."); return; } const token = localStorage.getItem('token'); if (!token) { alert("You need to be logged in."); return; } const body = { title: `${_animeTitle} - Episode ${_currentEpisode}`, video: _rawVideoData, subtitles: _currentSubtitles, chapters: _skipIntervals, animeId: _animeId, episode: _currentEpisode, entrySource: _entrySource, token: token }; try { const res = await fetch('/api/watch/mpv', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (res.ok) { console.log("MPV Request Sent"); closePlayer(); } else { console.error("MPV Request Failed"); } } catch (e) { console.error("MPV Error:", e); } } // Match Modal function openMatchModal() { const currentExt = els.extSelect.value; if (!currentExt || currentExt === 'local') return; if (typeof MatchModal !== 'undefined') { MatchModal.open({ provider: currentExt, initialQuery: _animeTitle, onSearch: async (query, prov) => { const res = await fetch(`/api/search/${prov}?q=${encodeURIComponent(query)}`); const data = await res.json(); return data.results || []; }, onSelect: (item) => { _manualExtensionId = item.id; loadStream(); } }); } } // RPC function sendRPC({ startTimestamp, endTimestamp, paused = false } = {}) { let stateText = `Episode ${_currentEpisode}`; let detailsText = _animeTitle; if (_roomMode) { stateText = `Watch Party - Ep ${_currentEpisode}`; } fetch("/api/rpc", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ details: detailsText, state: stateText, mode: "watching", startTimestamp, endTimestamp, paused }) }).catch(e => console.warn("RPC Error:", e)); } // Progress update async function updateProgress() { const token = localStorage.getItem('token'); if (!token) return; try { await fetch('/api/list/entry', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ entry_id: _animeId, source: _entrySource, entry_type: "ANIME", status: 'CURRENT', progress: _currentEpisode }) }); } catch (e) { console.error("Progress update failed", e); } } function setRoomHost(isHost) { console.log('Setting player host status:', isHost); _isRoomHost = isHost; // Re-ejecutar la configuración de controles con el nuevo permiso setupCustomControls(); // Forzar actualización visual si es necesario if (els.playerWrapper) { if (isHost) els.playerWrapper.classList.add('is-host'); else els.playerWrapper.classList.remove('is-host'); } } return { init, playEpisode, getCurrentEpisode: () => _currentEpisode, loadVideoFromRoom, getVideoElement: () => els.video, setRoomHost, setWebSocket }; })(); window.AnimePlayer = AnimePlayer;