'use strict'; (function () { const $ = id => document.getElementById(id); const roomId = decodeURIComponent((location.pathname.match(/^\/watch\/([A-Za-z0-9_-]+)\/?$/) || [])[1] || ''); const maxLines = Math.min(5, Math.max(1, parseInt(new URLSearchParams(location.search).get('maxlines'), 10) || 3)); if (new URLSearchParams(location.search).has('plain')) document.body.classList.add('plain'); const finals = []; let interim = null; let viewer = null; function status(message, error) { $('status').textContent = message; $('status').classList.toggle('error', !!error); $('status').hidden = false; } function render() { const lines = finals.slice(-maxLines); if (interim) lines.push(interim); const container = $('captions'); container.replaceChildren(); for (const item of lines) { const line = document.createElement('div'); line.className = 'line' + (item.interim ? ' interim' : ''); line.textContent = item.text; container.append(line); } if (viewer && viewer.getCursor) { const cursor = viewer.getCursor(); if (cursor) $('status').hidden = true; } } function receive(data) { const text = String(data.final || data.interm || '').trim(); if (!text) return; if (data.interm) { interim = {text}; } else { interim = null; finals.push({text}); if (finals.length > 50) finals.shift(); } render(); } async function boot() { if (!roomId) { status('This caption link is invalid.', true); return; } let response; try { response = await fetch('/api/public/rooms/' + encodeURIComponent(roomId), { credentials: 'same-origin', signal: AbortSignal.timeout(10000) }); } catch (_) { status('The caption service could not be reached.', true); return; } if (!response.ok) { status(response.status === 404 ? 'This caption room has ended.' : 'Captions are unavailable.', true); return; } let config; try { config = await response.json(); } catch (_) { status('Captions are unavailable.', true); return; } try { CaptionRelay.configure(config.address); } catch (error) { status('Captions are misconfigured: ' + error.message, true); return; } viewer = CaptionRelay.subscribe({ room: config.room, token: config.readToken, onCaption: receive, onState: state => { if (state === 'connected') { if (!finals.length && !interim) status('Connected. Waiting for captions…'); } else if (state === 'connected-with-gap') status('Reconnected; some captions may be missing.', true); else if (state === 'denied') status('This caption room link is no longer valid.', true); else if (state === 'reconnecting') status('Reconnecting to captions…'); }, onError: error => status('Caption relay: ' + (error.message || error), true), onGap: () => status('Some captions may be missing.', true) }); window.addEventListener('beforeunload', () => viewer.disconnect()); } boot().catch(() => status('Captions are unavailable.', true)); })();