/* ===================================================================== WIE DIE REACTION AUSSIEHT (28.09.2026) Die Pruefung nebenan misst, was die Schnittstelle antwortet. Das sagt nichts darueber, ob ein Mensch damit einen Abend verbringen moechte. Dieses Werkzeug oeffnet die Seite in einem echten Browser, schaltet sie durch alle drei Zustaende und legt Bilder ab. Gemessen wird dabei auch, was man sonst uebersieht: - laeuft etwas in die Konsole? - ragt etwas seitlich hinaus? - ist der Saal so hoch wie das Fenster, oder muss man scrollen? NIE DIE ECHTE DATENBANK. Eigener Ordner, eigener Port, eigenes Zertifikat -- und am Ende ist alles weg. Aufrufen mit: node server/mess-reaktion.mjs ===================================================================== */ import { mkdtempSync, rmSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { request } from "node:http"; import { execFileSync } from "node:child_process"; import { createServer as httpsServer } from "node:https"; import { zertifikatBauen } from "./helfer-openssl.mjs"; const HIER = dirname(fileURLToPath(import.meta.url)); const ordner = mkdtempSync(join(tmpdir(), "mess-reakt-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); const PORT = 5483; const HTTPS_PORT = PORT + 1; process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; for (const art of ["uncaughtException", "unhandledRejection"]) { process.on(art, (f) => { console.error(`\nABBRUCH (${art}): ${f?.stack || f}`); process.exit(8); }); } const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 1000)); const CREW = "crew.dogfather-universe.com"; /* ---------- Leute ------------------------------------------------------- */ const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes, createHmac } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const kenn = randomBytes(32).toString("hex"); d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)" + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING") .run("code_kennung_schluessel", kenn, new Date().toISOString()); const ALT = new Date(Date.now() - 200 * 86400000).toISOString(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n," + " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, createHmac("sha256", kenn).update(code).digest("hex"), ALT); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const CODES = { admin: "CODE-M00-000001", gast: "CODE-M01-000001", gast2: "CODE-M02-000001", modi: "CODE-M03-000001", linke: "CODE-M04-000001", }; const IDS = { admin: anlegen("DogFather", "admin", CODES.admin), gast: anlegen("Nachtfalter", "gast", CODES.gast), gast2: anlegen("Lenny", "gast", CODES.gast2), /* Fuer die Moderation. Sie ist der Teil, bei dem ein Fehler am meisten kostet -- also wird sie im Browser gemessen und nicht nur ueber die Schnittstelle. */ modi: anlegen("Miss", "modi", CODES.modi), linke: anlegen("Funny", "linke", CODES.linke), }; d.close(); function roh(pfad, { method = "GET", keks = "", rumpf = null } = {}) { return new Promise((fertig, schief) => { const daten = rumpf === null ? null : Buffer.from(JSON.stringify(rumpf)); const a = request({ host: "127.0.0.1", port: PORT, path: pfad, method, headers: { Host: CREW, Origin: `https://${CREW}`, ...(keks ? { Cookie: keks } : {}), ...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}), }, }, (antwort) => { const teile = []; antwort.on("data", (s) => teile.push(s)); antwort.on("end", () => { const text = Buffer.concat(teile).toString("utf8"); let j = null; try { j = JSON.parse(text); } catch { /* kein JSON */ } fertig({ code: antwort.statusCode, text, j, kopf: antwort.headers }); }); }); a.on("error", schief); if (daten) a.write(daten); a.end(); }); } async function anmelden(rolle) { const a = await roh("/workspace/api/anmelden", { method: "POST", rumpf: { rolle: rolle === "gast2" ? "gast" : rolle, code: CODES[rolle], alter_ok: true }, }); return [].concat(a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; "); } const k = { admin: await anmelden("admin"), gast: await anmelden("gast"), gast2: await anmelden("gast2"), modi: await anmelden("modi"), linke: await anmelden("linke"), }; /* ---------- Die Buehne fuellen ------------------------------------------ */ await roh("/workspace/api/reaktion", { method: "PUT", keks: k.admin, rumpf: { titel: "Wir schauen den neuen Trailer", video: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", beginnt_am: new Date(Date.now() + 7 * 60_000).toISOString(), }, }); /* ZWEI STUECKE IN DIE WARTESCHLANGE. Eine leere Liste sieht auf einem Bildschirmfoto genauso aus wie eine kaputte -- beides ist ein Kasten ohne Zeilen. Zwei echte Eintraege zeigen, ob Titel, Vorschaubild und die vier Knoepfe je Zeile wirklich ankommen. */ for (const adresse of ["https://youtu.be/9bZkp7q19f0", "https://www.youtube.com/watch?v=kJQP7kiw5Fk"]) { await roh("/workspace/api/reaktion/liste", { method: "POST", keks: k.admin, rumpf: { video: adresse }, }); } const REDEN = [ ["gast", "Bin schon daaa 🐺"], ["gast2", "Ich auch! Hab extra Popcorn geholt"], ["gast", "Wie lange noch?"], ["admin", "Zwei Minuten, ich richte noch die Kamera"], /* DAS TEAM SCHREIBT MIT. Ohne einen Beitrag von einer linken Hand und einem Modi liesse sich gar nicht messen, ob die Abzeichen ankommen -- und eine Messung, die ihren Gegenstand nicht herstellt, misst nichts. */ ["linke", "Ich pass auf den Chat auf, schreibt ruhig"], ["modi", "Und ich auch 🙂"], ["gast2", "Kein Stress, wir sitzen bequem"], ["gast", "Letztes Mal war so gut, ich hab geheult"], ]; /* ---------- Der Vorbau -------------------------------------------------- */ const schluesselDatei = join(ordner, "p.key"); const zertDatei = join(ordner, "p.crt"); zertifikatBauen(schluesselDatei, zertDatei, CREW); const vorbau = httpsServer({ key: readFileSync(schluesselDatei), cert: readFileSync(zertDatei), }, (von, nach) => { const weiter = request({ host: "127.0.0.1", port: PORT, path: von.url, method: von.method, headers: { ...von.headers, host: `${CREW}:${HTTPS_PORT}` }, }, (antwort) => { nach.writeHead(antwort.statusCode || 502, antwort.headers); antwort.pipe(nach); }); weiter.on("error", () => { try { nach.writeHead(502); nach.end(); } catch { /* egal */ } }); von.pipe(weiter); }); await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r)); const BASIS = `https://${CREW}:${HTTPS_PORT}`; const pw = await import( "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch({ args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors", /* Kamera und Mikrofon ohne Rueckfrage -- und ein bewegtes Testbild statt eines schwarzen Rechtecks. Ohne das sieht jede Aufnahme aus, als waere die Kamera kaputt. */ "--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"], }); /* JEDE GEOEFFNETE SEITE HAELT ZWEI STEHENDE VERBINDUNGEN (der Ereignisstrom der Kopfleiste und der dieser Seite). Ein Browser laesst je Adresse nur wenige davon zu -- wer seine Seiten nicht schliesst, nimmt den spaeteren den Platz weg. GEMESSEN: Die Seiten aus den ersten beiden Abschnitten blieben offen, und danach kam Lennys Strom gar nicht mehr zustande. Die Signale gingen ins Leere, und die Kamerafenster blieben schwarz -- ein Messfehler, der wie ein Programmfehler aussah. */ async function alsWer(rolle, breite, hoehe) { const kontext = await browser.newContext({ viewport: { width: breite, height: hoehe }, /* MIT FINGER, WENN ES EIN HANDY IST. Ohne `hasTouch` meldet der Browser `pointer: fine` -- und damit greift KEINE der Regeln aus `@media (pointer: coarse)`. Dort stehen im ganzen Haus die 44-Pixel-Beruehrziele und seit dem 28.09. auch das Ausblenden der Tastenkuerzel. Eine Handy-Messung mit Mauszeiger misst eine Seite, die auf keinem Handy so aussieht -- und meldet Hoehen, die dort andere sind. */ hasTouch: breite <= 860, ignoreHTTPSErrors: true, permissions: ["camera", "microphone"], }); await kontext.addCookies((k[rolle] || "").split("; ").filter(Boolean).map((z) => { const [n, ...r] = z.split("="); return { name: n, value: r.join("="), domain: CREW, path: "/" }; })); const seite = await kontext.newPage(); const konsole = []; const anfragen = []; seite.on("request", (r) => { if (/\/api\//.test(r.url())) anfragen.push(r.url().replace(/^https?:\/\/[^/]+/, "")); }); seite.on("pageerror", (f) => konsole.push(String(f?.message || f))); seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); }); seite.on("response", (a) => { if (a.status() >= 400) konsole.push(`${a.status()} ${a.url().replace(/^https?:\/\/[^/]+/, "")}`); }); return { kontext, seite, konsole, anfragen }; } async function bild(seite, name) { await seite.screenshot({ path: join(HIER, `mess-reaktion-${name}.png`) }); console.log(` Bild: server/mess-reaktion-${name}.png`); } async function vermessen(seite, wo) { const m = await seite.evaluate(() => ({ ueber: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth), hoch: Math.max(0, document.documentElement.scrollHeight - document.documentElement.clientHeight), stand: document.getElementById("saal")?.dataset.stand || "?", schild: document.getElementById("schild-wort")?.textContent || "", })); console.log(` ${wo}: Stand „${m.stand}", Schild „${m.schild}", ` + `seitlich ${m.ueber} px, senkrecht ${m.hoch} px`); return m; } console.log("\n=== ZU ==="); { const { kontext, seite, konsole } = await alsWer("gast", 1440, 900); await seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(900); await vermessen(seite, "Rechner"); await bild(seite, "1-zu-rechner"); if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | ")); await kontext.close(); } console.log("\n=== VORBEREITUNG ==="); await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "vorbereitung" } }); for (const [wer, text] of REDEN) { await roh("/workspace/api/reaktion/chat", { method: "POST", keks: k[wer], rumpf: { text } }); } { const { kontext, seite, konsole } = await alsWer("gast", 1440, 900); await seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" }); await seite.waitForTimeout(1400); await vermessen(seite, "Rechner"); await bild(seite, "2-warte-rechner"); if (konsole.length) console.log(" KONSOLE:", konsole.slice(0, 3).join(" | ")); await kontext.close(); } console.log("\n=== LIVE ==="); await roh("/workspace/api/reaktion/stand", { method: "POST", keks: k.admin, rumpf: { stand: "live" } }); await roh("/workspace/api/reaktion/gast", { method: "POST", keks: k.admin, rumpf: { person_id: IDS.gast2 } }); /* ==== BEIDE SEITEN GLEICHZEITIG ==================================== Der Host sendet, die Zuschauerin empfaengt -- das geht nur, wenn beide zur selben Zeit offen sind. */ const host = await alsWer("admin", 1440, 900); await host.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" }); await host.seite.waitForTimeout(2500); const gast2 = await alsWer("gast2", 1280, 800); await gast2.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" }); await gast2.seite.waitForTimeout(2500); const zuschauer = await alsWer("gast", 1440, 900); await zuschauer.seite.goto(`${BASIS}/workspace/reaktion.html`, { waitUntil: "networkidle" }); /* Verbindungen brauchen ein paar Sekunden: Adressen holen, Angebot, Antwort, Kandidaten. Acht Sekunden sind grosszuegig -- es geht darum, ob es UEBERHAUPT ankommt, nicht wie schnell. */ await zuschauer.seite.waitForTimeout(8000); async function bildBreiten(seite) { return seite.evaluate(() => { const raus = {}; for (const k2 of document.querySelectorAll("#kameras .kamera")) { const v = k2.querySelector("video"); raus[k2.querySelector(".kamera__name")?.textContent || k2.dataset.wer] = { breite: v ? v.videoWidth : -1, strom: !!(v && v.srcObject), laeuft: !!(v && !v.paused), stumm: !!(v && v.muted), }; } return raus; }); } console.log("\n --- Fliesst ein Bild? ---"); for (const [wer, w] of [["Host", host.seite], ["Gast", gast2.seite], ["Zuschauerin", zuschauer.seite]]) { const z = await w.evaluate(() => ({ url: location.pathname, saal: !!document.getElementById("saal"), stand: document.getElementById("saal")?.dataset.stand, kameras: document.querySelectorAll("#kameras .kamera").length, })).catch((f) => ({ url: "FEHLER " + f.message })); console.log(` ${wer.padEnd(12)} ${z.url} saal=${z.saal} stand=${z.stand} kameras=${z.kameras}`); const q = { Host: host, Gast: gast2, Zuschauerin: zuschauer }[wer]; console.log(" Strom-Anfragen: " + q.anfragen.filter((x) => x.includes("strom")).length + " | Signale: " + q.anfragen.filter((x) => x.includes("signal")).length); } const beiZuschauer = await bildBreiten(zuschauer.seite); for (const [name, m] of Object.entries(beiZuschauer)) { const gut = m.breite > 0; console.log(` Zuschauerin sieht ${name.padEnd(12)} ` + `${gut ? "BILD " + m.breite + " px" : (m.strom ? "Strom da, aber 0 px" : "NICHTS")}` + ` ${m.laeuft ? "laeuft" : "STEHT"} ${m.stumm ? "stumm" : "mit Ton"}`); } /* AUCH DER HOST MUSS SEINEN GAST SEHEN. Wenn nur die Zuschauer etwas sehen, redet der Host gegen eine schwarze Kachel -- und merkt nicht, wenn beim Gast die Kamera aus ist. */ /* LAEUFT DAS VIDEO UEBERHAUPT? Dieselbe Falle wie bei den Kameras: Etwas, das Ton macht, darf nicht von allein losgehen. */ let videoLaeuft = 0; for (const [wer, w] of [["Host", host.seite], ["Zuschauerin", zuschauer.seite]]) { const v = await w.evaluate(() => ({ stand: document.getElementById("video-platz")?.dataset.spieler || "(nichts)", rahmen: !!document.querySelector("#video-platz iframe"), })); console.log(` Video bei ${wer.padEnd(12)} Rahmen=${v.rahmen} Zustand=${v.stand}`); if (v.stand === "laeuft" || v.stand === "puffert") videoLaeuft++; } const beimHost = await bildBreiten(host.seite); for (const [name, m] of Object.entries(beimHost)) { console.log(` Host sieht ${name.padEnd(12)} ` + `${m.breite > 0 ? "BILD " + m.breite + " px" : "NICHTS"}` + ` ${m.laeuft ? "laeuft" : "STEHT"}`); } const zaehlt = Object.values(beiZuschauer).filter((m) => m.breite > 0).length; console.log(` -> ${zaehlt} von ${Object.keys(beiZuschauer).length} Kameras kommen an.`); /* DIESE MESSUNG HAT EINEN RUECKGABEWERT -- und zwar wegen dieser einen Zahl. Alles andere kann gruen sein: Die Schnittstelle antwortet, die Seite sieht gut aus, die Verbindung steht. Wenn hier 0 steht, sitzt trotzdem jeder vor einem schwarzen Rechteck. Gemessen wurde genau das zweimal: einmal, weil das Angebot hinausging, bevor der Empfaenger zuhoeren konnte -- einmal, weil ein Video mit Ton nicht von allein startet. Beide Male sahen die Bildschirmfotos plausibel aus. */ let ausgang = 0; /* BEKANNTE, AUFGESCHRIEBENE OFFENE PUNKTE. Sie sind keine Fehler dieses Laufs und kein bestandener Test -- sie sind der dritte Ausgang: Mangel bekannt, Weg beschrieben, noch nicht gegangen. Am Ende stehen sie gesondert. */ const offenePunkte = []; /* LAEUFT DAS VIDEO? Ein Zuschauer hat keine Bedienung -- startet es nicht von selbst, sitzt er eine Stunde vor einem Standbild und kann nichts dagegen tun. Gemessen war genau das der Fall. */ if (videoLaeuft < 2) { console.log(` ACHTUNG: Das Video laeuft nur bei ${videoLaeuft} von 2.`); ausgang = 1; } const beimHostAn = Object.values(beimHost).filter((m) => m.breite > 0).length; if (beimHostAn < Object.keys(beimHost).length) { console.log(" ACHTUNG: Der Host sieht nicht alle Kameras."); ausgang = 1; } if (!zaehlt) { console.log(" ACHTUNG: Es kommt kein einziges Bild an."); ausgang = 1; } const stehen = [...Object.values(beiZuschauer), ...Object.values(beimHost)] .filter((m) => m.breite > 0 && !m.laeuft).length; if (stehen) { console.log(` ACHTUNG: ${stehen} Bild(er) kommen an, stehen aber still ` + "-- ein Video mit Ton startet nicht von allein."); ausgang = 1; } /* ===================================================================== HAENGT DAS VIDEO? -- der Host darf sich nicht selbst anhalten ===================================================================== */ console.log(""); console.log("=== Der Gleichlauf des Videos ==="); { /* Wie ein Pufferer aussieht, wenn er gemeldet wird: `laeuft:false` bei einem Spieler, der in Wahrheit laeuft. Genau das schickt `hostMelden()` im YouTube-Zustand 3. */ const pufferer = (seite) => seite.evaluate(async () => { const stelle = document.getElementById("video-platz")?.dataset.spieler; const a = await fetch("/workspace/api/reaktion/video", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ laeuft: false, sekunde: 7 }), }); return { vorher: stelle, code: a.status }; }); const stand = (seite) => seite.evaluate( () => document.getElementById("video-platz")?.dataset.spieler || "(nichts)"); /* Erst sicherstellen, dass ueberhaupt etwas laeuft -- sonst misst die Pruefung ein stehendes Video und nennt das Erfolg. */ const vorHost = await stand(host.seite); const vorZu = await stand(zuschauer.seite); console.log(` Vorher: Host=${vorHost} Zuschauerin=${vorZu}`); const laeuftVorher = (w) => w === "laeuft" || w === "puffert"; if (!laeuftVorher(vorHost) || !laeuftVorher(vorZu)) { console.log(" ACHTUNG: Vor der Messung laeuft schon nichts -- " + "die Frage laesst sich so nicht beantworten."); ausgang = 1; } else { const p = await pufferer(host.seite); console.log(` Ein Pufferer wird gemeldet: ${p.code}`); await host.seite.waitForTimeout(2000); await zuschauer.seite.waitForTimeout(500); const nachHost = await stand(host.seite); const nachZu = await stand(zuschauer.seite); console.log(` Danach: Host=${nachHost} Zuschauerin=${nachZu}`); /* DER BEFUND. Der Host hat nichts angehalten -- er hat nur gemeldet, dass gerade Daten fehlen. Steht sein Video danach, hat sein eigenes Echo es angehalten. */ if (!laeuftVorher(nachHost)) { console.log(" ACHTUNG: Der Host haelt sich selbst an. Ein kurzer " + "Pufferer meldet 'laeuft nicht', das Echo kommt zurueck und " + "drueckt Pause -- deshalb haengt das Video."); ausgang = 1; } /* ---- DIE GEGENPROBE: ein ECHTES Anhalten wirkt weiter ------ Ohne sie waere „der Host folgt sich nicht selbst" womoeglich mit einem kaputten Gleichlauf fuer alle bezahlt. GEDRUECKT WIRD DER KNOPF, nicht eine Meldung gefaelscht. Bei einem echten Anhalten meldet der Host durchgehend „steht" -- die Antwort haengt damit nicht am Zufall seines Fuenf-Sekunden-Taktes. Die erste Fassung dieser Gegenprobe hat genau daran Alarm geschlagen: Sie hielt das Selbstheilen des Systems fuer einen Fehler. */ /* GEWARTET WIRD AUF DEN ZUSTAND, NICHT AUF DIE UHR. Eine feste Wartezeit stimmt meistens -- und wenn der Fuenf-Sekunden-Takt des Hosts dazwischenfaellt, hinkt die Zuschauerin ein Ereignis hinterher und die Messung meldet einen Fehler, den es nicht gibt. Genau das ist einmal passiert. */ const warteAuf = async (seite, wert, frist = 9000) => { try { await seite.waitForFunction( (w) => document.getElementById("video-platz")?.dataset.spieler === w, wert, { timeout: frist }); return true; } catch { return false; } }; await host.seite.evaluate(() => document.getElementById("v-spiel")?.click()); await warteAuf(host.seite, "pause"); await warteAuf(zuschauer.seite, "pause"); const haltHost = await stand(host.seite); const haltZu = await stand(zuschauer.seite); console.log(` Host haelt wirklich an: Host=${haltHost} Zuschauerin=${haltZu}`); if (haltHost === "laeuft" || haltZu === "laeuft") { console.log(" ACHTUNG: Ein echtes Anhalten des Hosts kommt nicht an -- " + "dann laeuft niemand mehr synchron."); ausgang = 1; } /* Und wieder los, mit demselben Knopf. */ await host.seite.evaluate(() => document.getElementById("v-spiel")?.click()); await warteAuf(host.seite, "laeuft"); await warteAuf(zuschauer.seite, "laeuft"); const weiterHost = await stand(host.seite); const weiterZu = await stand(zuschauer.seite); console.log(` Und wieder los: Host=${weiterHost} Zuschauerin=${weiterZu}`); if (!laeuftVorher(weiterHost) || !laeuftVorher(weiterZu)) { console.log(" ACHTUNG: Nach dem Anhalten laeuft es nicht wieder an."); ausgang = 1; } /* ---- DER KERN: was meldet der Host, WAEHREND er puffert? ---- Die beiden Teile darueber pruefen die Folge. Sie blieben auch gruen, wenn der Host beim Puffern weiter „steht" meldete -- die Zuschauer ruckelten dann bei jedem Pufferer, und niemand saehe es hier. Puffern wird hergestellt: weit nach vorn springen, wohin noch nichts geladen ist. Ob das GELINGT, ist nicht sicher -- auf einer schnellen Leitung kann der Sprung fertig sein, bevor jemand hinsieht. Deshalb der dritte Ausgang. */ let gesehen = null; for (let i = 0; i < 8 && !gesehen; i++) { await host.seite.evaluate(() => { for (let n = 0; n < 12; n++) document.getElementById("v-vor")?.click(); }); for (let t = 0; t < 14 && !gesehen; t++) { const w = await host.seite.evaluate(() => ({ spieler: document.getElementById("video-platz")?.dataset.spieler, })); if (w.spieler === "puffert") { /* SOFORT NACHSEHEN, was der Server jetzt weiss. Eine Sekunde spaeter ist der Pufferer vorbei und die Antwort nichts mehr wert. */ gesehen = await host.seite.evaluate(async () => { const a = await fetch("/workspace/api/reaktion", { credentials: "same-origin" }); const j = await a.json(); return { laeuft: j.laeuft, stand: document.getElementById("video-platz")?.dataset.spieler }; }); } await host.seite.waitForTimeout(120); } } if (!gesehen) { console.log(" HINWEIS: Ein Pufferer war hier nicht herzustellen " + "(schnelle Leitung) -- was der Host dabei meldet, konnte ich " + "nicht nachsehen."); offenePunkte.push("Was der Host beim Puffern meldet, war in diesem " + "Lauf nicht messbar -- der Sprung war zu schnell geladen."); } else { console.log(` Beim Puffern meldet der Server: laeuft=${gesehen.laeuft} ` + `(Spieler: ${gesehen.stand})`); if (!gesehen.laeuft) { console.log(" ACHTUNG: Der Host meldet beim Puffern 'laeuft nicht'. " + "Dann bekommt jeder Zuschauer bei jedem Stocken der Hostleitung " + "ein Pause-Play-Paar -- das ist das Ruckeln."); ausgang = 1; } } /* Und wieder anlaufen lassen, damit die folgenden Messungen ein laufendes Video vorfinden. */ await host.seite.evaluate(() => fetch("/workspace/api/reaktion/video", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ laeuft: true, sekunde: 7 }), })); await host.seite.waitForTimeout(1200); } } /* ==== 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