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:
+364
-28
@@ -873,13 +873,250 @@
|
||||
|
||||
NACHGEFUEHRT WIRD NUR BEI ECHTER ABWEICHUNG. Ein Player, dem man
|
||||
jede Sekunde eine neue Position gibt, ruckelt sichtbar. Erst ab
|
||||
anderthalb Sekunden Unterschied wird gesprungen -- darunter
|
||||
gut einer Sekunde Unterschied wird gesprungen -- darunter
|
||||
merkt es niemand, darüber merkt es jeder.
|
||||
|
||||
---------------------------------------------------------------
|
||||
WIE DIE SOLL-STELLE GERECHNET WIRD (neu gefasst 30.09.2026)
|
||||
|
||||
soll = sekunde + (alter + laufzeit + seither) * tempo
|
||||
|
||||
Hier stand bis heute `Date.now() - stand.gesendet` — die Uhr des
|
||||
Telefons gegen die Uhr des Servers. Zwei Uhren, die nie jemand
|
||||
verglichen hat. Gemessen: Ein Gerät, dessen Uhr 30 Sekunden
|
||||
vorgeht, spielte das Video **29,97 Sekunden weiter** als alle
|
||||
anderen. Kein langsames Netz, kein schwaches Handy — nur eine
|
||||
falsch gestellte Uhr, wie sie auf jedem dritten Telefon steht.
|
||||
|
||||
Die neue Rechnung benutzt überhaupt keine Tagesuhr mehr:
|
||||
|
||||
`alter` sagt der Server, gerechnet aus ZWEI Serverzeiten.
|
||||
Ein Alter ist eine Differenz — dafür braucht der
|
||||
Empfänger keinen eigenen Kalender.
|
||||
`laufzeit` die halbe kleinste selbst gemessene Umlaufzeit.
|
||||
`seither` `performance.now()`, eine Stoppuhr ab Seitenstart.
|
||||
Sie überlebt Zeitumstellung, Zeitzonenwechsel und
|
||||
das Aufwachen aus dem Ruhezustand; `Date.now()`
|
||||
springt dabei.
|
||||
`tempo` Bei 2× läuft die Uhr im Video doppelt so schnell.
|
||||
Das fehlte bisher ganz — bei einer Sekunde
|
||||
Laufzeit war das eine Sekunde Fehler.
|
||||
|
||||
UND ES WIRD NICHT NUR AUF ZURUF NACHGEFÜHRT. Der Host meldet
|
||||
alle fünf Sekunden; wer dazwischen stockt, hing bis zu fünf
|
||||
Sekunden hinterher und wurde dann mit einem Ruck geholt. Jetzt
|
||||
rechnet jeder alle zwei Sekunden selbst nach — der Anker weiß,
|
||||
wo die Sendung steht, auch ohne neue Meldung.
|
||||
|
||||
WER NICHTS WEISS, SPRINGT NICHT. Kommt kein `alter` mit oder ist
|
||||
die Seite so lange weggewesen, dass die Hochrechnung nicht mehr
|
||||
trägt, liefert `sollJetzt()` `null` — und `null` heißt „nicht
|
||||
anfassen", nicht „auf null springen". Lieber eine Sekunde hinten
|
||||
als an der falschen Stelle.
|
||||
================================================================= */
|
||||
let spieler = null;
|
||||
let spielerBereit = false;
|
||||
let letztesVideo = '';
|
||||
|
||||
/* ---- Wie lange eine Meldung zu mir braucht ---------------------
|
||||
Die HALBE KLEINSTE Umlaufzeit, nicht der Durchschnitt. Eine
|
||||
einzelne langsame Antwort sagt etwas über die Warteschlange
|
||||
unterwegs, nicht über den Weg; die schnellste gemessene kommt
|
||||
der reinen Laufzeit am nächsten. Dieselbe Überlegung wie bei
|
||||
jeder Zeitmessung über ein Netz. */
|
||||
let laufzeitMs = 0;
|
||||
let kleinsteUmlauf = Infinity;
|
||||
function umlaufMerken(ms) {
|
||||
if (!(ms >= 0) || ms > 10_000) return;
|
||||
if (ms < kleinsteUmlauf) { kleinsteUmlauf = ms; laufzeitMs = ms / 2; }
|
||||
}
|
||||
|
||||
/* ---- Der Anker: der letzte bekannte Stand ----------------------
|
||||
`seit` ist eine STOPPUHR (`performance.now()`), keine Uhrzeit.
|
||||
Gesetzt wird er in dem Augenblick, in dem die Nachricht ANKOMMT
|
||||
— nicht dort, wo sie später verarbeitet wird. Das ist kein
|
||||
Detail: Zwischen dem Laden des Stands und dem Fertigwerden des
|
||||
YouTube-Spielers vergehen ein bis drei Sekunden, und genau die
|
||||
fehlten sonst in der Rechnung. */
|
||||
let anker = null;
|
||||
function ankerSetzen(stand) {
|
||||
if (!stand) return;
|
||||
anker = {
|
||||
sekunde: Number(stand.sekunde) || 0,
|
||||
laeuft: !!stand.laeuft,
|
||||
tempo: Number(stand.tempo) || 1,
|
||||
/* `null` heißt „unbekannt" und ist etwas anderes als 0. */
|
||||
alterMs: Number.isFinite(stand.alter) ? stand.alter : null,
|
||||
seit: performance.now(),
|
||||
};
|
||||
}
|
||||
|
||||
/* Weiter als das trägt keine Hochrechnung. Wer eine Viertelstunde
|
||||
im Hintergrund lag, weiß nichts mehr — und eine Zahl, die aus
|
||||
einem Anker von vor einer Viertelstunde stammt, ist schlimmer
|
||||
als keine. */
|
||||
const ANKER_HAELT = 90;
|
||||
|
||||
/** Wo müsste ich jetzt stehen? `null` = weiß ich nicht. */
|
||||
function sollJetzt() {
|
||||
if (!anker) return null;
|
||||
if (!anker.laeuft) return anker.sekunde;
|
||||
if (anker.alterMs === null) return anker.sekunde;
|
||||
const her = (anker.alterMs + laufzeitMs + (performance.now() - anker.seit)) / 1000;
|
||||
if (her < 0 || her > ANKER_HAELT) return null;
|
||||
return anker.sekunde + her * anker.tempo;
|
||||
}
|
||||
|
||||
/** Wer die Sendung führt, folgt niemandem — er IST der Stand.
|
||||
*
|
||||
* GEPRÜFT WIRD DIE KENNUNG, NICHT DIE ROLLE: `ich.host` ist bei
|
||||
* DogFather UND bei seiner rechten Hand wahr, geführt wird aber
|
||||
* von genau einem. Wer danebensteht, muss mitlaufen. */
|
||||
function binSteuerHost() {
|
||||
return !!(lage?.host?.id && meineId && lage.host.id === meineId);
|
||||
}
|
||||
|
||||
/* ---- Springen, und wie weit vorgehalten wird -------------------
|
||||
|
||||
Nach einem Sprung lädt der Spieler nach. Bis er wieder spielt,
|
||||
ist die Sendung weitergelaufen — man landet also genau um diese
|
||||
Ladezeit hinten und braucht bald den nächsten Sprung. Deshalb
|
||||
wird ein Vorlauf dazugerechnet.
|
||||
|
||||
GEMESSEN WIRD DAS ERGEBNIS, NICHT DIE URSACHE.
|
||||
|
||||
Der erste Anlauf (30.09.2026) stoppte die LADEZEIT: Wie lange
|
||||
dauert es vom Sprung bis zum Weiterspielen? Klingt richtig und
|
||||
war systematisch falsch. Ein Sprung in einen schon geladenen
|
||||
Bereich dauert nämlich gar nicht — der Spieler verlässt den
|
||||
Zustand „läuft" nie, die Meldung darüber kommt nie, und gelernt
|
||||
wurde ausschließlich aus den LANGSAMEN Sprüngen. Der Vorlauf
|
||||
kannte nur den Weg nach oben.
|
||||
|
||||
Gemessen kam dabei heraus: Die Zuschauerin lag 0,95 s VOR dem
|
||||
Host — über acht Messungen auf zwei Hundertstel stabil. Ein
|
||||
Mittelwert, der nur eine Sorte Messung sieht, ist keiner.
|
||||
|
||||
Jetzt wird zweieinhalb Sekunden nach jedem Sprung nachgesehen,
|
||||
wie weit man DANEBEN liegt, und der Vorlauf um einen Teil dieses
|
||||
Restes verschoben. Zu weit vorn zieht ihn herunter, zu weit
|
||||
hinten hinauf — ein Regelkreis auf die Größe, auf die es
|
||||
ankommt, statt auf einen Hilfswert, von dem man hofft, dass er
|
||||
dasselbe bedeutet.
|
||||
|
||||
Auf einer schnellen Leitung geht er gegen null, auf einer
|
||||
langsamen wächst er mit. Eine feste Zahl wäre wieder die
|
||||
Rechnung von gestern. */
|
||||
let vorlauf = 0;
|
||||
let restPruefenAb = 0;
|
||||
/* `aufStelleSpringen` UND NICHT `springen` (berichtigt 30.09.2026).
|
||||
|
||||
Sie hiess zuerst `springen` -- genau wie die Funktion des Hosts
|
||||
1600 Zeilen weiter unten, die UM so viele Sekunden spult. In
|
||||
einem gemeinsamen Rumpf gewinnt die spaetere Deklaration, und
|
||||
zwar rueckwirkend fuer die ganze Datei. Nachgestellt:
|
||||
|
||||
function springen(ziel) { return 'MEINER'; }
|
||||
function springen(um) { return 'HOSTS'; }
|
||||
springen(101); // -> „HOSTS"
|
||||
|
||||
Ein Zuschauer bei Sekunde 100, dem „geh auf 101" gesagt wird,
|
||||
waere damit bei 201 gelandet. Und beim naechsten Mal bei 302.
|
||||
|
||||
Die Messung hat es NICHT gefunden: Oertlich blieb der Abstand
|
||||
unter der Sprungschwelle, die Zeile lief in drei Laeufen kein
|
||||
einziges Mal. Auf einer echten Leitung waere sie staendig
|
||||
gelaufen. */
|
||||
function aufStelleSpringen(ziel) {
|
||||
spieler.seekTo(Math.max(0, ziel + (anker?.laeuft ? vorlauf : 0)), true);
|
||||
/* Erst nachsehen, wenn das Nachladen vorbei sein kann. Sofort
|
||||
gemessen wäre der „Rest" nur die Ladezeit selbst. */
|
||||
restPruefenAb = performance.now() + 2500;
|
||||
}
|
||||
/** Was ist von dem Sprung übrig geblieben? `weg` ist positiv, wenn
|
||||
* man hinten liegt. */
|
||||
function vorlaufLernen(weg) {
|
||||
if (!restPruefenAb || performance.now() < restPruefenAb) return;
|
||||
restPruefenAb = 0;
|
||||
/* Mehr als fünf Sekunden Rest ist kein Nachladefehler, sondern
|
||||
etwas anderes (ein Sprung des Hosts, ein Videowechsel) — und
|
||||
darf den Vorlauf nicht verziehen. */
|
||||
if (Math.abs(weg) > 5) return;
|
||||
/* Nur ein Teil des Restes, damit einzelne Ausreißer den Wert
|
||||
nicht hin und her werfen. */
|
||||
vorlauf = Math.max(0, Math.min(1.5, vorlauf + 0.4 * weg));
|
||||
}
|
||||
|
||||
/* Ab wann gesprungen wird. Darunter merkt es niemand; ein Sprung
|
||||
kostet dagegen immer ein sichtbares Nachladen. */
|
||||
const SPRUNG_AB = 1.2;
|
||||
|
||||
/** Einmal nachführen: stehe ich da, wo die Sendung steht?
|
||||
*
|
||||
* `auchHost` — WER FÜHRT, WIRD NICHT NACHGEFÜHRT, mit genau einer
|
||||
* Ausnahme: dem ersten Mal. Sein Spieler IST der Stand; ihn alle
|
||||
* zwei Sekunden auf seine eigene Meldung zu setzen, wäre ein Echo,
|
||||
* das sich selbst hinterherläuft (genau der Fehler vom 29.09., als
|
||||
* ein Pufferer den Host über den Umweg des Servers anhielt).
|
||||
*
|
||||
* Beim Laden hat er aber noch gar keine Stelle. Im Quelltext stand
|
||||
* dazu schon: „Lädt der Host mitten in der Sendung neu, soll er
|
||||
* dort weitermachen, wo die Sendung steht, und nicht bei null."
|
||||
* Ohne diese Ausnahme finge er bei null an — und alle anderen
|
||||
* wären ihm eine Sekunde später dorthin gefolgt. */
|
||||
function nachfuehren(auchHost = false) {
|
||||
if (!spielerBereit || !anker) return;
|
||||
if (!auchHost && binSteuerHost()) return;
|
||||
const zustand = spieler.getPlayerState();
|
||||
/* WÄHREND GEPUFFERT WIRD, WIRD NICHT GESPRUNGEN. Ein Sprung löst
|
||||
neues Puffern aus — auf einer schwachen Leitung käme man aus
|
||||
dem Nachladen nicht mehr heraus, und genau das fühlt sich an
|
||||
wie „das Video hängt". */
|
||||
if (zustand === PUFFERT) return;
|
||||
const soll = sollJetzt();
|
||||
if (soll === null) { standAuffrischen(); return; }
|
||||
const ist = spieler.getCurrentTime();
|
||||
const weg = soll - ist;
|
||||
/* Ablesbar von außen, wie `data-spieler` daneben — die Messung
|
||||
braucht eine Zahl, die das Programm selbst für wahr hält. */
|
||||
const platz = $('video-platz');
|
||||
if (platz) {
|
||||
platz.dataset.stelle = ist.toFixed(2);
|
||||
platz.dataset.versatz = weg.toFixed(2);
|
||||
}
|
||||
/* ZUERST LERNEN, DANN ENTSCHEIDEN. Andersherum wäre der Rest
|
||||
schon wegkorrigiert, bevor jemand ihn gesehen hat — und der
|
||||
Vorlauf bliebe für immer auf seinem ersten Wert. */
|
||||
vorlaufLernen(weg);
|
||||
if (Math.abs(weg) > SPRUNG_AB) aufStelleSpringen(soll);
|
||||
if (anker.laeuft && zustand !== SPIELT && zustand !== PUFFERT) spieler.playVideo();
|
||||
if (!anker.laeuft && zustand === SPIELT) spieler.pauseVideo();
|
||||
}
|
||||
|
||||
/* Der eigene Takt. Der Host meldet alle fünf Sekunden; wer
|
||||
dazwischen stockt, hing bisher bis zu fünf Sekunden hinterher.
|
||||
Zwei Sekunden sind der Kompromiss: oft genug, dass man es nicht
|
||||
merkt, selten genug, dass es nichts kostet. */
|
||||
let gleichUhr = null;
|
||||
function gleichlaufTakt() {
|
||||
clearInterval(gleichUhr);
|
||||
/* Über eine Pfeilfunktion und nicht direkt: `setInterval`
|
||||
übergibt in manchen Umgebungen die Zahl der Verspätungen als
|
||||
erstes Argument — das wäre hier ein wahrheitswertiges
|
||||
`auchHost` und machte aus der Ausnahme die Regel. */
|
||||
gleichUhr = setInterval(() => nachfuehren(), 2000);
|
||||
}
|
||||
|
||||
/* Wenn der Anker nicht mehr trägt (lange im Hintergrund), wird er
|
||||
geholt statt geraten. Mit Bremse — sonst fragte eine Seite ohne
|
||||
Verbindung im Zwei-Sekunden-Takt. */
|
||||
let letztesAuffrischen = 0;
|
||||
function standAuffrischen() {
|
||||
if (performance.now() - letztesAuffrischen < 10_000) return;
|
||||
letztesAuffrischen = performance.now();
|
||||
standHolen(true);
|
||||
}
|
||||
|
||||
function apiLaden() {
|
||||
if (window.YT && window.YT.Player) { spielerBauen(); return; }
|
||||
if (document.getElementById('yt-api')) return;
|
||||
@@ -933,6 +1170,10 @@
|
||||
Sendung steht, und nicht bei null. */
|
||||
folgen(lage);
|
||||
if (lage.ich.host) hostTakt();
|
||||
/* FÜR ALLE, AUCH FÜR DEN HOST. `nachfuehren()` steigt bei
|
||||
dem, der führt, selbst wieder aus — und wechselt die
|
||||
Führung mitten in der Sendung, läuft der Takt schon. */
|
||||
gleichlaufTakt();
|
||||
},
|
||||
onStateChange: (e) => {
|
||||
/* Der Zustand steht am Element -- ablesbar von aussen, ohne
|
||||
@@ -963,8 +1204,33 @@
|
||||
nachgeschlagen und danach falsch erinnert wird. */
|
||||
const SPIELT = 1, PUFFERT = 3;
|
||||
|
||||
async function hostMelden() {
|
||||
if (!spielerBereit || !lage || lage.stand !== 'live') return;
|
||||
/** @param {object} dazu Was ausser dem Spielstand mitgehen soll
|
||||
* (heute nur `tempo`).
|
||||
*
|
||||
* ES GIBT NUR EINE STELLE, DIE DEN SPIELSTAND MELDET.
|
||||
* Der Tempo-Knopf hatte dieselbe Meldung ein zweites Mal von Hand
|
||||
* zusammengebaut -- und dabei beides falsch gemacht, was diese
|
||||
* Funktion richtig macht: Er hielt Puffern fuer Stillstand
|
||||
* (`=== 1`) und liess das Alter der Ablesung weg. Wer das Tempo
|
||||
* umstellte, waehrend seine Leitung nachlud, hielt damit alle
|
||||
* Zuschauer an. */
|
||||
async function hostMelden(dazu = null) {
|
||||
if (!lage) return null;
|
||||
/* ---- HERZSCHLAG ODER ANSAGE? ------------------------------
|
||||
Der Fuenf-Sekunden-Takt meldet nur waehrend der Sendung --
|
||||
sonst schriebe eine offene Regieseite dauernd in eine
|
||||
Sendung, die gar nicht laeuft.
|
||||
|
||||
Eine ABSICHTLICHE Ansage (`dazu`, heute das Tempo) geht
|
||||
dagegen immer hinaus. Das Tempo laesst sich naemlich vor der
|
||||
Sendung stellen; die Route verlangt `nurHost` und sonst
|
||||
nichts. Ohne diese Unterscheidung bekaeme Filipe in der
|
||||
Vorbereitung „Das Tempo ging nicht." zu sehen.
|
||||
|
||||
Der Unterschied ist inhaltlich: Ein Herzschlag sagt „so steht
|
||||
es gerade" und darf ausfallen. Eine Ansage sagt „mach das ab
|
||||
jetzt" und muss ankommen. */
|
||||
if (!dazu && (!spielerBereit || lage.stand !== 'live')) return null;
|
||||
try {
|
||||
/* PUFFERN IST KEIN ANHALTEN (29.09.2026).
|
||||
|
||||
@@ -983,23 +1249,56 @@
|
||||
|
||||
Gemessen in mess-reaktion: `Host=laeuft` → ein gemeldeter
|
||||
Pufferer → `Host=pause`. */
|
||||
const zustand = spieler.getPlayerState();
|
||||
/* OHNE FERTIGEN SPIELER GIBT ES KEINEN SPIELSTAND -- aber die
|
||||
Ansage geht trotzdem hinaus. Genau so hat es der Tempo-Knopf
|
||||
vorher gemacht (`sekunde: 0`), und das war richtig. */
|
||||
const zustand = spielerBereit ? spieler.getPlayerState() : -1;
|
||||
const laeuft = zustand === SPIELT || zustand === PUFFERT;
|
||||
const sekunde = spieler.getCurrentTime();
|
||||
await hole('/workspace/api/reaktion/video', {
|
||||
const gelesenAm = performance.now();
|
||||
const sekunde = spielerBereit ? spieler.getCurrentTime() : 0;
|
||||
/* WIE ALT DIE ABLESUNG IST, WENN SIE ANKOMMT.
|
||||
Das Ablesen, das Zusammenbauen und der Weg zum Server
|
||||
dauern zusammen ein paar Dutzend Millisekunden. Ohne diese
|
||||
Zahl hielte der Server die Sekunde für taufrisch, und jeder
|
||||
Zuschauer säße genau um diese Spanne zu weit hinten.
|
||||
Gerechnet wird mit der Stoppuhr, nicht mit dem Kalender —
|
||||
eine örtliche Differenz stimmt auch auf einem Gerät mit
|
||||
falsch gestellter Uhr. */
|
||||
const t0 = performance.now();
|
||||
const a = await hole('/workspace/api/reaktion/video', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ laeuft, sekunde }),
|
||||
body: JSON.stringify({
|
||||
...(dazu || {}),
|
||||
laeuft, sekunde,
|
||||
alter: Math.round(performance.now() - gelesenAm + laufzeitMs),
|
||||
}),
|
||||
});
|
||||
umlaufMerken(performance.now() - t0);
|
||||
return a;
|
||||
} catch { /* der nächste Takt versucht es wieder */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Alle anderen folgen. */
|
||||
/** Alle anderen folgen.
|
||||
*
|
||||
* DER ANKER WIRD HIER NICHT GESETZT, sondern dort, wo die
|
||||
* Nachricht ANKOMMT (`standHolen` und der Ereignisstrom). Der
|
||||
* Unterschied sind ein bis drei Sekunden: `folgen(lage)` läuft
|
||||
* aus `onReady`, und bis der YouTube-Spieler fertig ist, vergeht
|
||||
* genau diese Zeit. Sie hier in den Anker zu schreiben hieße,
|
||||
* sie zu verlieren. */
|
||||
function folgen(stand) {
|
||||
if (!spielerBereit || !stand) return;
|
||||
if (stand.video && stand.video !== letztesVideo) {
|
||||
letztesVideo = stand.video;
|
||||
spieler.loadVideoById(stand.video, stand.sekunde || 0);
|
||||
/* AN DIE HOCHGERECHNETE STELLE, nicht an die gemeldete. Ein
|
||||
Videowechsel mitten in der Sendung ist der Fall, in dem der
|
||||
Rückstand am meisten auffällt — alle anderen laufen weiter,
|
||||
während hier noch geladen wird. */
|
||||
const start = sollJetzt();
|
||||
spieler.loadVideoById(stand.video,
|
||||
Number.isFinite(start) ? Math.max(0, start) : (stand.sekunde || 0));
|
||||
/* YouTube setzt die Geschwindigkeit beim Laden auf 1 zurück.
|
||||
Ein kurzer Nachschlag, sobald das neue Video bereit ist --
|
||||
sonst liefe jedes zweite Video im falschen Tempo. */
|
||||
@@ -1007,14 +1306,11 @@
|
||||
return;
|
||||
}
|
||||
if (stand.tempo) tempoNachziehen(stand.tempo);
|
||||
/* Die Laufzeit der Meldung wird dazugerechnet. Ohne das wäre
|
||||
jeder genau so weit hinten, wie sein Netz langsam ist. */
|
||||
const unterwegs = stand.gesendet ? Math.max(0, (Date.now() - stand.gesendet) / 1000) : 0;
|
||||
const soll = (stand.sekunde || 0) + (stand.laeuft ? unterwegs : 0);
|
||||
const ist = spieler.getCurrentTime();
|
||||
if (Math.abs(ist - soll) > 1.5) spieler.seekTo(soll, true);
|
||||
if (stand.laeuft && spieler.getPlayerState() !== 1) spieler.playVideo();
|
||||
if (!stand.laeuft && spieler.getPlayerState() === 1) spieler.pauseVideo();
|
||||
/* `true`: Aus `folgen()` heraus darf auch der Host einmal
|
||||
einsteigen — von hier kommt entweder sein eigener erster
|
||||
Aufruf beim Laden oder ein Rundruf, und Rundrufe erreichen
|
||||
`folgen()` bei ihm ohnehin nicht. */
|
||||
nachfuehren(true);
|
||||
}
|
||||
|
||||
/** Die Geschwindigkeit nachziehen.
|
||||
@@ -2401,7 +2697,18 @@
|
||||
/* ---- Die Videospur bedienen -------------------------------------- */
|
||||
function spielPause() {
|
||||
if (!spielerBereit) return;
|
||||
if (spieler.getPlayerState() === 1) spieler.pauseVideo();
|
||||
/* PUFFERN ZAEHLT ALS LAEUFT (berichtigt 30.09.2026).
|
||||
|
||||
Hier stand `=== 1`. Waehrend nachgeladen wird, ist der Zustand
|
||||
3 -- also nicht 1, also ging der Klick in den else-Zweig und
|
||||
rief `playVideo()`. Wer die Pause drueckte, bekam „weiter".
|
||||
Der Knopf tat das Gegenteil dessen, was auf ihm stand, und
|
||||
zwar genau dann, wenn die Leitung ohnehin schon stockt.
|
||||
|
||||
Dieselbe Verwechslung wie am 29.09. in `hostMelden()`. Die
|
||||
Reparatur blieb damals an dieser einen Stelle haengen. */
|
||||
const zustand = spieler.getPlayerState();
|
||||
if (zustand === SPIELT || zustand === PUFFERT) spieler.pauseVideo();
|
||||
else spieler.playVideo();
|
||||
hostMelden();
|
||||
}
|
||||
@@ -2496,15 +2803,14 @@
|
||||
* zweites Mal. */
|
||||
async function tempoSetzen(wert) {
|
||||
tempoNachziehen(wert);
|
||||
const a = await hole('/workspace/api/reaktion/video', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tempo: wert,
|
||||
laeuft: spielerBereit && spieler.getPlayerState() === 1,
|
||||
sekunde: spielerBereit ? spieler.getCurrentTime() : 0,
|
||||
}),
|
||||
});
|
||||
/* UEBER `hostMelden()` UND NICHT NOCH EINMAL VON HAND
|
||||
(berichtigt 30.09.2026). Hier stand dieselbe Meldung ein
|
||||
zweites Mal, und sie machte beides falsch, was `hostMelden()`
|
||||
richtig macht: Sie hielt Puffern fuer Stillstand und liess das
|
||||
Alter der Ablesung weg. Wer das Tempo umstellte, waehrend
|
||||
seine Leitung nachlud, hielt damit alle Zuschauer an. */
|
||||
const a = await hostMelden({ tempo: wert });
|
||||
if (!a) { sagFehler('Das Tempo ging nicht.'); return; }
|
||||
if (!a.ok) { melde(await a.json().catch(() => null), 'Das Tempo ging nicht.'); return; }
|
||||
lage = { ...lage, tempo: wert };
|
||||
zeichnen();
|
||||
@@ -2554,7 +2860,13 @@
|
||||
|
||||
/* Das Positionsband */
|
||||
if (!spielerBereit) return;
|
||||
const laeuft = spieler.getPlayerState() === 1;
|
||||
/* UND AUCH HIER IST PUFFERN KEINE PAUSE. Ohne das sprang die
|
||||
Lampe bei jedem Stocken der Leitung auf „Pause" und gleich
|
||||
wieder zurueck -- ein Flackern, das sagt „etwas ist kaputt",
|
||||
waehrend nur nachgeladen wird. Dasselbe galt fuer das Bild auf
|
||||
dem Knopf: zwei Balken, Dreieck, zwei Balken. */
|
||||
const zustandJetzt = spieler.getPlayerState();
|
||||
const laeuft = zustandJetzt === SPIELT || zustandJetzt === PUFFERT;
|
||||
$('spur').dataset.laeuft = laeuft ? 'ja' : 'nein';
|
||||
$('spur-marke').textContent = laeuft ? 'Läuft' : 'Pause';
|
||||
$('v-spiel-bild').innerHTML = laeuft
|
||||
@@ -3379,6 +3691,15 @@
|
||||
Das Tempo kommt nicht aus seinem Spieler zurück, sondern
|
||||
aus dem Pult. Es ist kein Echo seiner selbst, sondern
|
||||
eine Ansage. */
|
||||
/* DER ANKER ZUERST, und zwar fuer JEDEN -- auch fuer den,
|
||||
der fuehrt. Er ist die Beschreibung dessen, was gerade
|
||||
laeuft, nicht eine Anweisung an den Spieler. Wer sie
|
||||
nicht mitschreibt, hat nach einem Wechsel der Fuehrung
|
||||
keinen Stand.
|
||||
|
||||
HIER OBEN, weil `seit` eine Stoppuhr ist: Sie gehoert in
|
||||
den Augenblick der Ankunft. */
|
||||
ankerSetzen(d);
|
||||
if (lage?.host?.id && lage.host.id === meineId) {
|
||||
if (d.tempo) tempoNachziehen(d.tempo);
|
||||
} else {
|
||||
@@ -4303,12 +4624,27 @@
|
||||
* Vorrang des Rundrufs ein Verlust. */
|
||||
async function standHolen(sanft) {
|
||||
const beim = standFolge;
|
||||
/* GLEICH MITGEMESSEN: wie lange ein Hin und Zurueck dauert.
|
||||
Daraus schaetzt der Gleichlauf die Laufzeit einer Meldung zu
|
||||
mir. Ein eigener Weg nur zum Messen waere ein zweiter Weg,
|
||||
der irgendwann eine andere Antwort gibt als dieser. */
|
||||
const t0 = performance.now();
|
||||
const a = await hole('/workspace/api/reaktion');
|
||||
umlaufMerken(performance.now() - t0);
|
||||
if (!a.ok) { sagFehler('Die Reaction ist gerade nicht erreichbar.'); return; }
|
||||
const neu = await a.json();
|
||||
if (sanft && beim !== standFolge) return;
|
||||
/* DER ANKER GEHT SOFORT LOS -- vor dem Zeichnen, vor allem
|
||||
anderen. Er merkt sich mit `performance.now()`, wann diese
|
||||
Zahl ankam; jede Sekunde, die zwischen Ankunft und Auswertung
|
||||
liegt, waere sonst ein Rueckstand, den niemand mehr sieht. */
|
||||
ankerSetzen(neu);
|
||||
lage = neu;
|
||||
zeichnen();
|
||||
/* Und gleich hinterher: Steht der Spieler schon, wird jetzt
|
||||
nachgefuehrt. Ohne das wartete ein frisch geladener Stand bis
|
||||
zum naechsten Takt. */
|
||||
nachfuehren();
|
||||
}
|
||||
|
||||
async function los() {
|
||||
|
||||
Reference in New Issue
Block a user