Reaction: der Gleichlauf rechnet ohne Uhrenvergleich

Filipe: „die videos muessen perfekt laufen wenn ich die reaktions
mache."

GEMESSEN, NICHT VERMUTET. mess-reaktion hat einen neuen Abschnitt:
Er liest den Spielstand am ECHTEN <video>-Element in beiden Browsern,
rechnet beide auf denselben Augenblick um und sagt, wie weit Host und
Zuschauerin auseinanderliegen. Die vorhandene Pruefung fragte nur, ob
Start und Stopp ANKOMMEN -- zwei Videos koennen beide laufen und
trotzdem zwanzig Sekunden auseinander sein.

                        vorher      nachher
  Im Lauf                0,12 s     -0,04 s
  Wer dazukommt          6,91 s      0,26 s
  Uhr des Geraets 30 s vor  -29,97 s      0,26 s

DER SCHLIMMSTE FUND: DIE UHR DES TELEFONS

Der Zuschauer rechnete `Date.now() - stand.gesendet`. `gesendet` kam
vom SERVER, `Date.now()` vom Geraet -- zwei Uhren, die nie jemand
verglichen hat. Wessen Telefon dreissig Sekunden vorgeht, schaute
29,97 s weiter als alle anderen. Kein langsames Netz, kein schwaches
Handy. Oertlich faellt das nie auf: Auf einem Rechner sind beide
Uhren dieselbe.

Jetzt geht ein ALTER hinaus statt einer Uhrzeit -- die Differenz
zweier SERVERzeiten. Der Empfaenger braucht dafuer gar keine Uhr,
nur eine Stoppuhr (`performance.now()`), und die haelt Zeitumstellung
und Aufwachen aus dem Ruhezustand aus.

WEITER

* WER DAZUKOMMT, STEIGT RICHTIG EIN. Der gespeicherte Stand war so
  alt wie der letzte Takt des Hosts; neue Spalte `sekunde_am` sagt,
  wann er galt.
* DAS TEMPO GEHT IN DIE HOCHRECHNUNG EIN. Bei 2x laeuft die Videouhr
  doppelt so schnell -- das fehlte ganz.
* JEDER RECHNET ALLE ZWEI SEKUNDEN SELBST NACH statt nur auf Zuruf.
  Wer stockte, hing bis zu fuenf Sekunden hinterher.
* DER VORLAUF NACH EINEM SPRUNG WIRD GEREGELT, nicht geschaetzt --
  gemessen wird der Rest, nicht die Ladezeit (siehe unten).
* PUFFERN IST AUCH IN DER TRANSPORTLEISTE KEINE PAUSE. Der
  Start-Stopp-Knopf rief beim Puffern `playVideo()` -- wer Pause
  drueckte, bekam „weiter". Lampe und Knopfbild flackerten bei jedem
  Stocken. Die Lektion vom 29.09. war nur an einer Stelle eingeloest.
* DAS TEMPO MELDET UEBER `hostMelden()` statt ein zweites Mal von
  Hand -- es hielt Puffern fuer Stillstand und hielt damit beim
  Umschalten alle an.
* EIN VIDEO-RUNDRUF STATT DREI ABSCHRIFTEN. Beim Einbau hatte ich
  zuerst nur eine nachgezogen; das Tauschen der beiden Videos haette
  still die alte Rechnung weiterverschickt.

DREI EIGENE FEHLER UNTERWEGS, alle im Text festgehalten

1. ZWEI FUNKTIONEN HIESSEN `springen`. Meine „spring AUF die Stelle"
   wurde von der vorhandenen „spring UM so viele Sekunden"
   ueberschrieben -- lautlos, rueckwirkend fuer die ganze Datei. Ein
   Zuschauer bei Sekunde 100, dem „geh auf 101" gesagt wird, waere
   bei 201 gelandet. DREI BROWSERLAEUFE HABEN ES NICHT GEFUNDEN:
   oertlich blieb der Abstand unter der Sprungschwelle, die Zeile
   lief kein einziges Mal. Gefunden hat es erst eine Frage an die
   ganze Datei -- die steht jetzt als Pruefung drin und wurde
   nachgestellt (rot).
2. DER VORLAUF LERNTE NUR NACH OBEN. Gemessen wurde die Ladezeit
   nach einem Sprung; ein Sprung in geladenes Material dauert aber
   gar nicht, meldet nichts und lehrt nichts. Ergebnis: -0,95 s
   stabil. Jetzt wird das ERGEBNIS geregelt statt der Ursache.
