Deutsch oeffnet mit „ und schliesst mit “. An 199 Stellen, die ein
Mensch liest, stand als Schlusszeichen ein GERADES " -- „Nächstes"
statt „Nächstes“. Auf sechzehn oeffentlichen Seiten, in vierzig
Dateien des Workspace und in zwoelf Servermodulen, die Texte
verschicken.
Auf dem Bildschirm sieht man den Unterschied sofort. Beim Schreiben
nicht: Das gerade " liegt auf der Tastatur, die anderen nicht.
WARUM EIN ERSTER ANLAUF ZURUECKGENOMMEN WURDE
Ein gerades " ist an vielen Stellen SYNTAX und kein Schriftzeichen --
Grenze einer Zeichenkette, Grenze eines HTML-Attributs, Zeichen in
einem regulaeren Ausdruck. Wer stumpf ersetzt, macht aus
„<a href="https://… ein kaputtes Attribut
/^["'„»\s]+|["'“«.\s]+$/ einen kaputten Ausdruck
"… nichts „mal " + "eben …" eine kaputte Zeichenkette
DIE UNTERSCHEIDUNG LAEUFT AN MERKMALEN, NICHT AN EINER LISTE
< > = dazwischen -> HTML-Marke oder Attribut
endet auf Leerzeichen -> die Zeichenkette hoert hier auf, der
Satz geht in der naechsten Zeile weiter.
Ein deutsches Schlusszeichen steht NIE
hinter einem Leerzeichen.
Rueckstrich mittendrin -> regulaerer Ausdruck
${ ohne } -> mitten in einem Ausdruck
Eine Liste erlaubter Ausnahmen waere die naechste, die niemand
pflegt. Zwoelf Stellen bleiben dadurch stehen, alle zwoelf einzeln
angesehen und alle zwoelf zu Recht -- dort steht das richtige
Schlusszeichen ohnehin weiter unten im Satz.
Fuenf davon waren allerdings ECHTE Fehler HINTER dem Link
(`…>HasiDog</a>".`) -- die erste Regel hatte nur das Attribut
gesehen, nicht den Satz danach. Gezielt nachgezogen.
Ein maskiertes `\"` in workspace-vorlagen.js (28 Hooks) wird zu “ --
ohne Rueckstrich, denn “ begrenzt nichts.
KOMMENTARE BLEIBEN, WIE SIE SIND. Dort liest es niemand ausser mir;
eine Wache, die auch Kosmetik anmahnt, wird weggeklickt. Beim ersten
Messen fielen ausserdem acht Stellen aus buehne.html faelschlich an,
weil `/* */` in HTML (in <style> und <script>) nicht ausgeblendet
war -- jetzt schon.
DIE WACHE DAZU
pruef-struktur prueft es ab sofort mit derselben Regel: 322 Dateien
mit sichtbarem Text, 0 Funde, und die zwoelf bewussten Ausnahmen
werden GEZAEHLT und genannt (erlaubt: 12). Eine Ausnahme, die niemand
sieht, waechst -- und irgendwann steht der echte Fall darin.
GEPRUEFT
pruef-struktur 59 -> 68 Pruefungen, 0 Fehler
node --check auf allen geaenderten JS-Dateien
nachgemessen: 199 geaendert, 12 mit Grund stehen geblieben
gruen geblieben: bewerbung-aufgaben 163, nachwuchs 262,
reaktion 421, support 63, content 45, terminregel 35, treff 85
Und nachgesehen, ob eine Pruefung noch die alte Schreibweise
ERWARTET: 13 Fundstellen, alle dreizehn nur Text in ihrer eigenen
Ausgabe, keine einzige ein Vergleich mit dem Seitentext.
GEGENPROBE: In reaktion.html ein Schlusszeichen zurueckgedreht ->
„workspace/reaktion.html:546 „Nächstes"", mit Datei und Zeile. Und
die Erkennung einzeln gegen HTML-Attribut, fortgesetzte
Zeichenkette, regulaeren Ausdruck und eingesetzten Wert geprueft.
Stempel gesetzt: workspace 670 Verweise in 45 Dateien, oeffentlich
554 in 48 Seiten.
Co-Authored-By: Claude Opus 5 <[email protected]>
225 lines
8.3 KiB
JavaScript
225 lines
8.3 KiB
JavaScript
/* =====================================================================
|
|
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 */ },
|
|
},
|
|
});
|
|
}
|
|
|
|
/* DIE GESCHWINDIGKEIT MUSS MIT -- sonst liefe der Stream anders
|
|
als der Saal. Stellt die Regie auf 1,5, wäre die Quelle nach fünf
|
|
Minuten zweieinhalb Minuten hinterher: Filipe kommentiert eine
|
|
Stelle, die die Zuschauer noch gar nicht gesehen haben. Und das
|
|
Nachspringen könnte es nicht auffangen, es würde nur alle paar
|
|
Sekunden sichtbar ruckeln. */
|
|
function tempoNachziehen(wert) {
|
|
if (!bereit || !wert) return;
|
|
try {
|
|
const kann = spieler.getAvailablePlaybackRates?.() || [];
|
|
if (kann.length && !kann.includes(wert)) return;
|
|
if (Math.abs((spieler.getPlaybackRate() || 1) - wert) > 0.01) {
|
|
spieler.setPlaybackRate(wert);
|
|
}
|
|
} catch { /* ein Spieler, der noch lädt, kann das noch nicht */ }
|
|
}
|
|
|
|
/* ==== DIE ANORDNUNG ====
|
|
Nur „nur Kamera" schaltet das Bild ab. „Kameras groß" lässt das
|
|
Video klein stehen — wie klein und wo, bestimmt Filipe in OBS
|
|
selbst, das ist nicht Sache dieser Quelle. */
|
|
function bildStellen(s) {
|
|
document.body.dataset.bild = (s && s.layout === 'nur_kamera') ? 'aus' : 'an';
|
|
}
|
|
|
|
/** Ein neuer Stand — an EINER Stelle.
|
|
*
|
|
* Vorher stand dasselbe Dreierlei zweimal da: merken, Spieler
|
|
* bauen oder nachziehen. Als die Anordnung dazukam, wäre sie an
|
|
* einer der beiden Stellen vergessen worden. Das ist keine
|
|
* Vermutung: Am selben Tag haben im selben Haus zwei Regeln für
|
|
* dieselbe Frage der Regieleiste null Pixel gegeben. */
|
|
/* ==== DIE WERBEEINBLENDUNG ================================
|
|
Sie ist der Grund, warum es sie gibt: Ein Band, das nur auf der
|
|
Seite laeuft, erreicht die Leute nicht, die ueber TikTok oder
|
|
YouTube zusehen -- und das sind die meisten.
|
|
|
|
Eingehaengt wird sie EINMAL, an den Koerper. Ein Band, das bei
|
|
jedem Stand neu entstuende, finge mitten im Satz von vorn an --
|
|
und der Stand kommt jede Sekunde. */
|
|
let werbung = null;
|
|
function werbungStellen(s) {
|
|
if (!window.Werbung) return;
|
|
if (!werbung) werbung = window.Werbung.einhaengen(document.body);
|
|
werbung.setzen(s && s.werbung);
|
|
}
|
|
|
|
function standSetzen(neu) {
|
|
stand = neu;
|
|
letztes = Date.now();
|
|
bildStellen(stand);
|
|
werbungStellen(stand);
|
|
if (!spieler) api(); else folgen(stand);
|
|
}
|
|
|
|
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();
|
|
/* YouTube setzt die Geschwindigkeit beim Laden auf 1 zurück. */
|
|
if (s.tempo) setTimeout(() => tempoNachziehen(s.tempo), 900);
|
|
return;
|
|
}
|
|
if (!s.video) return;
|
|
if (s.tempo) tempoNachziehen(s.tempo);
|
|
/* 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;
|
|
standSetzen(await a.json());
|
|
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 {
|
|
standSetzen({ ...(stand || {}), ...JSON.parse(e.data) });
|
|
} 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(); }
|
|
});
|
|
})();
|