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:
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user