/* Lazy classic chunk; its executing singleton stays pinned until reload. */ (window.YT ||= {}).Presenter = (() => { const noop = { boot() {}, active: () => false }; const view = $('presenterView'); if (!WEB || !view || typeof WebSocket === 'undefined') return noop; const overlay = LowerThird.active(); const KEY = overlay ? `ytpOverlayPair:${new URLSearchParams(location.search).get('code') || ''}` : 'ytpPresenterPair'; const load = () => { try { return JSON.parse(localStorage.getItem(KEY) || 'null'); } catch { return null; } }; const save = (v) => { try { if (v) localStorage.setItem(KEY, JSON.stringify(v)); else localStorage.removeItem(KEY); } catch { /* blocked */ } }; const wsUrl = (q) => `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws/remote?${new URLSearchParams(q)}`; const list = $('pvList'); const st = { ws: null, state: null, stateAt: 0, hostName: '', vid: null, all: [], docOff: 0, idx: -2, retry: null, notes: new Map(), wake: null, idleTimer: null, seq: 0 }; function setStatus(t) { $('pvStatus').textContent = t; } function show() { view.classList.remove('hidden'); document.body.classList.add('presenter-open'); // The app underneath may have focused its search box; keys belong here now. try { if (document.activeElement && !view.contains(document.activeElement)) document.activeElement.blur(); } catch { /* fine */ } try { if (Player.master && !Player.master.paused) Player.pause(); } catch { /* nothing loaded */ } keepAwake(); bumpIdle(); } async function keepAwake() { try { if ('wakeLock' in navigator && !st.wake) { st.wake = await navigator.wakeLock.request('screen'); st.wake.addEventListener('release', () => { st.wake = null; }); } } catch { /* not allowed */ } } function showPairForm(msg = '') { $('pvPair').classList.remove('hidden'); $('pvMsg').textContent = msg; setStatus('Not paired'); } async function pair(code) { code = String(code || '').replace(/\D/g, ''); if (code.length !== 6) { showPairForm('Enter the 6-digit code from the playing screen.'); return; } $('pvMsg').textContent = 'Connecting…'; try { const res = await fetch('/api/remote/pair', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, name: '🖥 Presenter' }) }); const j = await res.json().catch(() => null); if (!j || !j.ok) { showPairForm('⚠' + ((j && j.error) || `HTTP ${res.status}`)); return; } save({ roomId: j.roomId, remoteId: j.remoteId, token: j.token, hostName: j.hostName }); $('pvPair').classList.add('hidden'); connect(); } catch { showPairForm('⚠Network error — try again'); } } function connect() { const p = load(); if (!p) { showPairForm(); return; } clearTimeout(st.retry); try { if (st.ws) st.ws.close(); } catch { /* gone */ } setStatus('Connecting…'); const ws = new WebSocket(wsUrl({ role: 'remote', room: p.roomId, rid: p.remoteId, token: p.token, name: '🖥 Presenter' })); st.ws = ws; ws.onmessage = (e) => { let m; try { m = JSON.parse(e.data); } catch { return; } if (!overlay) { DirectMedia.attach('presenter', { send: m => ws.send(JSON.stringify(m)), peers: () => [{ id: 'host', name: st.hostName || 'Screen' }] }); } if (m.type === 'direct') { if (!overlay) DirectMedia.handle('presenter', m); return; } if (m.type === 'hello') { st.hostName = m.hostName || 'screen'; if (m.state) onState(m.state); else setStatus(`Following ${st.hostName}`); } else if (m.type === 'state') onState(m.state); else if (m.type === 'host-offline') { if (overlay) { st.all = []; LowerThird.paint([], -1); } setStatus(`${st.hostName || 'The screen'} is offline — waiting…`); } }; ws.onclose = (e) => { if (st.ws !== ws) return; st.ws = null; if (e.code === 4001) { save(null); showPairForm('This presenter was unpaired — enter a new code.'); return; } setStatus(e.code === 4004 ? 'Screen offline — retrying…' : 'Reconnecting…'); st.retry = setTimeout(connect, 3000); }; } function onState(s) { st.state = s; st.stateAt = Date.now(); const v = s && s.v; setStatus(v ? `● Following ${st.hostName} · ${v.title}` : `● Following ${st.hostName} · nothing playing`); const id = v && v.id; if (id !== st.vid) loadSong(id, v); } async function loadSong(id, v) { const seq = ++st.seq; st.vid = id; st.all = []; st.docOff = 0; render(v); if (!id || !isMediaId(id)) return; let notes = st.notes.get(id); if (!notes) { try { const j = await (await fetch(`/api/notes/${encodeURIComponent(id)}`)).json(); notes = j && j.ok ? j : null; if (notes) { st.notes.set(id, notes); if (st.notes.size > 50) st.notes.delete(st.notes.keys().next().value); } } catch { notes = null; } } if (seq !== st.seq) return; const doc = notes && notes.lyrics && notes.lyrics.data; st.all = (doc && doc.lines) || []; st.docOff = (doc && Number(doc.offset)) || 0; render(v); } function render(v) { if (overlay) { LowerThird.paint([], -1); st.idx = -2; tick(true); return; } const sung = st.all.map((l, i) => ({ ...l, i })).filter((l) => l.kind === 'line'); const title = $('pvTitle'); if (!sung.length) { list.innerHTML = ''; title.innerHTML = v ? `${escapeHtml(v.title)}${v.channel ? `${escapeHtml(v.channel)}` : ''}` : ''; title.classList.toggle('hidden', !v); return; } title.classList.add('hidden'); const pad = Math.round(list.clientHeight / 2); list.style.paddingTop = pad + 'px'; list.style.paddingBottom = pad + 'px'; list.innerHTML = sung.map((l) => `