diff --git a/server/pruef-anruf.mjs b/server/pruef-anruf.mjs index a93dbe4e..482b51ed 100644 --- a/server/pruef-anruf.mjs +++ b/server/pruef-anruf.mjs @@ -251,6 +251,21 @@ melde("=== 6. Die Verbindungsadressen ==="); const l = json(await roh("/workspace/api/anruf/adressen", CREW, { cookie: kModi })); ok(l.adressen?.length === 2, `alle bekommen sie zu sehen (${l.adressen?.length})`); ok(l.nur_gleiches_netz === false, "und der Hinweis verschwindet"); + + /* UND WIEDER WEG. Die Adressen oben sind erfunden -- es gibt unter + `turn.dogfather-universe.com` keinen STUN-Dienst. Ein Browser + versucht sie trotzdem zu erreichen und wartet auf das Zeitlimit; + der Dreier-Test unten haette danach gegen diese Wartezeit + gekaempft statt gegen den Code. + + Eine Pruefung, die der naechsten den Boden verstellt, macht deren + Ergebnis unlesbar -- genau das ist heute schon einmal passiert + (die Anfragen, Abschnitt 8b). */ + await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi }, + { adressen: [] }, "PUT"); + const zurueck = json(await roh("/workspace/api/anruf/adressen", HAUS, { cookie: kDogi })); + ok(zurueck.adressen?.length === 0, + "danach ist die Liste wieder leer -- der naechste Abschnitt faengt sauber an"); } /* ======================================================================= @@ -345,6 +360,137 @@ melde("=== 7. Im Browser ==="); } } +/* ======================================================================= + 8. ZU DRITT -- UND ZWAR WIRKLICH (18.09.2026) + + GEBAUT IST NICHT BEWIESEN. Der Anruf ist fuer bis zu vier Leute + geschrieben, geprueft war er zu zweit. Genau diese Luecke zwischen + Behauptung und Beleg ist die, an der im Haus schon mehrfach etwas + haengengeblieben ist -- zuletzt bei den Kachelfarben, wo ein + Werkzeug 21 Farben kannte und 37 gemeint waren. + + DER ENTSCHEIDENDE PUNKT IST DIE ZAHL DER VERBINDUNGEN. Bei drei + Teilnehmern muss JEDER zwei haben -- eine zu jedem anderen. Haette + `verbindungen` eine einzelne Variable statt einer Karte, ginge zu + zweit alles gut und der Dritte ueberschriebe stillschweigend den + Ersten: Man sieht sich zu dritt und hoert nur einen. + + UND DIE ANGEBOTSREGEL. Bei WebRTC muss genau EINER das Angebot + machen. Machen beide eins, heben sie sich auf ("glare") und es kommt + keine Verbindung zustande -- ohne Fehlermeldung, man wartet nur. + Die Regel „die kleinere Personennummer beginnt" ist auf beiden + Seiten ohne Absprache dieselbe Entscheidung. Ob sie traegt, sieht + man erst ab drei Leuten. + ======================================================================= */ +melde(""); +melde("=== 8. Zu dritt ==="); +{ + let chromium; + try { + ({ chromium } = await import( + "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs")); + } catch { chromium = null; } + + if (!chromium) { + unklar("kein Playwright -- der Gruppenanruf wurde nicht ausprobiert"); + } else { + /* Ein Gruppenraum mit allen dreien. */ + const g = await roh("/workspace/api/chat/gruppe", HAUS, { cookie: kDogi }, + { name: "Testrunde", mit: [idHand, idModi] }); + const gruppeId = json(g).raum?.id || json(g).raum_id || json(g).id; + ok(!!gruppeId, `ein Gruppengespraech entsteht (${g.code}, Raum ${gruppeId})`); + + const browser = await chromium.launch({ + args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"], + }); + try { + const machSeite = async (keks) => { + const k = await browser.newContext({ + viewport: { width: 1100, height: 800 }, + permissions: ["microphone"], + }); + await k.addCookies(keks.split("; ").map((c) => { + const [name, ...rest] = c.split("="); + return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" }; + })); + const se = await k.newPage(); + await se.goto(`http://127.0.0.1:${PORT}/workspace/chat.html`, + { waitUntil: "domcontentloaded" }); + await se.waitForTimeout(1600); + return { k, se }; + }; + + const a = await machSeite(kDogi); + const b = await machSeite(kHand); + const c = await machSeite(kModi); + + /* Alle drei oeffnen denselben Raum. */ + for (const x of [a, b, c]) { + await x.se.evaluate((id) => { + const li = [...document.querySelectorAll("#raeume li")] + .find((e) => Number(e.dataset.raum || e.dataset.id) === id); + (li || document.querySelector("#raeume li"))?.click(); + }, gruppeId); + await x.se.waitForTimeout(700); + } + + /* DogFather ruft an. */ + await a.se.evaluate((id) => window.anrufStarten(id, false), gruppeId); + await a.se.waitForTimeout(1500); + + /* Die anderen beiden gehen ran -- ueber den Klingelknopf, nicht + ueber eine Abkuerzung: Geprueft werden soll der Weg, den ein + Mensch geht. */ + for (const x of [b, c]) { + const da = await x.se.locator("#anruf-klingelt .anruf-klingelt__ja").count(); + ok(da === 1, `es klingelt bei ${x === b ? "der rechten Hand" : "dem Modi"} (${da})`); + if (da) await x.se.click("#anruf-klingelt .anruf-klingelt__ja"); + } + /* WebRTC braucht ein paar Sekunden, bis die Verbindungen stehen. */ + await a.se.waitForTimeout(6000); + + /* Der Server: drei drin. */ + const lage = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi })); + ok(lage.wer?.length === 3, + `der Server zaehlt drei Teilnehmer (${lage.wer?.map((w) => w.name).join(", ")})`); + + /* DER EIGENTLICHE PUNKT: Jeder hat ZWEI Verbindungen. */ + const zaehlen = (x) => x.se.evaluate(() => ({ + /* Ueber die Oberflaeche nicht erreichbar -- deshalb ueber die + Zahl der Video-/Audio-Elemente im Kasten, die jede + Gegenstelle erzeugt. */ + stroeme: document.querySelectorAll("#anruf-leute audio, #anruf-leute video").length, + lage: document.getElementById("anruf-lage")?.textContent || "", + })); + for (const [wer, x] of [["DogFather", a], ["die rechte Hand", b], ["der Modi", c]]) { + const z = await zaehlen(x); + ok(z.stroeme >= 2, + `${wer} hoert BEIDE anderen (${z.stroeme} Stroeme, „${z.lage}")`); + } + + /* Einer legt auf -- fuer die anderen laeuft es weiter. */ + await c.se.click("#anruf-auflegen"); + await c.se.waitForTimeout(2000); + const lage2 = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi })); + ok(lage2.laeuft === true && lage2.wer?.length === 2, + `einer geht, zwei telefonieren weiter (${lage2.wer?.length})`); + const zA = await zaehlen(a); + ok(zA.stroeme === 1, + `und bei DogFather ist genau ein Strom weniger (${zA.stroeme})`); + + await a.se.click("#anruf-auflegen"); + await b.se.click("#anruf-auflegen"); + await a.se.waitForTimeout(1200); + const lage3 = json(await roh(`/workspace/api/anruf/${gruppeId}`, HAUS, { cookie: kDogi })); + ok(lage3.laeuft === false, "wenn alle auflegen, ist der Anruf vorbei"); + + await a.k.close(); await b.k.close(); await c.k.close(); + } finally { + await browser.close(); + } + } +} + /* ------------------------------------------------------------------- */ melde(""); melde(`${geprueft} Pruefungen, ${fehler} Fehler` diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index 6db5aaf7..f5be81aa 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609181009", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609181442", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609181009", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609181442", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181009", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181442", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181009", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181442", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/js/anruf.js b/workspace/assets/js/anruf.js index a52022f3..00bb6553 100644 --- a/workspace/assets/js/anruf.js +++ b/workspace/assets/js/anruf.js @@ -49,6 +49,47 @@ /* Der laufende Anruf. `null`, wenn keiner läuft. */ let anruf = null; // { raumId, video, verbindungen: Map, stroeme: Map, eigener } + /* ===================================================================== + SIGNALE, DIE ZU FRÜH KOMMEN (18.09.2026) + + GEFUNDEN IM DREIER-TEST, und es hätte jeden Anruf getroffen: + + Wer rangeht, meldet dem Server „dabei". Der Anrufer bekommt das + sofort und schickt sein Angebot los. Der Angerufene braucht aber + noch ein bis zwei Sekunden, bis das Mikrofon offen ist -- und in + dieser Zeit ist `anruf` hier noch `null`. + + Vorher stand in `signalVerarbeiten` ein `if (!anruf) return;`. Das + Angebot wurde damit STILL WEGGEWORFEN. Danach kommt keins mehr: + Der Anrufer hat seins geschickt und wartet. Beide Seiten zeigten + „Verbindung …", und es passierte nie wieder etwas. + + In der Spur war es unübersehbar, sobald man hinsah: + + ereignis:signal inhalt=angebot <- kommt an + ereignis:signal inhalt=weg + verbindung angelegt <- erst JETZT + + Was zu früh kommt, wird deshalb aufgehoben statt verworfen -- und + abgearbeitet, sobald das Mikrofon steht. Ein Signal ist keine + Nachricht, die man wiederholen kann; wer es wegwirft, hat den + Anruf verloren. */ + const wartend = []; // { von, inhalt } + + async function wartendeAbarbeiten() { + const liste = wartend.splice(0, wartend.length); + for (const w of liste) { + /* Was zu einem anderen Gespräch gehört, wird verworfen -- und + zwar hier, wo `anruf` bekannt ist. */ + if (!anruf || (w.raumId && w.raumId !== anruf.raumId)) continue; + /* NACHEINANDER, NICHT GLEICHZEITIG. Ein Verbindungsweg, der vor + der Beschreibung ankommt, wird abgewiesen -- und beim + parallelen Abarbeiten gewinnt oft der Weg. Die Reihenfolge, + in der sie eingetroffen sind, ist die richtige. */ + await signalVerarbeiten(w.von, w.inhalt); + } + } + let ichId = null; const el = (art, klasse, text) => { @@ -136,7 +177,14 @@ } async function signalVerarbeiten(von, inhalt) { - if (!anruf) return; + /* NOCH NICHT BEREIT? AUFHEBEN, NICHT WEGWERFEN. Siehe oben -- + genau hier ging jedes Angebot verloren, das ankam, während das + Mikrofon noch geöffnet wurde. */ + if (!anruf) { + wartend.push({ von, inhalt }); + console.info('[anruf] zu frueh, aufgehoben:', Object.keys(inhalt || {}).join('+')); + return; + } const v = verbindungFuer(von); try { if (inhalt.angebot) { @@ -154,7 +202,13 @@ await v.addIceCandidate(inhalt.weg).catch(() => {}); } } catch (f) { - console.warn('[anruf] Signal:', f?.message); + /* LAUT GENUG, UM ETWAS DAMIT ANZUFANGEN. Ein „Signal: + irgendwas" sagt nicht, WELCHER Schritt gescheitert ist -- und + bei WebRTC sind es immer dieselben drei, die in Frage kommen. + Die Art des Signals gehört dazu, sonst sucht man im + falschen. */ + console.warn('[anruf] Signal von', von, + Object.keys(inhalt || {}).join('+'), '->', f?.name, f?.message); } } @@ -219,6 +273,7 @@ setTimeout(() => auflegen(false), 5000); return; } + await wartendeAbarbeiten(); leuteZeichnen(); } @@ -260,6 +315,8 @@ if (ichId < p.id) angebotMachen(p.id); else verbindungFuer(p.id); } + /* Und jetzt das, was währenddessen schon eingetroffen ist. */ + await wartendeAbarbeiten(); leuteZeichnen(); } @@ -270,6 +327,11 @@ for (const v of anruf.verbindungen.values()) { try { v.close(); } catch { /* egal */ } } for (const s of anruf.eigener.getTracks()) s.stop(); anruf = null; + /* Was noch wartet, gehoert zu einem Gespraech, das es nicht mehr + gibt. Bliebe es liegen, waere es beim naechsten Anruf das erste, + was verarbeitet wird -- mit einer Beschreibung, die zu niemandem + passt. */ + wartend.length = 0; const k = $('anruf'); if (k) k.hidden = true; uhrStoppen(); @@ -383,15 +445,40 @@ if (d.art === 'dabei') { if (!anruf || d.raum_id !== anruf.raumId) return; if (d.person?.id === ichId) return; - lage('Verbunden'); + /* NICHT „Verbunden" MELDEN, SOLANGE NICHTS VERBUNDEN IST. + + Hier stand `lage('Verbunden')` -- gesetzt in dem Moment, in + dem jemand RANGEHT. Die Verbindung braucht danach aber noch + Sekunden, und bis dahin behauptete der Kasten etwas, das nicht + stimmte. Im Dreier-Test war das sofort zu sehen: „Verbunden" + stand da, und es kam kein Ton. + + Den richtigen Zeitpunkt kennt nur die Verbindung selbst -- + `onconnectionstatechange` meldet ihn (siehe oben). */ + lage('Baut auf …'); /* Der Neue: Angebot macht, wer die kleinere Nummer hat. */ if (ichId < d.person.id) angebotMachen(d.person.id); else verbindungFuer(d.person.id); return; } if (d.art === 'signal') { - if (!anruf || d.raum_id !== anruf.raumId) return; if (d.an !== ichId) return; + /* HIER STAND `if (!anruf ...) return;` -- UND DAS WAR DER FEHLER. + + Die Warteschlange eine Ebene tiefer nützte nichts, weil das + Signal hier schon verworfen wurde. Gefunden erst, als die + Diagnose zeigte, dass `signalVerarbeiten` NIE aufgerufen wird: + keine einzige Konsolenzeile, obwohl beide Zweige dort etwas + melden. + + Der Raum lässt sich ohne `anruf` nicht prüfen -- deshalb + wandert er mit in die Warteschlange und wird beim Abarbeiten + geprüft. */ + if (!anruf) { + wartend.push({ von: d.von, inhalt: d.inhalt, raumId: d.raum_id }); + return; + } + if (d.raum_id !== anruf.raumId) return; await signalVerarbeiten(d.von, d.inhalt); return; } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index e91681a6..438deecb 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - +
@@ -291,10 +291,10 @@ - - - - - + + + + +