/* ===================================================================== DAS LAUFENDE VIDEO ALS BROWSER-QUELLE (28.09.2026) Dieselbe Nachführung wie in der Reaction-Seite: Der Host schickt seinen Spielstand, diese Quelle folgt ihm. Übertragen wird nur Kennung, läuft/pausiert und Sekunde — das Video selbst lädt jede Seite bei YouTube, und das muss auch so sein (die Begründung steht in reaktion-tabellen.js). --------------------------------------------------------------------- DREI DINGE, DIE EINE QUELLE BRAUCHT UND EINE SEITE NICHT STUMM. Der Ton kommt bei OBS aus dem Mischpult, nicht aus dem Browser — sonst läge er doppelt im Stream, einmal davon verzögert. Und ohne `mute` würde der Player gar nicht erst anlaufen; das verbietet jeder Browser. KEINE BEDIENUNG. Was hier zu sehen ist, geht in den Stream. Ein Fortschrittsbalken oder ein YouTube-Logo, das beim Überfahren erscheint, wäre für alle sichtbar. EINE WACHE. Eine Quelle läuft im Hintergrund; bleibt sie stehen, fällt es erst auf, wenn das Bild nicht mehr passt — mitten in der Sendung. Kommt 70 Sekunden nichts, wird neu verbunden. ===================================================================== */ (() => { 'use strict'; const schluessel = new URLSearchParams(location.search).get('s') || ''; function sagFehler(text) { let k = document.querySelector('.obs-fehler'); if (!k) { k = document.createElement('div'); k.className = 'obs-fehler'; document.body.append(k); } k.textContent = text; } if (!schluessel) { sagFehler('Diese Adresse ist unvollständig — der Schlüssel fehlt. ' + 'Hol sie dir im Regiepult unter „Bühne".'); return; } let spieler = null; let bereit = false; let letztesVideo = ''; let stand = null; let letztes = Date.now(); function api() { if (window.YT && window.YT.Player) { bauen(); return; } if (document.getElementById('yt-api')) return; const s = document.createElement('script'); s.id = 'yt-api'; s.src = 'https://www.youtube.com/iframe_api'; window.onYouTubeIframeAPIReady = () => bauen(); document.head.append(s); } function bauen() { if (spieler || !stand || !stand.video) return; const halter = document.createElement('div'); halter.id = 'yt'; document.getElementById('platz').append(halter); letztesVideo = stand.video; spieler = new window.YT.Player('yt', { videoId: stand.video, host: 'https://www.youtube-nocookie.com', playerVars: { rel: 0, modestbranding: 1, playsinline: 1, /* STUMM. Der Ton kommt aus dem Mischpult -- und ohne `mute` würde der Player ohnehin nicht anlaufen. */ mute: 1, /* KEINE BEDIENUNG. Was hier zu sehen ist, geht in den Stream. */ controls: 0, disablekb: 1, fs: 0, iv_load_policy: 3, }, events: { onReady: () => { bereit = true; folgen(stand); }, onStateChange: () => { /* nichts zu melden -- diese Quelle sendet nicht */ }, }, }); } function folgen(s) { if (!bereit || !s) return; if (s.video && s.video !== letztesVideo) { letztesVideo = s.video; spieler.loadVideoById(s.video, s.sekunde || 0); spieler.mute(); return; } if (!s.video) return; /* Die Laufzeit der Meldung wird dazugerechnet -- sonst wäre die Quelle genau so weit hinten, wie das Netz langsam ist. */ const unterwegs = s.gesendet ? Math.max(0, (Date.now() - s.gesendet) / 1000) : 0; const soll = (s.sekunde || 0) + (s.laeuft ? unterwegs : 0); try { const ist = spieler.getCurrentTime(); /* ERST AB ANDERTHALB SEKUNDEN SPRINGEN. Darunter merkt es niemand, darüber merkt es jeder -- und ein Player, dem man jede Sekunde eine neue Stelle gibt, ruckelt sichtbar. */ if (Math.abs(ist - soll) > 1.5) spieler.seekTo(soll, true); if (s.laeuft && spieler.getPlayerState() !== 1) spieler.playVideo(); if (!s.laeuft && spieler.getPlayerState() === 1) spieler.pauseVideo(); } catch { /* der nächste Takt richtet es */ } } async function lageHolen() { const a = await fetch(`/workspace/api/buehne/lage?s=${encodeURIComponent(schluessel)}`); if (a.status === 404) { sagFehler('Der Schlüssel stimmt nicht mehr. ' + 'Hol dir die Adresse neu im Regiepult unter „Bühne".'); return false; } if (!a.ok) return false; stand = await a.json(); letztes = Date.now(); if (!spieler) api(); else folgen(stand); return true; } let strom = null; function verbinden() { try { if (strom) strom.close(); } catch { /* war schon zu */ } strom = new EventSource( `/workspace/api/buehne/strom?s=${encodeURIComponent(schluessel)}`); strom.addEventListener('open', () => { letztes = Date.now(); const k = document.querySelector('.obs-fehler'); if (k) k.remove(); }); strom.addEventListener('message', () => { letztes = Date.now(); }); strom.addEventListener('video', (e) => { letztes = Date.now(); try { stand = { ...(stand || {}), ...JSON.parse(e.data) }; if (!spieler) api(); else folgen(stand); } catch { /* eine kaputte Meldung darf die Quelle nicht reissen */ } }); /* Beim Beenden oder bei einem Videowechsel kommt die ganze Lage. Sie wird frisch geholt statt aus der Meldung gelesen: Die Meldung an die angemeldeten Seiten enthält Dinge, die hier nichts zu suchen haben. */ strom.addEventListener('reaktion', () => { letztes = Date.now(); lageHolen(); }); strom.addEventListener('error', () => { if (strom.readyState === EventSource.CLOSED) setTimeout(verbinden, 3000); }); } lageHolen().then((ok) => { if (ok) verbinden(); }); setInterval(() => { if (Date.now() - letztes > 70_000) { letztes = Date.now(); verbinden(); lageHolen(); } }, 10_000); document.addEventListener('visibilitychange', () => { if (!document.hidden && Date.now() - letztes > 30_000) { verbinden(); lageHolen(); } }); })();