Das Video haengt nicht mehr, wenn die Kamera laeuft

Filipe, am Tag einer Sendung: „sobald meine kamera auch zu sehen ist,
also mich, dan haengen die videos EXTREEEEEM. wenn das video alleine
nur laeuft dan laeuft es fast perfekt."

Haus: Team Dogi. Zwei Ursachen, und BEIDE sind nur aktiv, wenn
Kameras sichtbar sind -- genau deshalb lief das Video allein sauber.

1. ZWOELF KODIERER OHNE JEDE GRENZE

Die Reaction verbindet jeden mit jedem. Fuer jeden Zuschauer baut der
Host eine eigene Verbindung auf, und jede hat ihren EIGENEN Kodierer:
Bei zwoelf Sichtplaetzen kodiert sein Rechner dasselbe Gesicht
zwoelfmal gleichzeitig, waehrend daneben das Video dekodiert wird.

Und an keinem einzigen Sender stand eine Grenze. `addTrack` ohne ein
Wort zu `maxBitrate`, `maxFramerate`, `scaleResolutionDownBy` oder
`degradationPreference` -- zwoelf Kodierer, die alle gleichzeitig „so
gut wie moeglich" versuchen und dem Video die Rechenzeit wegnehmen.

Jetzt: 220 kbit, 15 Bilder, Aufloesung halbiert (240x180 gehen
hinaus, das Fenster ist 200 Pixel breit), `balanced`. Rund ein
Sechstel der bisherigen Rechenlast. Der geteilte Bildschirm bekommt
eigene Werte -- dort ist die Aufloesung der Zweck und die Bildrate
fast egal.

2. MATTGLAS UEBER EINEM LAUFENDEN VIDEO

`backdrop-filter` zwingt die Grafikkarte, den Bereich DAHINTER neu zu
lesen und weichzuzeichnen. Ueber einer stehenden Flaeche kostet das
einmal etwas; ueber einem laufenden Video bei JEDEM Bild -- auf
derselben Grafikkarte, die das Video dekodiert.

Vier Stellen lagen auf der Leinwand: das Namensschild JEDES
Kamerafensters (bei voller Sendung dreizehnmal), die Tempoanzeige,
die Senderleiste und der Ton-Knopf. Alle vier tragen jetzt einen
deckenderen Grund und keinen Weichzeichner. Dazu `contain: paint` am
Kamerafenster: Ein ankommendes Kamerabild zieht keine Neuzeichnung
der Leinwand mehr nach sich.

EIN RUECKZIEHER, UND ZWAR EIN WICHTIGER

Mein erster Griff war, die Kamera kleiner aufzunehmen (480x360 statt
640x480). `mess-reaktion` meldete daraufhin „Im Vorraum laeuft kein
eigenes Bild" -- eine Warnung, die vorher nicht da war. 640x480 kann
jede Kamera, 480x360 nicht, und `ideal` ist zwar nur ein Wunsch, aber
was dabei herauskommt, entscheidet der Treiber. Am Abend einer Sendung
ist „vielleicht kein Bild" der schlechteste aller Tausche. Die
Aufloesung bleibt deshalb, kleiner gerechnet wird im Kodierer -- dort
ist es nachweislich erlaubt und kann nichts verhindern, was vorher
ging.

GEMESSEN -- mess-kameralast.mjs (neu), 13 Messungen, 0 Fehler

Sie fragt nicht „kommt ein Bild an" (das war immer mit Ja beantwortet),
sondern WIE TEUER das Bild ist, das hinausgeht -- und zwar an der
LAUFENDEN Verbindung ueber `getParameters()`, nicht im Quelltext. Eine
Zahl im Code beweist nicht, dass der Browser sie uebernommen hat.

