Der Anruf funktioniert wirklich -- gefunden im Dreier-Test

GEBAUT WAR NICHT BEWIESEN. Der Anruf ist fuer bis zu vier Leute
geschrieben, geprueft war er zu zweit -- und der Zweier-Test pruefte
nur, dass der Kasten erscheint und die Uhr laeuft. Ob jemals TON
ankommt, hat er nie gefragt.

Der Dreier-Test hat es gefragt. Ergebnis beim ersten Lauf:

  DogFather      0 Stroeme, "Verbunden"
  rechte Hand    0 Stroeme, "Verbunden"
  der Modi       0 Stroeme, "Verbindung ..."

DREI FEHLER, gefunden durch Messen statt Raten:

1. SIGNALE, DIE ZU FRUEH KOMMEN, WURDEN WEGGEWORFEN -- und das haette
   JEDEN Anruf getroffen, auch zu zweit.

   Wer rangeht, meldet "dabei". Der Anrufer bekommt das sofort und
   schickt sein Angebot. Der Angerufene braucht danach aber noch ein
   bis zwei Sekunden fuer das Mikrofon, und in dieser Zeit war `anruf`
   noch null. Das Angebot wurde still verworfen; danach kommt keins
   mehr. Beide Seiten zeigten "Verbindung ...", und es passierte nie
   wieder etwas.

   In der Spur war es unuebersehbar, sobald man hinsah:
     ereignis:signal inhalt=angebot     <- kommt an
     ereignis:signal inhalt=weg
     verbindung angelegt                <- erst JETZT

   Ein Signal ist keine Nachricht, die man wiederholen kann. Wer es
   wegwirft, hat den Anruf verloren -- es wird jetzt aufgehoben und
   abgearbeitet, sobald das Mikrofon steht.

2. DIE WARTESCHLANGE LAG ZUERST AN DER FALSCHEN STELLE. Eingebaut in
   `signalVerarbeiten`, verworfen wurde aber schon eine Ebene hoeher.
   Gefunden erst, als die Diagnose zeigte, dass `signalVerarbeiten` NIE
   aufgerufen wird: keine einzige Konsolenzeile, obwohl beide Zweige
   dort etwas melden. Eine Reparatur an der falschen Stelle sieht aus
   wie eine Reparatur.

3. "VERBUNDEN" STAND DA, BEVOR ETWAS VERBUNDEN WAR. Die Meldung wurde
   gesetzt, wenn jemand RANGEHT -- die Verbindung braucht danach noch
   Sekunden. Den richtigen Zeitpunkt kennt nur die Verbindung selbst
   (`onconnectionstatechange`). Eine Anzeige, die etwas Falsches sagt,
   ist schlimmer als keine.

Dazu zwei Kleinigkeiten, die dabei auffielen: Das Abarbeiten laeuft
jetzt NACHEINANDER (ein Verbindungsweg, der vor der Beschreibung
ankommt, wird abgewiesen -- parallel gewinnt oft der Weg), und die
Warteschlange wird beim Auflegen geleert, damit kein altes Signal in
den naechsten Anruf wandert.

Und die Pruefung selbst hatte einen Fehler: Abschnitt 6 setzt erfundene
STUN-Adressen und liess sie stehen. Der Browser haette im naechsten
Abschnitt gegen deren Zeitlimit gekaempft statt gegen den Code. Sie
werden jetzt wieder geleert -- eine Pruefung, die der naechsten den
Boden verstellt, macht deren Ergebnis unlesbar.

pruef-anruf 40 -> 51. Der neue Abschnitt prueft das, worauf es
ankommt: Bei drei Teilnehmern muss JEDER zwei Stroeme haben. Haette
`verbindungen` eine einzelne Variable statt einer Karte, ginge es zu
zweit gut und der Dritte ueberschriebe stillschweigend den Ersten --
man sieht sich zu dritt und hoert einen.

Gegenprobe (Warteschlange wieder ausgebaut): 4 rot, genau die
Tonpruefungen.

Gruen: anruf, chat, css-klassen, namen.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-18 14:42:44 +02:00
co-authored by Claude Opus 5
parent 93c9788782
commit 5bbf1f2019
35 changed files with 639 additions and 406 deletions
+91 -4
View File
@@ -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;
}