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,