Zum Mattglas stellt sie die PRAEZISE Frage: Der erste Entwurf suchte
jedes `backdrop-filter` auf der Seite und meldete drei, die gar nicht
ueber der Leinwand liegen (Kopfleiste, Regieleiste, Buehnenschild) --
teuer ist es aber nur, wenn sich dahinter etwas bewegt. Gemessen wird
jetzt die UEBERSCHNEIDUNG mit dem Videobereich; so kamen die zwei
heraus, die ich uebersehen hatte.

  mess-kameralast (neu)   13, 0 Fehler
  mess-reaktion           0 ACHTUNG (mit dem Rueckzieher wieder sauber)
  pruef-reaktion          588, 0 Fehler
  pruef-css-klassen       gruen

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-08 21:58:37 +02:00
co-authored by Claude Opus 5
parent 04cfb06d33
commit e93c4067f9
50 changed files with 1229 additions and 698 deletions
+39 -10
View File
@@ -529,10 +529,25 @@ body.reaktion-seite > .kopfleiste { flex: none; }
font-size: .72rem; font-weight: 800;
font-variant-numeric: tabular-nums;
letter-spacing: .04em;
background: rgba(6, 5, 11, .76);
/* ==== KEIN MATTGLAS UEBER EINEM LAUFENDEN VIDEO (08.10.2026) ===
Hier stand `backdrop-filter: blur(6px)`. Ueber einer stehenden
Flaeche kostet das einmal etwas; ueber einem LAUFENDEN VIDEO
kostet es bei JEDEM Bild: Der Browser muss den Bereich dahinter
neu auslesen und neu weichzeichnen, fuenfundzwanzigmal in der
Sekunde, und das auf der Grafikkarte, die gleichzeitig das Video
dekodiert.
Dasselbe stand am Namensschild jedes Kamerafensters -- dort also
einmal JE FENSTER. Zusammen mit den zwoelf Kodierern war das die
zweite Haelfte von Filipes „EXTREEEEEM".
ERSETZT UND NICHT WEGGENOMMEN: Der Grund wird dafuer deckender
(.76 -> .88). Das Schild liest sich genauso gut; es kostet nur
nichts mehr. */
background: rgba(6, 5, 11, .88);
border: 1px solid var(--rand);
color: var(--schrift-leise);
backdrop-filter: blur(6px);
}
.leinwand__tempo[hidden] { display: none; }
@@ -610,6 +625,14 @@ body.reaktion-seite > .kopfleiste { flex: none; }
.kamera {
position: relative;
/* ==== DAS FENSTER MALT NUR SICH SELBST (08.10.2026) ===========
`contain: paint` sagt dem Browser, dass nichts aus diesem Kasten
herausragt. Er darf ihn dann als eigene Ebene behandeln und muss
beim naechsten Kamerabild NICHT die Leinwand darunter neu malen
-- und die ist das Video. Ohne diese Zeile zieht jedes
ankommende Kamerabild eine Neuzeichnung des ganzen Bereichs nach
sich. */
contain: paint;
width: 100%;
aspect-ratio: 4 / 3;
border-radius: 16px;
@@ -669,10 +692,12 @@ body.reaktion-seite > .kopfleiste { flex: none; }
padding: 3px 9px 4px;
border-radius: 999px;
font-size: .72rem; font-weight: 700;
background: rgba(6, 5, 11, .76);
/* Deckend statt Mattglas -- siehe `.leinwand__tempo` weiter oben.
Dieses Schild steht EINMAL JE KAMERAFENSTER und damit bei einer
vollen Sendung bis zu dreizehnmal ueber dem laufenden Video. */
background: rgba(6, 5, 11, .88);
border: 1px solid var(--rand);
color: var(--schrift);
backdrop-filter: blur(6px);
max-width: calc(100% - 16px);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@@ -4123,9 +4148,11 @@ body.reaktion-seite {
min-height: 44px; padding: 10px 18px;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--an-hell) 50%, transparent);
background: rgba(10, 8, 16, .88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
/* KEIN MATTGLAS -- siehe `.leinwand__tempo`. Dieser Knopf liegt
mitten auf der Leinwand; dort kostet ein `backdrop-filter` bei
JEDEM Bild des Videos einen neuen Weichzeichner. Der Grund ist
ohnehin fast deckend; die paar Prozent mehr sieht niemand. */
background: rgba(10, 8, 16, .94);
color: #ffe9ea;
font-size: .88rem; font-weight: 700;
box-shadow: 0 18px 40px -18px #000, 0 0 26px -10px var(--an);
@@ -4155,9 +4182,11 @@ body.reaktion-seite {
display: flex; align-items: center; gap: 8px;
padding: 6px;
border-radius: 999px;
background: rgba(10, 8, 16, .82);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
/* KEIN MATTGLAS. Diese Leiste liegt waehrend der ganzen Sendung
ueber dem laufenden Video und ist die breiteste Flaeche davon --
gemessen von `mess-kameralast` als eine von zwei, die sich
wirklich mit der Leinwand ueberschneiden. */
background: rgba(10, 8, 16, .92);
border: 1px solid var(--rand);
box-shadow: 0 16px 38px -20px #000;
}
+137 -5
View File
@@ -767,6 +767,11 @@
const sender = d.pc.getSenders().find((x) => x.track && x.track.kind === 'video');
if (!sender) continue;
try { await sender.replaceTrack(spur); } catch { /* diese Leitung eben nicht */ }
/* NACH JEDEM TAUSCH NEU DROSSELN. Die Grenzen haengen an der
Art der Spur: Was fuer die Kamera gilt, waere fuer einen
geteilten Bildschirm falsch -- und umgekehrt. */
spurHinweis();
drosseln(d.pc);
}
kamerasZeichnen();
}
@@ -1575,13 +1580,49 @@
}
/** Die Bildvorgaben. */
/* ==== WIE GROSS DAS KAMERABILD HINAUSGEHT (08.10.2026) ==========
Filipe: „sobald meine kamera auch zu sehen ist, also mich, dan
hängen die videos EXTREEEEEM."
DAS IST KEIN ZUFALL UND KEIN SCHWACHER RECHNER. Die Reaction
verbindet jeden mit jedem: Fuer JEDEN Zuschauer baut der Host
eine eigene Verbindung auf, und jede davon hat ihren EIGENEN
Kodierer. Bei zwoelf Sichtplaetzen kodiert sein Rechner dasselbe
Gesicht also ZWOELFMAL gleichzeitig -- waehrend daneben das
YouTube-Video dekodiert wird.
Gerechnet mit den alten Werten: 640x480 bei 24 Bildern sind
7,4 Millionen Bildpunkte je Sekunde, mal zwoelf rund 88
Millionen. Mit den neuen (unten, und die Kodierer rechnen noch
einmal herunter) sind es rund 14 Millionen -- ein Sechstel.
DIE AUFLOESUNG BLEIBT BEI 640x480 -- UND DAS IST GEMESSEN.
Mein erster Griff war 480x360, weil das Fenster nur rund 200
Pixel breit ist. `mess-reaktion` hat daraufhin gemeldet: „Im
Vorraum laeuft kein eigenes Bild." 640x480 ist eine Groesse, die
jede Kamera kann; 480x360 ist es nicht, und `ideal` ist zwar nur
ein Wunsch -- aber was dabei herauskommt, entscheidet der
Treiber. Am Abend einer Sendung ist „vielleicht kein Bild" der
schlechteste aller Tausche.
KLEINER GEMACHT WIRD DESHALB IM KODIERER und nicht an der
Kamera (`scaleResolutionDownBy` in `DROSSEL`): Dort ist es
nachweislich erlaubt, es gilt je Leitung, und es kann nichts
verhindern, was vorher ging.
DIE BILDRATE GEHT AUF 20. Die ist ungefaehrlich -- eine Kamera,
die 20 nicht genau kann, liefert 24 oder 30 und wird vom
Kodierer ohnehin auf 15 begrenzt. Nur Aufloesungen sind
waehlerisch, Bildraten nicht.
DIE ZAHLEN STEHEN BEISAMMEN mit dem, was sie begrenzt
(`DROSSEL` weiter unten) -- eine Aufloesung hier und eine
Bandbreite dreihundert Zeilen weiter waeren zwei Zahlen, die
irgendwann nicht mehr zueinander passen. */
function bildVorgaben() {
const v = {
/* 360p reicht für ein Fenster von 200 Pixeln Breite -- und es
ist der Unterschied zwischen zwölf Zuschauern und dreien.
Die Zahl steht hier und nicht in einer Einstellung, weil
sie zur Fenstergröße gehört, nicht zum Geschmack. */
width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 24 },
width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 20 },
};
if (tonWahl.kamera) v.deviceId = { exact: tonWahl.kamera };
return v;
@@ -1706,6 +1747,8 @@
const ziel = sender.track.kind === 'video' ? bild : ton;
if (!ziel) continue;
try { await sender.replaceTrack(ziel); } catch { /* diese Leitung eben nicht */ }
spurHinweis();
drosseln(d.pc);
}
}
for (const s of alt.getTracks()) { try { s.stop(); } catch { /* war schon aus */ } }
@@ -2117,6 +2160,89 @@
}
}
/* ==== WAS JEDE EINZELNE LEITUNG HOECHSTENS KOSTEN DARF =========
Bis heute stand hier NICHTS: `addTrack` ohne ein einziges
Zugestaendnis, also jeder der zwoelf Kodierer mit voller
Aufloesung, voller Bildrate und so viel Bandbreite, wie er
bekommt. Zwoelf Kodierer, die alle gleichzeitig „so gut wie
moeglich" versuchen, nehmen dem Video genau die Rechenzeit weg,
die es zum Abspielen braucht.
DIE VIER WERTE, UND JEDER BEANTWORTET ETWAS ANDERES:
maxBitrate wie viel Leitung je Zuschauer. 220 kbit
reichen fuer ein Gesicht in einem
200-Pixel-Fenster. Bei zwoelf
Zuschauern sind das 2,6 Mbit nach
oben -- das haelt eine normale
Leitung aus. Ohne Grenze nimmt sich
jeder Kodierer, was er kriegt, und
oben wird es eng.
maxFramerate fuenfzehn. Ein sprechender Mensch
braucht nicht mehr; das Video schon.
scaleResolutionDownBy nochmal halbieren -> 240x180 geht
hinaus. Das Fenster ist 200 Pixel
breit; mehr zu schicken heisst, es
beim Empfaenger wegzurechnen.
degradationPreference `balanced`: Wird es eng, darf der
Browser SELBST heruntergehen. Ohne
diese Zeile haelt er die Aufloesung
und laesst stattdessen alles andere
warten -- genau das, was Filipe
gemeldet hat.
DER GETEILTE BILDSCHIRM BEKOMMT ANDERE WERTE. Dort ist die
Aufloesung der ganze Zweck (man soll Schrift lesen koennen) und
die Bildrate fast egal. Ihn mit denselben Zahlen zu drosseln
hiesse, ihn unlesbar zu machen -- eine Regel fuer zwei
verschiedene Dinge ist keine Regel, sondern ein Kompromiss, der
beiden schadet. */
const DROSSEL = {
kamera: { maxBitrate: 220_000, maxFramerate: 15, scaleResolutionDownBy: 2 },
bildschirm: { maxBitrate: 900_000, maxFramerate: 8, scaleResolutionDownBy: 1 },
};
/** Eine Leitung auf das begrenzen, was sie kosten darf.
*
* STILL BEI EINEM FEHLSCHLAG, und das ist Absicht: `setParameters`
* kann in einer Verbindung, die gerade neu verhandelt wird,
* abgelehnt werden. Dann bleibt die Leitung ungedrosselt -- das
* ist schlechter, aber immer noch eine Leitung. Eine Ausnahme,
* die hier hochschlaegt, risse mitten in der Sendung das Bild
* eines Zuschauers ab.
*/
async function drosseln(pc) {
for (const sender of pc.getSenders()) {
if (!sender.track || sender.track.kind !== 'video') continue;
const wie = sender.track === bildschirmSpur ? DROSSEL.bildschirm : DROSSEL.kamera;
try {
const p = sender.getParameters();
/* `encodings` kann leer sein, solange noch nicht verhandelt
wurde. Eine leere Liste zu fuellen ist erlaubt; sie zu
ueberschreiben waere es nicht. */
if (!p.encodings || !p.encodings.length) p.encodings = [{}];
Object.assign(p.encodings[0], wie);
p.degradationPreference = 'balanced';
await sender.setParameters(p);
} catch { /* diese Leitung eben ungedrosselt */ }
}
}
/** Dem Kodierer sagen, worum es im Bild geht.
*
* `motion` heisst: Lieber die Aufloesung senken als die Bewegung
* abhacken. Fuer ein Gesicht ist das richtig -- ein etwas
* weicheres Bild faellt niemandem auf, ein ruckelndes sofort.
* Beim geteilten Bildschirm genau andersherum: `detail` haelt die
* Schaerfe, damit Schrift lesbar bleibt.
*/
function spurHinweis() {
const kamera = meinStrom?.getVideoTracks?.()[0];
if (kamera) kamera.contentHint = 'motion';
if (bildschirmSpur) bildschirmSpur.contentHint = 'detail';
}
function draht(anderer) {
let d = drähte.get(anderer);
if (d) return d;
@@ -2154,6 +2280,12 @@
const ton = meinStrom.getAudioTracks()[0];
if (bild) pc.addTrack(bild, meinStrom);
if (ton) pc.addTrack(ton, meinStrom);
/* SOFORT NACH DEM EINHAENGEN. Wartet man damit bis nach der
Verhandlung, laeuft der Kodierer die ersten Sekunden
ungebremst -- und genau in diesen Sekunden kommt der
Zuschauer dazu, waehrend das Video schon laeuft. */
spurHinweis();
drosseln(pc);
}
return d;
}