let currentHls = null; let currentBgHls = null; let currentSources = []; let resolvedStreams = []; let selectedSourceKey = null; let selectedStreamUrl = null; let expandedSources = new Set(); let currentQualityLevels = []; let selectedQualityIndex = -1; let activeSubtitles = []; let mediaSegments = []; let activeSegment = null; let plPrev = null; let plT = null; let plItem = null; let pmK = null; let plCtx = null; let viewS = null; let drag = 0; let discoverySession = 0; let userHasManuallySelectedSource = false; let subSettings = { size: 24, color: '#ffffff', bg: 'rgba(0,0,0,0.75)', offset: 0 }; const pi = (p, f) => `${p}`; const SPK = ''; const PI = { x: pi(''), play: pi('', 1), pause: pi('', 1), cc: pi(''), vol: pi(SPK + ''), mute: pi(SPK + ''), qlt: pi(''), set: pi(''), more: pi(''), fs: pi(''), fx: pi(''), eps: pi(''), chk: pi('') }; $('#ptop').innerHTML = `
${PI.x}
${PI.qlt}
${PI.cc}
${PI.vol}
${PI.set}
${PI.more}
`; $('#pbot').innerHTML = `0:00
0:00
${PI.eps}
${PI.fs}
`; $('#pmid').innerHTML = PI.play; const pm = $('#pm'); const sk = $('#pseek'); const pbg = $('#pbg'); const fmt = t => { t = Math.max(0, Math.floor(t || 0)); const h = Math.floor(t / 3600); const m = Math.floor((t % 3600) / 60); const c = t % 60; return (h ? h + ':' + String(m).padStart(2, '0') : m) + ':' + String(c).padStart(2, '0'); }; const tgl = () => vd.paused ? vd.play() : vd.pause(); const pico = () => { $('#pmid').innerHTML = vd.paused ? PI.play : PI.pause; }; function showCtl() { pl.classList.add('ui'); clearTimeout(plT); plT = setTimeout(() => { if (!vd.paused && !pmK && !pl.classList.contains('eps')) { pl.classList.remove('ui'); } }, 3800); } function closeMenu(rf) { if (!pmK) return; const t = { qlt: '#pqlt', cc: '#pcc', vol: '#pvol', set: '#pset', more: '#pmore' }[pmK]; pmK = null; pm.classList.remove('open'); pm.innerHTML = ''; if (rf) setFocus($(t)); } function updateSubStyles() { vd.classList.add('sub-active'); vd.style.setProperty('--sub-size', `${subSettings.size}px`); vd.style.setProperty('--sub-color', subSettings.color); vd.style.setProperty('--sub-bg', subSettings.bg); Array.from(vd.textTracks).forEach(track => { if (track.cues) { for (let i = 0; i < track.cues.length; i++) { const cue = track.cues[i]; if (cue && !cue._initialOffsetApplied) { cue.startTime += subSettings.offset; cue.endTime += subSettings.offset; } } } }); } function applySubOffset(delta) { subSettings.offset += delta; Array.from(vd.textTracks).forEach(track => { if (track.cues) { for (let i = 0; i < track.cues.length; i++) { const cue = track.cues[i]; if (cue) { cue.startTime += delta; cue.endTime += delta; } } } }); } function buildProxyUrl(rawUrl, headersObj = {}) { return `${API_BASE}/api/proxy?url=${encodeURIComponent(rawUrl)}&headers=${encodeURIComponent(JSON.stringify(headersObj))}`; } async function fetchSources() { currentSources = Vyla.sources(); } async function startConcurrentDiscovery(it, s = null, e = null) { const session = ++discoverySession; pl.classList.add('buf'); resolvedStreams = []; selectedStreamUrl = null; let foundFirstPlayable = false; if (!currentSources.length) { await fetchSources(); } if (session !== discoverySession) return; const sourcePromises = currentSources.map(async (src) => { try { const data = await Vyla.stream(src.key, it.id, s, e); if (session !== discoverySession) return; if (data.success && data.stream) { const streamData = data.stream; const headers = streamData.headers || {}; let playable = []; if (Array.isArray(streamData.allUrls) && streamData.allUrls.length > 0) { playable = streamData.allUrls; } else if (streamData.url) { playable = [{ url: streamData.url, quality: streamData.quality || 'Auto', isM3U8: streamData.isM3U8 }]; } if (playable.length > 0) { const resolvedEntry = { provider: src.key, label: src.label || src.key, headers, primaryUrl: playable[0].url, allUrls: playable }; resolvedStreams.push(resolvedEntry); if (pmK === 'set') { renderSourcesMenu(); } if (!foundFirstPlayable && !userHasManuallySelectedSource) { foundFirstPlayable = true; selectedSourceKey = src.key; playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers); } else if (userHasManuallySelectedSource && selectedSourceKey === src.key && !selectedStreamUrl) { playSpecificStream(resolvedEntry.primaryUrl, resolvedEntry.headers); } } } } catch (_) { } }); Promise.allSettled(sourcePromises).then(() => { if (session !== discoverySession) return; if (!resolvedStreams.length) { pl.classList.remove('buf'); toast('No playable sources available.'); } }); loadSubtitles(it.id, s, e); loadTimestamps(it.id, s, e); } function playSpecificStream(streamUrl, headers = {}) { pl.classList.add('buf'); selectedStreamUrl = streamUrl; if (currentHls) { currentHls.destroy(); currentHls = null; } if (currentBgHls) { currentBgHls.destroy(); currentBgHls = null; } const finalUrl = buildProxyUrl(streamUrl, headers); if (streamUrl.includes('.m3u8')) { if (Hls.isSupported()) { currentHls = new Hls({ xhrSetup: function (xhr) { xhr.withCredentials = false; } }); currentHls.loadSource(finalUrl); currentHls.attachMedia(vd); currentHls.on(Hls.Events.MANIFEST_PARSED, (_, data) => { currentQualityLevels = data.levels || []; vd.play().catch(() => { }); }); currentHls.on(Hls.Events.LEVEL_SWITCHED, (_, data) => { selectedQualityIndex = data.level; }); if (pbg) { currentBgHls = new Hls({ xhrSetup: function (xhr) { xhr.withCredentials = false; } }); currentBgHls.loadSource(finalUrl); currentBgHls.attachMedia(pbg); currentBgHls.on(Hls.Events.MANIFEST_PARSED, () => { pbg.muted = true; if (!vd.paused) pbg.play().catch(() => { }); }); } } else if (vd.canPlayType('application/vnd.apple.mpegurl')) { vd.src = finalUrl; vd.play().catch(() => { }); if (pbg) { pbg.src = finalUrl; pbg.muted = true; pbg.play().catch(() => { }); } } } else { currentQualityLevels = []; vd.src = finalUrl; vd.play().catch(() => { }); if (pbg) { pbg.src = finalUrl; pbg.muted = true; pbg.play().catch(() => { }); } } } function setPlaybackQuality(levelIndex) { selectedQualityIndex = levelIndex; if (currentHls) { currentHls.currentLevel = levelIndex; } } async function loadSubtitles(id, s, e) { Array.from(vd.querySelectorAll('track')).forEach(t => t.remove()); activeSubtitles = []; try { activeSubtitles = await Vyla.subtitles(id, s, e); activeSubtitles.forEach((sub, idx) => { const track = document.createElement('track'); track.kind = 'subtitles'; track.label = `${sub.label} (${sub.source || 'mirror'})`; track.srclang = sub.label.slice(0, 2).toLowerCase(); track.src = buildProxyUrl(sub.file); track.default = idx === 0; vd.appendChild(track); track.track.mode = 'disabled'; }); updateSubStyles(); } catch (_) { } } async function loadTimestamps(tmdbId, season = null, episode = null, durationSec = null) { mediaSegments = []; activeSegment = null; $('#pskip').classList.remove('v'); try { let q = `${API_BASE}/api/timestamps?tmdb_id=${tmdbId}`; if (season) q += `&season=${season}`; if (episode) q += `&episode=${episode}`; if (durationSec) q += `&duration_ms=${Math.round(durationSec * 1000)}`; const res = await fetch(q); const data = await res.json(); mediaSegments = data.segments || []; } catch (_) { mediaSegments = []; } } function renderSourcesMenu() { let container = $('#pm-src-list'); if (!container) return; container.innerHTML = ''; if (!resolvedStreams.length) { container.innerHTML = '
Probing providers...
'; return; } resolvedStreams.forEach(s => { const grp = document.createElement('div'); grp.className = 'src-group'; const hdr = document.createElement('div'); hdr.className = 'src-header f' + (s.provider === selectedSourceKey ? ' on' : ''); const hasMulti = s.allUrls && s.allUrls.length > 1; const countLabel = hasMulti ? ` (${s.allUrls.length})` : ''; const expandIcon = hasMulti ? `${expandedSources.has(s.provider) ? '▲' : '▼'}` : ''; hdr.innerHTML = `${s.label}${countLabel}
${expandIcon}${s.provider === selectedSourceKey ? PI.chk : ''}
`; hdr._a = () => { userHasManuallySelectedSource = true; if (hasMulti && s.provider === selectedSourceKey) { if (expandedSources.has(s.provider)) expandedSources.delete(s.provider); else expandedSources.add(s.provider); renderSourcesMenu(); } else { selectedSourceKey = s.provider; playSpecificStream(s.primaryUrl, s.headers); renderSourcesMenu(); } }; grp.appendChild(hdr); if (hasMulti) { const sub = document.createElement('div'); sub.className = 'src-sublist' + (expandedSources.has(s.provider) ? ' open' : ''); s.allUrls.forEach((u, i) => { const item = document.createElement('div'); item.className = 'src-subitem f' + (u.url === selectedStreamUrl ? ' on' : ''); item.innerHTML = `${u.quality || 'Stream ' + (i + 1)}${u.url === selectedStreamUrl ? PI.chk : ''}`; item._a = () => { userHasManuallySelectedSource = true; selectedSourceKey = s.provider; playSpecificStream(u.url, s.headers); renderSourcesMenu(); }; sub.appendChild(item); }); grp.appendChild(sub); } container.appendChild(grp); }); } function openMenu(k) { if (pmK === k) { closeMenu(1); return; } closeEps(); pm.innerHTML = ''; pmK = k; pm.classList.add('open'); let first = null; const h = t => { const e = document.createElement('h4'); e.textContent = t; pm.appendChild(e); }; const r = (l, on, fn) => { const e = mkf('pi', `${l}${on ? PI.chk : ''}`, () => { fn(); closeMenu(1); }); pm.appendChild(e); if (on && !first) first = e; return e; }; if (k === 'qlt') { h('Stream Quality'); if (currentHls && currentQualityLevels.length > 0) { r('Auto', selectedQualityIndex === -1, () => setPlaybackQuality(-1)); currentQualityLevels.forEach((lvl, idx) => { const label = lvl.height ? `${lvl.height}p` : `${Math.round(lvl.bitrate / 1000)} kbps`; r(label, selectedQualityIndex === idx, () => setPlaybackQuality(idx)); }); } else { const activeProv = resolvedStreams.find(s => s.provider === selectedSourceKey); if (activeProv && activeProv.allUrls.length > 1) { activeProv.allUrls.forEach((u, idx) => { const label = u.quality || `Stream ${idx + 1}`; r(label, u.url === selectedStreamUrl, () => { userHasManuallySelectedSource = true; playSpecificStream(u.url, activeProv.headers); }); }); } else { r('Auto', true, () => { }); } } } else if (k === 'cc') { h('Subtitles'); const T = [...vd.textTracks]; r('Off', !T.some(t => t.mode === 'showing'), () => { T.forEach(t => t.mode = 'disabled'); }); T.forEach(t => r(t.label, t.mode === 'showing', () => { T.forEach(u => u.mode = u === t ? 'showing' : 'disabled'); updateSubStyles(); })); h('Subtitle Size'); [18, 22, 26, 32].forEach(sz => { r(`${sz}px`, subSettings.size === sz, () => { subSettings.size = sz; updateSubStyles(); }); }); h('Subtitle Color'); [['White', '#ffffff'], ['Yellow', '#ffd700'], ['Cyan', '#00ffff']].forEach(([col, code]) => { r(col, subSettings.color === code, () => { subSettings.color = code; updateSubStyles(); }); }); h('Subtitle Sync'); const syncRow = document.createElement('div'); syncRow.className = 'sub-ctrl-row'; syncRow.innerHTML = `Timing Offset
`; pm.appendChild(syncRow); $('#sub-min').onclick = (ev) => { ev.stopPropagation(); applySubOffset(-0.5); }; $('#sub-plus').onclick = (ev) => { ev.stopPropagation(); applySubOffset(0.5); }; } else if (k === 'vol') { h('Volume'); const w = document.createElement('div'); w.className = 'vrow'; w.innerHTML = '
'; pm.appendChild(w); const vm = $('#vm'); const vs = $('#vsl'); const vu = () => { const v = vd.muted ? 0 : vd.volume; vs.firstChild.style.width = v * 100 + '%'; $('#vpc').textContent = Math.round(v * 100); vm.innerHTML = $('#pvol').innerHTML = v ? PI.vol : PI.mute; }; vm._a = () => { vd.muted = !vd.muted; vu(); }; vs._a = () => { }; vs._v = vu; vs.addEventListener('click', e => { const b = vs.getBoundingClientRect(); vd.muted = false; vd.volume = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width)); vu(); }); vu(); first = vs; } else if (k === 'set') { h('Available Sources'); const srcContainer = document.createElement('div'); srcContainer.id = 'pm-src-list'; pm.appendChild(srcContainer); renderSourcesMenu(); h('Playback Speed'); [.5, 1, 1.25, 1.5, 2].forEach(v => r(v === 1 ? 'Normal' : v + '\u00d7', vd.playbackRate === v, () => vd.playbackRate = v)); h('Aspect Ratio'); r('Fit Screen', !pl.classList.contains('cover'), () => { pl.classList.remove('cover'); if (pbg) { pbg.currentTime = vd.currentTime; if (!vd.paused) pbg.play().catch(() => { }); } }); r('Fill Screen', pl.classList.contains('cover'), () => { pl.classList.add('cover'); if (pbg) pbg.pause(); }); } else { h('Library'); r('Watch Later', has('wl', plItem), () => toggleLib('wl', plItem)); r('Favorites', has('fav', plItem), () => toggleLib('fav', plItem)); } showCtl(); setFocus(first || pm.querySelector('.f')); } async function openPlayer(it, ep, ctx) { if (!it) return; plPrev = cur; plItem = it; plCtx = ctx || null; userHasManuallySelectedSource = false; $('#peps').style.display = plCtx ? '' : 'none'; pl.classList.remove('eps'); $('#pnext').classList.remove('v'); $('#pt').textContent = it.title || it.name || ''; $('#ps').textContent = ep || [yr(it), it._t === 'tv' ? 'Series' : 'Movie'].filter(Boolean).join(' \u2022 '); $('#pav').src = it.poster_path ? I + 'w185' + it.poster_path : PH; vd.poster = it.backdrop_path ? I + 'w1280' + it.backdrop_path : ''; pl.classList.add('show'); showCtl(); pico(); setFocus($('#pmid')); if (plCtx) { const epData = plCtx.eps[plCtx.i]; startConcurrentDiscovery(it, plCtx.s, epData.episode_number); } else { startConcurrentDiscovery(it); } } function closePlayer() { closeMenu(); closeEps(); if (currentHls) { currentHls.destroy(); currentHls = null; } if (currentBgHls) { currentBgHls.destroy(); currentBgHls = null; } plCtx = null; vd.pause(); vd.removeAttribute('src'); vd.load(); if (pbg) { pbg.pause(); pbg.removeAttribute('src'); pbg.load(); } if (document.fullscreenElement) document.exitFullscreen(); pl.classList.remove('show', 'ui'); if (plPrev && document.body.contains(plPrev)) setFocus(plPrev); } $('#pback')._a = closePlayer; $('#pmid')._a = tgl; $('#pskip')._a = () => { if (activeSegment) { const target = activeSegment.end != null ? activeSegment.end + 0.5 : (vd.duration || vd.currentTime + 30); vd.currentTime = target; $('#pskip').classList.remove('v'); activeSegment = null; } else { vd.currentTime += 85; } }; $('#pqlt')._a = () => openMenu('qlt'); $('#pcc')._a = () => openMenu('cc'); $('#pvol')._a = () => openMenu('vol'); $('#pset')._a = () => openMenu('set'); $('#pmore')._a = () => openMenu('more'); $('#pfs')._a = () => document.fullscreenElement ? document.exitFullscreen() : pl.requestFullscreen && pl.requestFullscreen(); document.addEventListener('fullscreenchange', () => { $('#pfs').innerHTML = document.fullscreenElement ? PI.fx : PI.fs; }); const sp = e => { const b = sk.getBoundingClientRect(); vd.currentTime = Math.min(1, Math.max(0, (e.clientX - b.left) / b.width)) * (vd.duration || 0); }; sk.addEventListener('pointerdown', e => { drag = 1; sk._m = 1; sk.setPointerCapture(e.pointerId); sp(e); }); sk.addEventListener('pointermove', e => { if (drag) sp(e); }); sk.addEventListener('pointerup', () => drag = 0); sk._a = () => { if (sk._m) { sk._m = 0; return; } tgl(); }; pl.querySelectorAll('.f').forEach(e => { if (!e._a) e._a = () => { }; }); vd.addEventListener('click', () => { if (pmK) closeMenu(); else if (pl.classList.contains('eps')) closeEps(); else tgl(); showCtl(); }); vd.addEventListener('play', () => { if (!pl.classList.contains('cover') && pbg) { pbg.muted = true; pbg.currentTime = vd.currentTime; pbg.play().catch(() => { }); } }); vd.addEventListener('pause', () => { if (pbg) pbg.pause(); }); vd.addEventListener('seeking', () => { if (pbg) pbg.currentTime = vd.currentTime; }); vd.addEventListener('seeked', () => { if (pbg) pbg.currentTime = vd.currentTime; }); vd.addEventListener('ratechange', () => { if (pbg) pbg.playbackRate = vd.playbackRate; }); vd.addEventListener('durationchange', () => { $('#pdur').textContent = fmt(vd.duration); if (vd.duration && plItem) { const c = plCtx; loadTimestamps(plItem.id, c ? c.s : null, c ? c.eps[c.i].episode_number : null, vd.duration); } }); vd.addEventListener('timeupdate', () => { const d = vd.duration || 0; const t = vd.currentTime; const p = d ? (t / d) * 100 : 0; $('#pfill').style.width = p + '%'; $('#phd').style.left = p + '%'; $('#pcur').textContent = fmt(t); if (d && vd.buffered.length) { $('#pbuf').style.width = (vd.buffered.end(vd.buffered.length - 1) / d * 100) + '%'; } if (!pl.classList.contains('cover') && pbg && Math.abs(pbg.currentTime - t) > 0.35) { pbg.currentTime = t; } const currentSeg = mediaSegments.find(seg => { const afterStart = t >= seg.start; const beforeEnd = seg.end == null ? (d ? t < d - 5 : true) : t < seg.end; return afterStart && beforeEnd; }); if (currentSeg) { activeSegment = currentSeg; $('#pskip').textContent = currentSeg.label; $('#pskip').classList.add('v'); } else { activeSegment = null; $('#pskip').classList.remove('v'); } }); ['play', 'pause', 'ended'].forEach(n => vd.addEventListener(n, pico)); ['loadstart', 'waiting', 'seeking'].forEach(n => vd.addEventListener(n, () => pl.classList.add('buf'))); ['playing', 'canplay', 'seeked', 'error'].forEach(n => vd.addEventListener(n, () => pl.classList.remove('buf'))); const hasNext = () => !!plCtx && (plCtx.i + 1 < plCtx.eps.length || plCtx.seasons.some(x => x.season_number > plCtx.s)); const getSeason = async s => plCtx.cache[s] || (plCtx.cache[s] = (await api(`/tv/${plCtx.it.id}/season/${s}`)).episodes || []); async function playEp(s, i) { const c = plCtx; if (!c) return; const eps = await getSeason(s); const p = eps[i]; if (!p || plCtx !== c) return; c.s = s; c.i = i; c.eps = eps; $('#ps').textContent = `S${s} E${p.episode_number} \u2022 ${p.name || ''}`; $('#pnext').classList.remove('v'); closeEps(); showCtl(); setFocus($('#pmid')); userHasManuallySelectedSource = false; startConcurrentDiscovery(c.it, s, p.episode_number); } function nextEp() { const c = plCtx; if (!hasNext()) return; if (c.i + 1 < c.eps.length) { playEp(c.s, c.i + 1); } else { playEp(c.seasons.find(x => x.season_number > c.s).season_number, 0); } } function closeEps(rf) { if (!pl.classList.contains('eps')) return; pl.classList.remove('eps'); if (rf) setFocus($('#peps')); } function openEps() { const c = plCtx; if (!c) return; closeMenu(); $('#peT').innerHTML = `Episodes${esc(c.it.title || c.it.name || '')}`; const sr = $('#pseas'); sr.innerHTML = ''; c.seasons.forEach(x => { sr.appendChild(mkf('chip', esc(x.name || 'Season ' + x.season_number), () => showSeason(x.season_number))); }); pl.classList.add('eps'); showCtl(); showSeason(c.s, true); } async function showSeason(sn, focus) { const c = plCtx; const car = $('#pcar'); if (!c) return; viewS = sn; [...$('#pseas').children].forEach((e, i) => e.classList.toggle('act', c.seasons[i].season_number === sn)); car.innerHTML = SK.pep(8); const eps = await getSeason(sn); if (viewS !== sn || plCtx !== c) return; car.innerHTML = ''; eps.forEach((p, i) => { const n = p.episode_number; const cur = c.s === sn && c.i === i; const img = p.still_path ? I + 'w300' + p.still_path : (c.it.backdrop_path ? I + 'w300' + c.it.backdrop_path : PH); car.appendChild(mkf('pe' + (cur ? ' cur' : ''), `
${n}${cur ? '' : ''}
${esc(p.name || 'Episode ' + n)}${p.runtime ? p.runtime + 'm' : 'Episode ' + n}`, () => playEp(sn, i))); }); const t = car.querySelector('.cur') || car.firstChild; if (focus && t) setFocus(t); } $('#pep').innerHTML = `

${PI.x}
`; $('#pnext').innerHTML = 'Next Episode' + pi(''); $('#pnext')._a = nextEp; $('#pepx')._a = () => closeEps(1); $('#peps')._a = () => pl.classList.contains('eps') ? closeEps(1) : openEps(); vd.addEventListener('timeupdate', () => { const d = vd.duration || 0; const r = d - vd.currentTime; $('#pnext').classList.toggle('v', !!d && hasNext() && r <= 300 && r > 0); $('#pep').style.setProperty('--p', d ? (vd.currentTime / d) * 100 + '%' : '0%'); }); vd.addEventListener('ended', () => { if (hasNext()) nextEp(); });