Files
dogfather-universe/workspace/assets/js/buehne.js
T
DogFatherGitandClaude Opus 5 95c4599baa Anfuehrungszeichen: 199 falsche Schlusszeichen im sichtbaren Text
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]>
2026-10-01 01:56:15 +02:00

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(); }
});
})();