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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user