3. DER AUSREISSER VON 39,84 s WAR MEINE EIGENE MESSUNG -- der Block
   davor schickt eine erfundene Position. Jetzt wird abgeklungen und
   in Messreihenfolge ausgegeben.

GEPRUEFT
pruef-reaktion 421/0 (14 neue, darunter der Riegel gegen die
Rueckkehr der Uhrenrechnung und die Wache gegen doppelte
Funktionsnamen -- beide nachgestellt), mess-reaktion ohne ein
einziges ACHTUNG, pruef-buehne 38/0, pruef-struktur, pruef-
zwischenspeicher 34/0.

Schemaaenderung: ADD COLUMN sekunde_am. Datenbank vorher gesichert
und geprueft (integrity_check ok, 20 Personen).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-30 15:45:48 +02:00
co-authored by Claude Opus 5
parent d5c84c7608
commit a3ec558127
50 changed files with 1483 additions and 719 deletions
+184
View File
@@ -553,6 +553,190 @@ console.log("=== Der Gleichlauf des Videos ===");
}
}
/* ==== WIE WEIT LIEGEN SIE AUSEINANDER? ==============================
Der Abschnitt darueber prueft, ob Start und Stopp ANKOMMEN. Das ist
etwas anderes als gemeinsam schauen: Zwei Videos koennen beide
laufen und trotzdem zwanzig Sekunden auseinanderliegen. Dort saehe
man davon nichts -- beide meldeten brav „laeuft".
GELESEN WIRD AM ECHTEN <video>-ELEMENT im YouTube-Rahmen. Nicht an
einer Zahl, die die Seite ueber sich selbst schreibt: Die koennte
stimmen, waehrend das Bild etwas anderes zeigt. Playwright kommt an
den Rahmen heran, also wird dort gemessen, wo das Bild entsteht.
BEIDE AUF DENSELBEN AUGENBLICK UMGERECHNET. Zwei Ablesungen
nacheinander liegen 20 bis 80 ms auseinander; ungerechnet waere
genau das der „Abstand". Jede Ablesung bekommt deshalb ihren
Zeitpunkt (die Mitte zwischen Anfrage und Antwort) und wird auf
einen gemeinsamen Bezug hochgerechnet -- mit dem Tempo, denn bei 2x
laeuft die Uhr im Video doppelt so schnell.
==================================================================== */
console.log("");
console.log(" --- Der Abstand zwischen Host und Zuschauerin ---");
{
const stelleLesen = async (w) => {
const f = w.seite.frames().find((x) => /youtube(-nocookie)?\.com\/embed/.test(x.url()));
if (!f) return null;
const t0 = Date.now();
let z = null;
try {
z = await f.evaluate(() => {
const v = document.querySelector("video");
return v ? { zeit: v.currentTime, steht: v.paused, tempo: v.playbackRate || 1 } : null;
});
} catch { return null; } /* der Rahmen wurde gerade neu gebaut */
const t1 = Date.now();
return z ? { ...z, am: (t0 + t1) / 2, spanne: (t1 - t0) / 1000 } : null;
};
/** Der Abstand in Sekunden. Positiv heisst: die Zuschauerin ist
* HINTEN -- der Host ist schon weiter. */
const abstandMessen = async () => {
const [h, g] = await Promise.all([stelleLesen(host), stelleLesen(zuschauer)]);
if (!h || !g) return null;
const bezug = Math.max(h.am, g.am);
const bei = (z) => z.zeit + (z.steht ? 0 : ((bezug - z.am) / 1000) * z.tempo);
return {
abstand: bei(h) - bei(g),
/* Wie genau diese eine Messung ueberhaupt sein kann. Ein
Befund, der kleiner ist als seine eigene Ungenauigkeit, ist
keiner. */
genauigkeit: h.spanne + g.spanne,
hSteht: h.steht, gSteht: g.steht,
};
};
const reihe = async (wieoft, abstandMs) => {
const werte = [];
for (let i = 0; i < wieoft; i++) {
const m = await abstandMessen();
if (m) werte.push(m);
await host.seite.waitForTimeout(abstandMs);
}
return werte;
};
const auswerten = (werte, was) => {
if (!werte.length) {
console.log(` KONNTE NICHT NACHSEHEN: ${was} -- der YouTube-Rahmen `
+ "war nicht zu lesen.");
offenePunkte.push(`Abstand „${was}" war in diesem Lauf nicht messbar.`);
return null;
}
const b = werte.map((w) => w.abstand).sort((x, y) => x - y);
const mitte = b[Math.floor(b.length / 2)];
const groesste = b.reduce((a, x) => (Math.abs(x) > Math.abs(a) ? x : a), 0);
const gen = Math.max(...werte.map((w) => w.genauigkeit));
console.log(` ${was}: Mitte ${mitte.toFixed(2)} s, `
+ `groesster ${groesste.toFixed(2)} s, ${werte.length} Messungen `
+ `(Messgenauigkeit ${gen.toFixed(2)} s)`);
/* IN MESSREIHENFOLGE, nicht sortiert. Die Reihenfolge ist eine
Auskunft: Ein Ausreisser gleich am Anfang heisst „es hat sich
noch nicht eingerenkt", einer mittendrin heisst etwas ganz
anderes. Sortiert sieht beides gleich aus. */
console.log(` der Reihe nach: `
+ `${werte.map((w) => w.abstand.toFixed(2)).join(" ")}`);
return { mitte, groesste };
};
/* ---- 1. Der Normalfall: beide laufen schon eine Weile ---------
ERST ABKLINGEN LASSEN -- und zwar wegen des Blocks DARUEBER,
nicht wegen des Programms. Um einen Pufferer herzustellen,
springt der Host dort 60 Sekunden vor; danach schickt die
Messung eine erfundene Meldung („die Sendung steht bei Sekunde
7"). Die Zuschauerin folgt ihr, der Host bleibt bei 60 -- bis
sein naechster Takt die Wahrheit sagt.
Im ersten Lauf stand deshalb ein Ausreisser von 39,84 s in der
Reihe. Er kam aus dem Aufbau der Messung, nicht aus der
Software. Zwei Takte des Hosts abwarten, dann ist der Zustand
wieder echt. */
await host.seite.waitForTimeout(11000);
const normal = auswerten(await reihe(8, 1800), "Im Lauf");
if (normal && Math.abs(normal.mitte) > 1.5) {
console.log(" ACHTUNG: Sie schauen nicht dasselbe. Ein Abstand von "
+ `${normal.mitte.toFixed(1)} s heisst, dass die Zuschauerin eine andere `
+ "Stelle sieht als der Host -- bei einer Reaction ist genau das der Punkt.");
ausgang = 1;
}
/* ---- 2. Wer mitten in der Sendung dazukommt -------------------
Der haeufigste Fall ueberhaupt: Jemand macht die Seite auf,
waehrend schon laeuft. Er bekommt den Stand aus der Datenbank --
und der ist so alt wie der letzte Takt des Hosts. */
await zuschauer.seite.reload({ waitUntil: "networkidle" });
let da = false;
for (let i = 0; i < 40 && !da; i++) {
da = !!(await stelleLesen(zuschauer));
if (!da) await zuschauer.seite.waitForTimeout(250);
}
if (!da) {
console.log(" KONNTE NICHT NACHSEHEN: nach dem Neuladen kam kein Spieler.");
offenePunkte.push("Der Abstand beim Dazukommen war nicht messbar.");
} else {
/* SOFORT messen, nicht nach zehn Sekunden -- die Frage ist, was
der Mensch in der ersten Sekunde sieht. Danach noch einmal:
Holt das System den Rueckstand von allein auf? */
const gleich = await abstandMessen();
if (gleich) {
console.log(` Beim Dazukommen: ${gleich.abstand.toFixed(2)} s`);
if (Math.abs(gleich.abstand) > 2) {
console.log(" ACHTUNG: Wer dazukommt, steigt "
+ `${gleich.abstand.toFixed(1)} s versetzt ein.`);
ausgang = 1;
}
}
await zuschauer.seite.waitForTimeout(7000);
const spaeter = await abstandMessen();
if (spaeter) {
console.log(` Sieben Sekunden spaeter: ${spaeter.abstand.toFixed(2)} s`);
}
}
/* ---- 3. UND DIE UHR DES GERAETS -------------------------------
Der Zuschauer rechnet `Date.now() - stand.gesendet`. `gesendet`
kommt vom SERVER, `Date.now()` vom Handy. Das sind zwei Uhren,
und niemand hat sie je verglichen.
HERGESTELLT, NICHT ABGEWARTET: Die Uhr der Seite wird um 30
Sekunden vorgestellt -- genau das, was ein Telefon mit von Hand
gestellter Zeit mitbringt. Ein Geraet, das nachgeht, faellt
durch `Math.max(0, ...)` weniger auf; eines, das vorgeht, nicht. */
console.log("");
console.log(" --- Wenn die Uhr des Geraets falsch geht ---");
await zuschauer.seite.evaluate((ms) => {
window.__echtJetzt = Date.now;
Date.now = () => window.__echtJetzt() + ms;
}, 30000);
/* Zwei Takte des Hosts abwarten -- erst dann hat die falsche Uhr
ueberhaupt Gelegenheit, sich auszuwirken. */
await host.seite.waitForTimeout(12000);
const schief = auswerten(await reihe(5, 1500), "Uhr 30 s vor");
if (schief && Math.abs(schief.mitte) > 3) {
console.log(" ACHTUNG: Eine falsch gehende Uhr im Geraet verschiebt das "
+ `Video um ${schief.mitte.toFixed(1)} s. Der Abstand wird aus zwei `
+ "Uhren gerechnet, die niemand verglichen hat -- wessen Telefon "
+ "falsch steht, schaut etwas anderes.");
ausgang = 1;
}
/* DIE GEGENPROBE ZUR GEGENPROBE: Uhr zurueckstellen, und es muss
sich wieder einrenken. Ohne sie hiesse ein grosser Wert oben
womoeglich nur, dass die Messung kaputtgegangen ist. */
await zuschauer.seite.evaluate(() => {
if (window.__echtJetzt) Date.now = window.__echtJetzt;
});
await host.seite.waitForTimeout(12000);
const wieder = auswerten(await reihe(4, 1200), "Uhr wieder richtig");
if (wieder && Math.abs(wieder.mitte) > 2) {
console.log(" ACHTUNG: Nach dem Zurueckstellen der Uhr renkt es sich "
+ "nicht wieder ein.");
ausgang = 1;
}
}
/* ---- Die Regie ---------------------------------------------------
NICHT UMSCHALTEN, SONDERN HERSTELLEN. Ein Klick auf „Griff" heisst
„das Gegenteil von dem, was gerade ist" -- und setzt damit voraus,
+160
View File
@@ -1660,6 +1660,166 @@ melde("=== 17. Tempo, Anordnung und das zweite Video ===");
method: "POST", keks: k.admin, rumpf: { tempo: 1, laeuft: true, sekunde: 25 },
});
/* =====================================================================
DER GLEICHLAUF: WIE ALT IST DER STAND? (30.09.2026)
Bis heute schickte der Server seine UHRZEIT mit (`gesendet`), und
der Browser rechnete `Date.now() - gesendet`. Das vergleicht die
Uhr des Servers mit der Uhr des Telefons -- zwei Uhren, die nie
jemand abgeglichen hat.
Gemessen am echten Browser (mess-reaktion): Ein Geraet, dessen Uhr
30 Sekunden vorgeht, spielte das Video 29,97 Sekunden weiter als
alle anderen. Und wer mitten in der Sendung dazukam, stieg 6,91
Sekunden hinter allen ein, weil der gespeicherte Stand so alt war
wie der letzte Takt des Hosts.
Jetzt geht ein ALTER hinaus -- die Differenz zweier SERVERzeiten.
Der Empfaenger braucht dafuer keine eigene Uhr.
===================================================================== */
{
const jetztStellen = (sek, alter) => roh(CREW, "/workspace/api/reaktion/video", {
method: "POST", keks: k.admin, rumpf: { laeuft: true, sekunde: sek, alter },
});
const holen = async () => (await roh(CREW, "/workspace/api/reaktion", { keks: k.gast })).j;
await jetztStellen(100, 0);
const frisch = await holen();
ok(Number.isFinite(frisch?.alter) && frisch.alter >= 0 && frisch.alter < 500,
`ein frischer Stand ist fast null Millisekunden alt (${frisch?.alter})`);
/* ES IST WIRKLICH EIN ALTER UND KEINE FESTE ZAHL. Ohne diese
Zeile hiesse „0 ms" oben womoeglich nur, dass dort immer 0
steht -- und dann rechnete niemand je etwas dazu. */
await new Promise((r) => setTimeout(r, 1300));
const gereift = await holen();
ok(gereift?.alter >= 1200 && gereift?.alter <= 2600,
`nach 1,3 s ist derselbe Stand 1,3 s alt (${gereift?.alter} ms)`);
/* UND DIE RECHNUNG GEHT AUF. Das ist der eigentliche Zweck:
sekunde + alter muss die Stelle ergeben, an der die Sendung
jetzt steht. */
const stelle = (gereift.sekunde || 0) + (gereift.alter || 0) / 1000;
ok(Math.abs(stelle - 101.3) < 0.4,
`daraus folgt die richtige Stelle: ${stelle.toFixed(2)} s (erwartet ~101,3)`);
/* ---- Was der Host ueber seine eigene Ablesung sagt ----------
Er misst oertlich, wie alt sie beim Absenden war. Das gehoert
dazugerechnet -- sonst saesse jeder Zuschauer genau um diese
Spanne zu weit hinten. */
await jetztStellen(200, 3000);
const mitAlter = await holen();
ok(mitAlter?.alter >= 3000 && mitAlter?.alter < 3600,
`eine 3 s alte Ablesung kommt auch als 3 s alt an (${mitAlter?.alter})`);
/* GEDECKELT, WEIL ES VOM BROWSER KOMMT. Was hereinkommt, wird
geprueft und nicht geglaubt -- sonst setzte eine einzige
falsche Zahl alle Zuschauer an eine erfundene Stelle. */
await jetztStellen(300, 999999);
const frech = await holen();
ok(frech?.alter !== null && frech.alter <= 5600,
`ein unsinniges Alter wird auf 5 s gedeckelt (${frech?.alter})`);
await jetztStellen(310, -8000);
const negativ = await holen();
ok(negativ?.alter !== null && negativ.alter >= 0 && negativ.alter < 600,
`ein negatives Alter gilt als null (${negativ?.alter})`);
/* ---- Der dritte Ausgang: „weiss ich nicht" -------------------
Ein Stand ohne Zeitstempel (alte Zeile, frisch aufgeraeumte
Sendung) darf NICHT als „null Millisekunden alt" durchgehen.
`null` heisst beim Empfaenger „nicht hochrechnen"; eine 0
hiesse „taufrisch" und schickte alle an eine alte Stelle. */
/* EIGENES HANDLE. Das vom Anfang der Datei ist laengst
geschlossen (Zeile 108) -- die Pruefung legt die Leute an und
ueberlaesst die Datenbank danach dem Server. */
const { DatabaseSync: DB3 } = await import("node:sqlite");
const d3 = new DB3(process.env.WORKSPACE_DB);
d3.prepare("UPDATE reaktion SET sekunde_am = 0 WHERE id = 1").run();
const ohne = await holen();
ok(ohne?.alter === null,
`ohne Zeitstempel ist das Alter ausdruecklich unbekannt (${JSON.stringify(ohne?.alter)})`);
/* UND EIN UNMOEGLICH ALTER STAND EBENSO. Eine Zeile aus der
vorigen Woche wuerde sonst „604800000 ms" melden, und der
Browser rechnete sie brav dazu. */
d3.prepare("UPDATE reaktion SET sekunde_am = ? WHERE id = 1")
.run(Date.now() - 7 * 86400000);
const uralt = await holen();
ok(uralt?.alter === null,
`ein eine Woche alter Stand gilt als unbekannt (${JSON.stringify(uralt?.alter)})`);
d3.close();
/* Und wieder in einen brauchbaren Zustand -- die Pruefungen
danach sollen keinen Scherbenhaufen vorfinden. */
await jetztStellen(0, 0);
/* ---- UND EIN RIEGEL GEGEN DIE RUECKKEHR ---------------------
Die alte Zeile sah harmlos aus:
const unterwegs = Date.now() - stand.gesendet;
Der Kommentar daneben erklaerte sogar richtig, warum die
Laufzeit dazugehoert. Nur verglich sie die Uhr des Servers mit
der Uhr des Telefons -- und das faellt oertlich NIE auf: Auf
einem Rechner sind beide Uhren dieselbe. Erst als jemand die
Uhr absichtlich verstellt hat, kam es heraus.
GEMESSEN WIRD AM KOMMENTARFREIEN TEXT. Im Kopf der Datei steht
die alte Zeile als Beispiel, und ein Beispiel im Kommentar ist
kein Programm. Genau diese Falle hat mich heute frueh beim
Abzeichen schon einmal erwischt. */
const { jsOhneKommentar: ohneK } = await import("./helfer-ohne-kommentar.mjs");
const rjs = ohneK(readFileSync(
join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8"));
ok(!/\bgesendet\b/.test(rjs),
"der Browser rechnet nicht mehr mit der Uhrzeit des Servers");
const kern = rjs.slice(rjs.indexOf("function sollJetzt"));
const rumpf = kern.slice(0, kern.indexOf("\n }"));
ok(rumpf.length > 40 && /performance\.now\(\)/.test(rumpf),
"die Soll-Stelle wird mit der Stoppuhr gerechnet, nicht mit dem Kalender");
ok(/anker\.tempo/.test(rumpf),
"und das Tempo geht in die Hochrechnung ein (bei 2x laeuft die Videouhr doppelt)");
ok(!/Date\.now\(\)/.test(rumpf),
"in der Rechnung selbst steckt keine Tagesuhr mehr");
/* GEGENPROBE: Die Suche liest wirklich etwas. Ohne sie hiessen
die drei Zeilen darueber womoeglich nur, dass `rumpf` leer
ist -- und ein leerer Text besteht jede Verbotspruefung. */
ok(/const her =/.test(rumpf),
`die Suche findet den Rumpf von sollJetzt (${rumpf.length} Zeichen)`);
/* ---- KEIN NAME ZWEIMAL ---------------------------------------
`reaktion.js` ist ein einziger Rumpf von 4400 Zeilen. Wer dort
einen Funktionsnamen ein zweites Mal vergibt, ueberschreibt
die erste -- und zwar RUECKWIRKEND fuer die ganze Datei, auch
fuer Aufrufe weit davor. Es gibt keine Fehlermeldung.
GENAU DAS IST AM 30.09.2026 PASSIERT. Die neue Nachfuehrung
hiess `springen(ziel)` („spring AUF diese Stelle"), der Host
hatte laengst ein `springen(um)` („spring UM so viele
Sekunden"). Gewonnen hat das zweite. Ein Zuschauer bei
Sekunde 100, dem „geh auf 101" gesagt wird, waere bei 201
gelandet.
DIE MESSUNG IM BROWSER HAT ES NICHT GEFUNDEN -- oertlich blieb
der Abstand unter der Sprungschwelle, die Zeile lief in drei
Laeufen kein einziges Mal. Gefunden hat es erst diese Frage an
die ganze Datei. */
const namen = [...rjs.matchAll(/^\s{0,4}(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm)]
.map((t) => t[1]);
const doppelt = [...new Set(namen.filter((n, i) => namen.indexOf(n) !== i))];
ok(namen.length > 40 && doppelt.length === 0,
doppelt.length
? `ZWEIMAL VERGEBEN: ${doppelt.join(", ")} — die spaetere gewinnt, lautlos`
: `alle ${namen.length} Funktionsnamen in reaktion.js sind einmalig`);
}
/* ---- Das zweite Video ---- */
const A = "dQw4w9WgXcQ";
const B = "9bZkp7q19f0";
+19
View File
@@ -495,6 +495,25 @@ export function reaktionTabellen(d) {
["ecke_gross", "REAL NOT NULL DEFAULT 1"],
];
if (!da.has("sekunde_am")) {
/* WANN DIE SEKUNDE GALT -- in Millisekunden der Serveruhr.
(30.09.2026)
Ohne sie liess sich nicht sagen, wie alt der gespeicherte
Spielstand ist. Wer mitten in der Sendung dazukam, bekam
`sekunde` aus der Datenbank und setzte sich damit dorthin --
gemessen 6,91 s hinter allen anderen, weil der Host nur alle
fuenf Sekunden meldet.
Sie wird NICHT an den Browser weitergegeben. Hinausgeschickt
wird nur die Differenz `Date.now() - sekunde_am`, also ein
ALTER. Eine absolute Serverzeit koennte der Empfaenger nur mit
seiner eigenen Uhr verrechnen, und genau das war der Fehler:
Ein Geraet mit 30 s falscher Uhr schaute 30 s woanders. */
d.exec("ALTER TABLE reaktion ADD COLUMN sekunde_am INTEGER NOT NULL DEFAULT 0");
console.log("[reaktion] Spalte sekunde_am angelegt.");
}
if (!da.has("chat_modus")) {
/* Wer im Live-Chat schreiben darf. Gilt fuer die ganze Sendung,
nicht je Person -- deshalb hier und nicht an den Zusehenden. */
+86 -21
View File
@@ -117,7 +117,7 @@ const jetzt = () => new Date().toISOString();
====================================================================== */
const SENDUNGS_FELDER = {
titel: "", video: "", video_titel: "", vorschau: "",
beginnt_am: null, laeuft: 0, sekunde: 0, gestartet_am: null,
beginnt_am: null, laeuft: 0, sekunde: 0, sekunde_am: 0, gestartet_am: null,
zweit_video: "", zweit_titel: "", zweit_sekunde: 0,
};
@@ -133,6 +133,63 @@ const PULT_EINSTELLUNGEN = {
chat_modus: "offen",
};
/** Wie alt ist dieser Spielstand -- in Millisekunden?
*
* DAS IST DIE EINE ZAHL, DIE HINAUSGEHT (30.09.2026), und
* ausdruecklich keine Uhrzeit. Ein Alter ist die Differenz zweier
* Serverzeiten; der Empfaenger braucht dafuer keine eigene Uhr.
*
* Vorher ging `gesendet: Date.now()` hinaus, und der Browser rechnete
* `Date.now() - gesendet`. Das verglich die Serveruhr mit der Uhr des
* Telefons. Gemessen am 30.09.2026: Ein Geraet, dessen Uhr 30 s
* vorgeht, spielte das Video 29,97 s weiter als alle anderen. Kein
* Fehler im Netz, kein langsames Handy -- nur zwei Uhren, die nie
* jemand verglichen hat.
*
* GEDECKELT. Ein `sekunde_am` von 0 (alte Zeile, frisch angelegte
* Sendung) hiesse sonst „dieser Stand ist 56 Jahre alt". Wer nichts
* weiss, gibt `null` zurueck -- und `null` heisst beim Empfaenger
* „nicht hochrechnen", nicht „null Millisekunden". */
const MAX_ALTER_MS = 60_000;
function standAlter(s) {
const am = Number(s?.sekunde_am) || 0;
if (!am) return null;
const alt = Date.now() - am;
if (alt < 0 || alt > MAX_ALTER_MS) return null;
return Math.round(alt);
}
/** Der Rundruf „so steht das Video jetzt" -- an EINER Stelle.
*
* ER WURDE AN DREI STELLEN VON HAND ZUSAMMENGESETZT: beim Starten
* der Sendung, beim Takt des Hosts und beim Tauschen der beiden
* Videos. Beim Einbau des Alters (30.09.2026) habe ich zuerst nur
* eine davon nachgezogen -- das Tauschen haette danach weiter die
* alte, kaputte Rechnung verschickt, und zwar still.
*
* Dieselbe Sorte Doppelung wie am 11.09.2026, als drei Spalten mit
* Inhalt verlorengegangen sind. Eine Liste, die niemand pflegt, kann
* nicht veralten.
*
* GELESEN WIRD AUS DER ZEILE, nicht aus dem, was der Aufrufer
* gerade zu wissen glaubt. */
function videoRundruf(s) {
melden("reaktion", {
was: "video",
video: s.video,
laeuft: !!s.laeuft,
sekunde: s.sekunde,
tempo: Number(s.tempo) || 1,
/* DAS ALTER, NICHT DIE UHRZEIT. Warum: siehe standAlter(). */
alter: standAlter(s),
/* `gesendet` bleibt vorerst mit drin. Ein Browser, der die alte
Fassung noch im Zwischenspeicher hat, wuerde sonst gar nicht
mehr nachfuehren -- und „gar nicht" ist schlechter als „etwas
ungenau". Darf weg, sobald der Stempel ueberall durch ist. */
gesendet: Date.now(),
});
}
/* ---------- Schranken --------------------------------------------------- */
function angemeldet(req, res, next) {
@@ -236,7 +293,7 @@ function listeHolen() {
* Zuruecksetzen der Sekunde vergessen kann. */
function weiterZu(zeile, personId) {
db().prepare("UPDATE reaktion SET video = ?, video_titel = ?, laeuft = 1,"
+ " sekunde = 0, geaendert = ? WHERE id = 1")
+ " sekunde = 0, sekunde_am = unixepoch('subsec') * 1000, geaendert = ? WHERE id = 1")
.run(zeile.video, zeile.titel, jetzt());
db().prepare("DELETE FROM reaktion_liste WHERE id = ?").run(zeile.id);
protokolliere("reaktion_weiter", { personId, detail: zeile.video });
@@ -248,7 +305,11 @@ function weiterZu(zeile, personId) {
betrifft nur die Anzeige und darf einen Wimpernschlag spaeter
kommen. Nur die erste zu schicken hiesse: Das Video wechselt,
und im Pult steht weiter der alte Name. */
melden("reaktion", { was: "video", video: s.video, laeuft: true, sekunde: 0, gesendet: Date.now() });
/* HIER GILT DIE SEKUNDE JETZT -- das Video faengt von vorn an.
Ohne `sekunde_am` waere der Stand „unbekannt alt" und niemand
duerfte hochrechnen; ausgerechnet beim Start, wo alle
gleichzeitig einsteigen. */
videoRundruf(s);
return s;
}
@@ -300,6 +361,12 @@ function lage(person) {
layout: s.layout,
laeuft: !!s.laeuft,
sekunde: s.sekunde,
/* WIE ALT DIESE SEKUNDE IST. Ohne sie setzte sich jeder, der
mitten in der Sendung dazukam, auf den Stand des letzten
Host-Taktes -- gemessen 6,91 s hinter allen anderen. Jetzt
rechnet er die verstrichene Zeit dazu und steigt an der
richtigen Stelle ein. */
alter: standAlter(s),
/* DIE GESCHWINDIGKEIT GEHT AN ALLE. Sie ist kein Regieknopf,
sondern Teil dessen, was gerade laeuft -- wer sie nicht
bekaeme, saehe dasselbe Video in einem anderen Tempo und waere
@@ -943,6 +1010,14 @@ reaktionRouter.post("/workspace/api/reaktion/video", nurHost, express.json({ lim
try {
const laeuft = req.body?.laeuft ? 1 : 0;
const sekunde = Math.max(0, Number(req.body?.sekunde) || 0);
/* WIE ALT DIE ABLESUNG BEIM ABSENDEN WAR.
Der Host misst das oertlich (Differenz zweier eigener
Messpunkte) -- das stimmt auch auf einem Geraet mit falscher
Uhr, waehrend eine mitgeschickte Uhrzeit dort falsch waere.
Gedeckelt, weil es vom Browser kommt: Was hereinkommt, wird
geprueft, nicht geglaubt. */
const alter = Math.min(5000, Math.max(0, Number(req.body?.alter) || 0));
const sekundeAm = Date.now() - alter;
const video = req.body?.video === undefined ? null : videoKennung(String(req.body.video));
if (req.body?.video !== undefined && !video) {
return res.status(400).json({ fehler: "video_unklar" });
@@ -963,8 +1038,9 @@ reaktionRouter.post("/workspace/api/reaktion/video", nurHost, express.json({ lim
db().prepare("UPDATE reaktion SET tempo = ? WHERE id = 1").run(tempo);
}
if (video) {
db().prepare("UPDATE reaktion SET video = ?, laeuft = ?, sekunde = ?, geaendert = ? WHERE id = 1")
.run(video, laeuft, sekunde, jetzt());
db().prepare(`UPDATE reaktion SET video = ?, laeuft = ?, sekunde = ?,
sekunde_am = ?, geaendert = ? WHERE id = 1`)
.run(video, laeuft, sekunde, sekundeAm, jetzt());
/* Der Name wird NACHGEREICHT und nicht abgewartet: Diese
Route laeuft waehrend der Sendung im Sekundentakt. Sie
darf unter keinen Umstaenden auf YouTube warten. */
@@ -977,19 +1053,11 @@ reaktionRouter.post("/workspace/api/reaktion/video", nurHost, express.json({ lim
});
}
} else {
db().prepare("UPDATE reaktion SET laeuft = ?, sekunde = ? WHERE id = 1")
.run(laeuft, sekunde);
db().prepare("UPDATE reaktion SET laeuft = ?, sekunde = ?, sekunde_am = ? WHERE id = 1")
.run(laeuft, sekunde, sekundeAm);
}
const s = sendung();
melden("reaktion", {
was: "video",
video: s.video, laeuft: !!s.laeuft, sekunde: s.sekunde,
tempo: Number(s.tempo) || 1,
/* Die Uhrzeit des Servers geht mit: Der Empfaenger rechnet
die Laufzeit seit dem Absenden dazu. Ohne das waere jeder
genau so weit hinten, wie sein Netz langsam ist. */
gesendet: Date.now(),
});
videoRundruf(s);
res.json({ ok: true, tempo: Number(s.tempo) || 1 });
} catch (fehler) {
console.error("[reaktion] Video:", fehler?.message);
@@ -1073,6 +1141,7 @@ reaktionRouter.post("/workspace/api/reaktion/tauschen", nurHost,
try {
db().prepare(`UPDATE reaktion SET
video = ?, video_titel = ?, sekunde = ?,
sekunde_am = unixepoch('subsec') * 1000,
zweit_video = ?, zweit_titel = ?, zweit_sekunde = ?,
geaendert = ?
WHERE id = 1`)
@@ -1095,11 +1164,7 @@ reaktionRouter.post("/workspace/api/reaktion/tauschen", nurHost,
}
const n = sendung();
melden("reaktion", {
was: "video",
video: n.video, laeuft: !!n.laeuft, sekunde: n.sekunde,
tempo: Number(n.tempo) || 1, gesendet: Date.now(),
});
videoRundruf(n);
melden("reaktion", { was: "getauscht", lage: oeffentlich(lage(req.person)) });
res.json(lage(req.person));
} catch (fehler) {