Kamerafenster: Groesse und Ecke lassen sich stellen

Filipe: "und danach perfektionierst du auch die verschiedenen
groessen von kamera und so, perfektionier das alles bitte."

Sechs Groessen (0,6x bis 2x) und vier Ecken, beide an der SENDUNG
und nicht am Browser: Was Filipe einstellt, sehen alle. Waere es
eine Einstellung je Geraet, redete er ueber ein Bild, das bei den
Zusehenden anders aussieht -- und im Stream stuende ein drittes.

Anordnung, Groesse und Ecke gehen jetzt EINEN Weg (/layout), weil
sie zusammen eine einzige Frage beantworten: Wie sieht das Bild aus?
Drei getrennte Aufrufe waeren drei Rundrufe, und dazwischen saehen
die Zusehenden eine Mischung -- neue Anordnung, alte Ecke. Eine
falsche Angabe laesst auch das Gute stehen, statt halb umzustellen.

Die Ecke gibt es, weil unten rechts bei TikTok die Knopfreihe liegt,
bei YouTube die Fortschrittsleiste, und Untertitel fast immer unten
stehen. Eine feste Ecke ist eine, die bei jeder zweiten Plattform
im Weg ist.

Die OBS-Videoquelle kennt jetzt die Anordnung und blendet sich bei
"Nur Kamera" aus -- sonst laege im Stream ein stehengebliebenes
YouTube-Bild unter den Kameras, und Filipe saehe es nicht, weil er
auf seine Szene schaut und nicht auf die Quelle.

VIER BEFUNDE, ALLE VON DEN PRUEFUNGEN UND KEINER VOM AUGE:

1. Die Knoepfe fuer Groesse und Ecke gab es gar nicht. HTML und
   Stilblatt waren da, das Programm nicht. Gemessen: "0 von 0
   Eckknoepfen gesperrt", und der Hinweistext stand noch wortgleich
   wie im HTML -- zwei leere Kaesten, die aussahen, als sei alles
   in Ordnung.

2. Bei "Nur Kamera" war der Stapel 493 px breit statt 1072. Der
   neue Deckel max-width: 46% -- richtig gegen ein zu grosses
   Fenster bei "Video gross" -- galt still auch dort, wo die
   Kameras die ganze Flaeche fuellen sollen.

3. Am Handy haette die Groesseneinstellung ueberhaupt nichts
   bewirkt: Dort stand weiterhin width: clamp(88px, 26vw, 140px)
   am Fenster selbst. Das ist die dritte Wiederholung derselben
   Sache an einem Tag (die Saalzeilen, die Tafeln, jetzt die
   Kamerabreite): Zwei Regeln fuer dieselbe Frage sind die
   Garantie, dass eine davon irgendwo falsch gewinnt. Breite und
   Rand gehen jetzt ueber --kam-grund/--kam-rand, --kam wird an
   genau EINER Stelle gerechnet, und eine Pruefung zaehlt das nach.

4. Die Pruefung schlug auf ihren EIGENEN Kommentar an, der die
   entfernte Zeile zitiert. Ein Werkzeug, das bei jedem Lauf
   meckert, wird nach dem zweiten Mal weggeklickt -- samt dem
   echten Befund darin. Gezaehlt werden jetzt Regeln, nicht Prosa.

Gemessen beim Zuschauer und nicht in der Datenbank: 1x -> 208 px,
2x -> 416 px, alle Fenster innerhalb der Leinwand, und jede der
vier Ecken am Abstand zu den Kanten nachgewiesen statt an ihrem
eigenen Namen. Ein Knopf, der gerade nichts bewirken kann, ist
grau, und der Satz darunter sagt warum.

pruef-reaktion 320/0 - pruef-buehne 36/0 - pruef-css-klassen ok -
pruef-tippziele 11/0 - mess-reaktion ohne Beanstandung

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-28 14:35:02 +02:00
co-authored by Claude Opus 5
parent 2b02d88767
commit 3fedeea716
55 changed files with 1537 additions and 693 deletions
+167
View File
@@ -685,6 +685,173 @@ if (zuschauer.konsole.length) console.log(" KONSOLE:", zuschauer.konsole.slice
Abzeichen UND ein zweiter Knopf dazugekommen. Genau so ist am
28.09. die Regie 197 px uebergelaufen: Die Rechnung ging auf, die
Wirklichkeit nicht. */
/* ---- Groesse und Ecke der Kamerafenster -------------------------
Filipe: „und danach perfektionierst du auch die verschiedenen
groessen von kamera und so."
GEMESSEN WIRD BEIM ZUSCHAUER. Dass in der Datenbank 1,5 steht,
sagt nichts darueber, ob das Fenster bei den anderen groesser
wird -- dazwischen liegen ein Rundruf, eine Variable im Stilblatt
und eine Rechnung mit `clamp`. */
console.log("");
console.log(" --- Kameragroesse und Ecke ---");
{
/* Sauberer Anfang: „Video gross", damit die Ecke ueberhaupt gilt. */
await roh("/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { layout: "kino" },
});
await zuschauer.seite.waitForTimeout(900);
const messen = () => zuschauer.seite.evaluate(() => {
const kino = document.getElementById("teil-live");
const kk = document.querySelector(".kameras");
const ein = document.querySelector(".kamera");
const lein = document.querySelector(".leinwand");
if (!kk || !ein || !lein) return null;
const r = ein.getBoundingClientRect();
const lr = lein.getBoundingClientRect();
const fenster = [...document.querySelectorAll(".kamera")];
return {
gross: kino.style.getPropertyValue("--kam-gross") || "(?)",
ecke: kino.dataset.ecke || "(?)",
breit: Math.round(r.width),
/* Wo sitzt der Stapel? Als Abstand zu den vier Kanten der
Leinwand -- daran erkennt man die Ecke ohne Vorwissen. */
links: Math.round(kk.getBoundingClientRect().left - lr.left),
rechts: Math.round(lr.right - kk.getBoundingClientRect().right),
oben: Math.round(kk.getBoundingClientRect().top - lr.top),
unten: Math.round(lr.bottom - kk.getBoundingClientRect().bottom),
/* Und liegt noch alles IN der Leinwand? Sie hat
`overflow: hidden` -- was daneben liegt, ist unsichtbar,
nicht abgeschnitten sichtbar. */
drin: fenster.filter((f) => {
const q = f.getBoundingClientRect();
return q.left >= lr.left - 1 && q.right <= lr.right + 1
&& q.top >= lr.top - 1 && q.bottom <= lr.bottom + 1;
}).length,
fenster: fenster.length,
leinwand: Math.round(lr.width),
};
});
const normal = await messen();
console.log(` 1×: Fenster ${normal?.breit} px, Ecke ${normal?.ecke}, `
+ `Abstände l/r/o/u ${normal?.links}/${normal?.rechts}/${normal?.oben}/${normal?.unten}, `
+ `${normal?.drin}/${normal?.fenster} im Bild`);
/* ---- DIE GROESSE MUSS WIRKEN ---- */
const { KAMERA_GROESSEN } = await import("./reaktion-tabellen.js");
const gross = KAMERA_GROESSEN[KAMERA_GROESSEN.length - 1];
await roh("/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_gross: gross },
});
await zuschauer.seite.waitForTimeout(900);
const dick = await messen();
console.log(` ${gross}×: Fenster ${dick?.breit} px, `
+ `${dick?.drin}/${dick?.fenster} im Bild (Leinwand ${dick?.leinwand} px)`);
if (!normal || !dick) {
console.log(" ACHTUNG: Die Kamerafenster liessen sich nicht messen.");
ausgang = 1;
} else {
/* NICHT AUF DEN FAKTOR GENAU. `clamp` deckelt die Grundbreite,
und `max-width: 46%` deckelt den Stapel -- beides ist Absicht.
Geprueft wird, dass es SPUERBAR waechst und im Bild bleibt. */
const wuchs = dick.breit / normal.breit;
console.log(` Das Fenster wuchs um das ${wuchs.toFixed(2)}-fache`);
if (wuchs < 1.3) {
console.log(" ACHTUNG: Die Groesse wirkt nicht.");
ausgang = 1;
}
if (dick.drin !== dick.fenster) {
console.log(" ACHTUNG: Ein Fenster liegt ausserhalb der Leinwand.");
ausgang = 1;
}
if (dick.breit > dick.leinwand * 0.5) {
console.log(" ACHTUNG: Ein Fenster deckt mehr als die halbe Leinwand zu.");
ausgang = 1;
}
}
/* ---- DIE VIER ECKEN ----
Geprueft wird der ABSTAND zu den Kanten, nicht der Name der
Ecke. Eine Einstellung, die nur ihren eigenen Namen bestaetigt,
bestaetigt nichts. */
await roh("/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_gross: 1 },
});
const erwartet = {
ol: ["links", "oben"], or: ["rechts", "oben"],
ul: ["links", "unten"], ur: ["rechts", "unten"],
};
for (const [ecke, [waag, senk]] of Object.entries(erwartet)) {
await roh("/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_ecke: ecke },
});
await zuschauer.seite.waitForTimeout(800);
const m = await messen();
const nahWaag = m[waag] < (waag === "links" ? m.rechts : m.links);
const nahSenk = m[senk] < (senk === "oben" ? m.unten : m.oben);
console.log(` Ecke ${ecke}: l/r/o/u ${m.links}/${m.rechts}/${m.oben}/${m.unten} `
+ `-> ${waag}+${senk} ${nahWaag && nahSenk ? "stimmt" : "STIMMT NICHT"}`);
if (!nahWaag || !nahSenk || m.ecke !== ecke) {
console.log(` ACHTUNG: Die Ecke „${ecke}" kommt nicht an.`);
ausgang = 1;
}
}
await bild(zuschauer.seite, "22-kamera-ecke");
/* ---- EIN KNOPF, DER NICHTS TUN KANN, IST GRAU ----
Die Ecke wirkt nur bei „Video gross"; die Groesse wirkt ueberall,
wo es Kamerafenster GIBT -- bei „Nur Video" gibt es keine.
GEMESSEN WIRD DER ZUSTAND DER KNOEPFE, nicht der Satz darunter.
Ein Hinweis, der die Wahrheit sagt, waehrend der Knopf daneben
bedienbar bleibt, ist die schlechtere Haelfte: Man liest ihn
nicht und drueckt trotzdem. */
const knoepfe = () => host.seite.evaluate(() => {
const e = [...document.querySelectorAll("#kamecke .kamecke__knopf")];
const g = [...document.querySelectorAll("#kamgroesse .kamgroesse__knopf")];
return {
ecken: e.length, eckenGrau: e.filter((x) => x.disabled).length,
groessen: g.length, groessenGrau: g.filter((x) => x.disabled).length,
hinweis: document.getElementById("kamecke-hinweis")?.textContent || "",
};
});
for (const [anordnung, sollEcken, sollGroessen, wort] of [
["kamera", "alle", "keine", "Mitte"],
["nur_video", "alle", "alle", "Nur Video"],
["kino", "keine", "keine", "Ecke"],
]) {
await roh("/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { layout: anordnung },
});
await host.seite.waitForTimeout(800);
const m = await knoepfe();
console.log(` „${anordnung}": ${m.eckenGrau}/${m.ecken} Ecken grau, `
+ `${m.groessenGrau}/${m.groessen} Größen grau`);
console.log(` Hinweis: „${m.hinweis.replace(/\s+/g, " ").trim()}"`);
const soll = (art, wieviel, da) => (wieviel === "alle" ? art === da : art === 0);
if (m.ecken !== 4 || m.groessen < 4) {
console.log(" ACHTUNG: Die Knoepfe fuer Groesse oder Ecke fehlen.");
ausgang = 1;
} else if (!soll(m.eckenGrau, sollEcken, m.ecken)
|| !soll(m.groessenGrau, sollGroessen, m.groessen)) {
console.log(` ACHTUNG: Bei „${anordnung}" stimmt das Grauwerden nicht.`);
ausgang = 1;
} else if (!m.hinweis.includes(wort)) {
console.log(` ACHTUNG: Der Hinweis sagt nichts von „${wort}".`);
ausgang = 1;
}
}
await roh("/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { layout: "kino", kamera_ecke: "ur" },
});
await host.seite.waitForTimeout(700);
}
/* ---- Die Regieleiste ueber dem Saal -----------------------------
Filipe, 28.09.2026: „ich haette das gerne oben also nicht in dieser
+4 -1
View File
@@ -172,7 +172,10 @@ melde("=== 2. Der Schluessel ===");
wird von Hand gepflegt UND ist absichtlich abschliessend: Sie ist
die einzige Stelle, an der auffaellt, wenn jemand dem Stand ein
Feld hinzufuegt, das ohne Anmeldung abrufbar waere. */
const erlaubt = ["gesendet", "laeuft", "sekunde", "stand", "tempo",
/* `layout` kam am 28.09. dazu: Bei „nur Kamera" muss die Quelle
das Bild abschalten, sonst liegt im Stream ein
stehengebliebenes YouTube-Bild unter den Kameras. */
const erlaubt = ["gesendet", "laeuft", "layout", "sekunde", "stand", "tempo",
"titel", "video", "video_titel"];
ok(JSON.stringify(felder) === JSON.stringify(erlaubt),
`die Auskunft hat genau die erlaubten Felder (${felder.join(", ")})`);
+216
View File
@@ -1652,6 +1652,222 @@ melde("=== 18. Die Regieleiste ===");
"nur wer die Sendung fuehrt, sieht sie");
}
/* =======================================================================
19. GROESSE UND ECKE DER KAMERAFENSTER
Filipe, 28.09.2026: „und danach perfektionierst du auch die
verschiedenen groessen von kamera und so."
Die Anordnung sagt das VERHAELTNIS von Video und Kameras. Was
fehlte, sind die zwei Dinge daneben: wie gross genau, und wo.
======================================================================= */
melde("");
melde("=== 19. Groesse und Ecke der Kamerafenster ===");
{
const { KAMERA_GROESSEN, KAMERA_ECKEN } = await import("./reaktion-tabellen.js");
/* ---- DIE LISTEN SELBST ----
Sechs Stufen statt eines Schiebers: Ein Schieber verspricht eine
stufenlose Einstellung, und dann sucht mitten in der Sendung
jemand nach „dem richtigen Wert", statt zu senden. */
ok(KAMERA_GROESSEN.length >= 4 && KAMERA_GROESSEN.includes(1),
`es gibt ${KAMERA_GROESSEN.length} Groessen: ${KAMERA_GROESSEN.join(", ")}`);
ok(KAMERA_GROESSEN.every((g) => g >= 0.5 && g <= 2),
"und alle liegen zwischen halb und doppelt");
/* Aufsteigend, weil die Knoepfe in dieser Reihenfolge stehen. Eine
Reihe, in der 1,5 vor 0,8 steht, sieht aus wie ein Fehler und
ist keiner -- man sucht trotzdem danach. */
ok(KAMERA_GROESSEN.every((g, i) => i === 0 || g > KAMERA_GROESSEN[i - 1]),
"die Groessen stehen aufsteigend");
ok(KAMERA_ECKEN.length === 4 && ["ol", "or", "ul", "ur"].every((e) => KAMERA_ECKEN.includes(e)),
`und vier Ecken: ${KAMERA_ECKEN.join(", ")}`);
/* ---- DIE VORGABE DER DATENBANK MUSS IN DER LISTE STEHEN ----
Sonst startet eine frische Anlage mit einem Wert, den der Server
selbst ablehnen wuerde -- und der erste Klick auf einen anderen
Knopf waere die einzige Gelegenheit, es zu merken. */
{
const tab = readFileSync(join(HIER, "reaktion-tabellen.js"), "utf8");
const vGross = tab.match(/kamera_gross REAL NOT NULL DEFAULT ([\d.]+)/);
const vEcke = tab.match(/kamera_ecke TEXT NOT NULL DEFAULT '([a-z]+)'/);
ok(!!vGross && KAMERA_GROESSEN.includes(Number(vGross[1])),
`die Vorgabe der Spalte (${vGross?.[1]}) steht in der Liste`);
ok(!!vEcke && KAMERA_ECKEN.includes(vEcke[1]),
`und die Vorgabe der Ecke (${vEcke?.[1]}) auch`);
}
/* ---- JEDE GROESSE LAESST SICH STELLEN ---- */
for (const g of KAMERA_GROESSEN) {
const a = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_gross: g },
});
ok(a.code === 200 && a.j?.kamera_gross === g, `Groesse ${g} (${a.code})`);
}
for (const e of KAMERA_ECKEN) {
const a = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_ecke: e },
});
ok(a.code === 200 && a.j?.kamera_ecke === e, `Ecke ${e} (${a.code})`);
}
/* ---- UND WAS NICHT IN DER LISTE STEHT, WIRD ABGELEHNT ----
Die Gegenprobe zu oben. Eine Pruefung, die nur bestaetigt, dass
das Erlaubte geht, bestaetigt nichts ueber die Grenze. */
const zuGross = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_gross: 8 },
});
ok(zuGross.code === 400 && zuGross.j?.fehler === "kameragroesse_unklar",
`achtfach wird abgelehnt (${zuGross.code}/${zuGross.j?.fehler})`);
const quatschE = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: { kamera_ecke: "mitte" },
});
ok(quatschE.code === 400 && quatschE.j?.fehler === "ecke_unklar",
`eine erfundene Ecke wird abgelehnt (${quatschE.code}/${quatschE.j?.fehler})`);
const leer = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin, rumpf: {},
});
ok(leer.code === 400 && leer.j?.fehler === "nichts_zu_tun",
`ein Aufruf ohne Inhalt loest keinen Rundruf aus (${leer.code})`);
const fremd = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.modi, rumpf: { kamera_gross: 2 },
});
ok(fremd.code === 403, `ein Modi stellt die Groesse nicht (${fremd.code})`);
/* ---- DREI EINSTELLUNGEN IN EINEM AUFRUF ----
Das ist der Grund, warum es EIN Weg ist. Drei getrennte Aufrufe
waeren drei Rundrufe, und zwischen ihnen saehen die Zusehenden
eine Mischung: neue Anordnung, alte Ecke. */
const alles = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin,
rumpf: { layout: "gleich", kamera_gross: 1.5, kamera_ecke: "ol" },
});
ok(alles.code === 200 && alles.j?.layout === "gleich"
&& alles.j?.kamera_gross === 1.5 && alles.j?.kamera_ecke === "ol",
`Anordnung, Groesse und Ecke in einem Zug (${alles.code})`);
/* UND EINE FALSCHE ANGABE AENDERT GAR NICHTS. Sonst waere eine
abgelehnte Ecke eine halb durchgefuehrte Umstellung -- die
Anordnung stuende neu, die Ecke alt, und die Fehlermeldung
saehe aus, als sei nichts passiert. */
const halb = await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin,
rumpf: { layout: "kino", kamera_ecke: "diagonal" },
});
const danach = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
ok(halb.code === 400 && danach.j?.layout === "gleich",
`eine falsche Angabe laesst auch das Gute stehen (${danach.j?.layout})`);
/* ---- DER STAND TRAEGT BEIDES UND DIE AUSWAHL DAZU ----
Die Regie baut ihre Knoepfe daraus. Baute sie sie aus einer
eigenen Liste, waere das eine zweite Liste, die irgendwann von
der ersten abweicht -- und dann steht ein Knopf da, den der
Server ablehnt. */
await roh(CREW, "/workspace/api/reaktion/layout", {
method: "POST", keks: k.admin,
rumpf: { layout: "kino", kamera_gross: 1, kamera_ecke: "ur" },
});
const stand = await roh(CREW, "/workspace/api/reaktion", { keks: k.admin });
ok(stand.j?.kamera_gross === 1 && stand.j?.kamera_ecke === "ur",
"der Stand traegt Groesse und Ecke");
ok(Array.isArray(stand.j?.kamera_groessen)
&& stand.j.kamera_groessen.length === KAMERA_GROESSEN.length
&& Array.isArray(stand.j?.kamera_ecken)
&& stand.j.kamera_ecken.length === 4,
"und die Auswahl kommt vom Server, nicht aus dem Browser");
/* ---- AUCH DIE ZUSEHENDEN BEKOMMEN ES ----
Es ist Teil des Bildes, nicht der Regie. Wer es nicht bekaeme,
saehe die Fenster woanders als der, der darueber redet. */
const gast = await roh(CREW, "/workspace/api/reaktion", { keks: k.gast });
ok(gast.j?.kamera_gross === 1 && gast.j?.kamera_ecke === "ur",
"auch ein Zuschauer bekommt Groesse und Ecke");
/* ---- WAS DER QUELLTEXT HERGIBT ---------------------------------- */
{
const css = readFileSync(join(HIER, "..", "workspace", "assets", "css", "reaktion.css"), "utf8");
const js = readFileSync(join(HIER, "..", "workspace", "assets", "js", "reaktion.js"), "utf8");
const mld = readFileSync(join(HIER, "..", "workspace", "assets", "js", "meldung.js"), "utf8");
/* ==== KOMMENTARE RAUS, BEVOR IRGENDETWAS GEZAEHLT WIRD ========
Beim ersten Lauf meldete die Pruefung „1x steht eine feste
Breite am Fenster" -- und fand sie in einem Kommentar, der
genau davor warnt und die entfernte Zeile zitiert. Ein
Werkzeug, das bei jedem Lauf meckert, wird nach dem zweiten
Mal weggeklickt, zusammen mit dem echten Befund darin.
Gezaehlt werden Regeln, nicht Prosa. */
const regeln = css.replace(/\/\*[\s\S]*?\*\//g, " ");
/* JEDE ECKE BRAUCHT EINE REGEL -- sonst ist sie ein Knopf, der
nichts tut. */
const ohneEcke = KAMERA_ECKEN.filter((e) => !regeln.includes(`[data-ecke="${e}"]`));
ok(ohneEcke.length === 0,
ohneEcke.length ? `OHNE REGEL: ${ohneEcke.join(", ")}` : "jede Ecke hat eine eigene Regel");
ok(!regeln.includes('[data-ecke="mitte"]'),
"Gegenprobe: eine erfundene Ecke hat keine");
/* ---- EINE BREITE, EINE STELLE ----
Bis zum 28.09. stand die Breite eines Kamerafensters zweimal im
Stilblatt: als Grundregel und noch einmal fuer „Kameras gross".
Wer sie einstellbar macht, muesste beide mitdenken, und die
zweite wird vergessen. Das ist im selben Haus am selben Tag
zweimal passiert (die Saalzeilen, die Tafeln). */
const breiten = [...regeln.matchAll(/\.kamera\s*\{[^}]*?\bwidth:\s*clamp\(/g)];
ok(breiten.length === 0,
breiten.length
? `${breiten.length}x steht eine feste Breite am Fenster statt an --kam`
: "die Breite eines Fensters steht nur als --kam da");
ok(/--kam:\s*calc\([\s\S]*?var\(--kam-gross\)/.test(regeln),
"und der Faktor der Regie geht dort ein");
/* UND GERECHNET WIRD NUR EINMAL. `--kam-grund` darf jede
Anordnung und jede Bildschirmbreite fuer sich setzen -- die
Rechnung daraus steht an einer Stelle. Wer sie ein zweites Mal
hinschreibt, verliert den Faktor der Regie genau dort, wo er
es nicht sieht. Am 28.09. war das der Handy-Block. */
const rechnungen = [...regeln.matchAll(/--kam:\s*/g)];
ok(rechnungen.length === 1,
`die Breite wird ${rechnungen.length}x gerechnet (soll: 1x)`);
/* ---- EIN DECKEL, DER NICHT VOM FAKTOR ABHAENGT ----
Bei zweifacher Groesse auf einem schmalen Bildschirm deckte ein
einzelnes Fenster sonst die halbe Leinwand zu. Die Messung
nebenan prueft das am Bild; hier steht, dass es ueberhaupt
einen Deckel gibt. */
const stapel = regeln.match(/\.kameras \{[^}]*\}/);
ok(!!stapel && /max-width:\s*\d+%/.test(stapel[0]),
"der Stapel hat einen Deckel in Prozent der Leinwand");
/* ---- DIE REGIE BAUT AUS DER SERVERLISTE ---- */
ok(/lage\.kamera_groessen/.test(js) && /lage\.kamera_ecken/.test(js),
"die Regie baut ihre Knoepfe aus der Liste des Servers");
ok(!/\[\s*0\.6\s*,\s*0\.8\s*,\s*1\s*,/.test(js),
"Gegenprobe: sie haelt keine zweite Liste vor");
/* ---- EIN KNOPF, DER NICHTS TUT, WIRD NICHT ANGEBOTEN ----
Die Ecke wirkt nur bei „Video gross"; sonst stehen die Fenster
mittig oder fuellen die Flaeche. */
ok(/k\.disabled = !eckeGilt/.test(js),
"ausserhalb von „Video gross“ ist die Ecke nicht zu stellen");
/* UND DIE GROESSE BEI „NUR VIDEO“: Dort gibt es gar keine
Kamerafenster. Wer dann auf 2x drueckt, sieht nichts passieren
und sucht den Fehler bei sich. */
ok(/k\.disabled = !kamerasDa/.test(js),
"und bei „Nur Video“ ist auch die Groesse nicht zu stellen");
ok(/kamecke-hinweis/.test(js), "und die Regie sagt auch, warum");
/* ---- DER RUNDRUF BRINGT DAS GANZE BILD ----
Sonst saehen die Zusehenden die neue Anordnung mit der alten
Ecke, bis zufaellig etwas anderes den ganzen Stand nachzieht. */
ok(/was === 'layout'/.test(js) && /d\.kamera_gross/.test(js) && /d\.kamera_ecke/.test(js),
"die Meldung „layout“ bringt Anordnung, Groesse und Ecke");
/* ---- UND DIE ABLEHNUNG HAT EINEN DEUTSCHEN SATZ ----
Ein Fehlerkuerzel auf dem Bildschirm ist keine Antwort. */
for (const wort of ["kameragroesse_unklar", "ecke_unklar", "nichts_zu_tun"]) {
ok(mld.includes(wort), `„${wort}" hat einen deutschen Satz`);
}
}
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler === 0 ? "ALLES IN ORDNUNG" : "NICHT IN ORDNUNG");
+36
View File
@@ -93,6 +93,35 @@ export const STAENDE = ["zu", "vorbereitung", "live"];
* nichts doppelt zeigen. */
export const LAYOUTS = ["kino", "gleich", "kamera", "nur_video", "nur_kamera"];
/** Wie gross die Kamerafenster sind (Faktor auf die Grundgroesse).
*
* Filipe: „die verschiedenen groessen von kamera."
*
* SECHS STUFEN UND KEIN SCHIEBER. Ein Schieber verspricht eine
* stufenlose Einstellung; im Live sucht dann jemand mitten in der
* Sendung nach „dem richtigen Wert" statt zu senden. Sechs Stufen
* trifft man blind.
*
* DIE GRENZEN SIND GEMESSEN. Unter 0,6 ist der Name unter dem Bild
* nicht mehr zu lesen (das Schild steht bei 11,52 px und waechst
* nicht mit); ueber 2 deckt ein einzelnes Fenster bei „Video gross"
* mehr als die Haelfte der Leinwand zu, und dann ist es keine
* Anordnung mehr, sondern die falsche. */
export const KAMERA_GROESSEN = [0.6, 0.8, 1, 1.25, 1.5, 2];
/** In welcher Ecke die Kamerafenster liegen.
*
* NUR BEI „VIDEO GROSS" -- in den anderen Anordnungen stehen sie
* mittig oder fuellen die Flaeche, da gibt es keine Ecke. Genau das
* sagt die Regie auch, statt einen Knopf anzubieten, der nichts
* tut.
*
* WARUM ES DIESE EINSTELLUNG UEBERHAUPT BRAUCHT: Unten rechts liegt
* bei TikTok die Knopfreihe, bei YouTube die Fortschrittsleiste,
* und Untertitel stehen fast immer unten. Eine feste Ecke ist eine,
* die bei jeder zweiten Plattform falsch ist. */
export const KAMERA_ECKEN = ["ol", "or", "ul", "ur"];
/** Die Geschwindigkeiten, die es gibt.
*
* YouTube kann 0,25 bis 2. Die 0,25 fehlt hier mit Absicht: Bei
@@ -348,6 +377,13 @@ export function reaktionTabellen(d) {
d.exec("ALTER TABLE reaktion_dabei ADD COLUMN mikro_aus INTEGER NOT NULL DEFAULT 0");
console.log("[reaktion] Spalte mikro_aus angelegt.");
}
if (!da.has("kamera_gross")) {
/* Groesse und Ecke der Kamerafenster. Beide gelten fuer die
ganze Sendung -- wer sendet, stellt das Bild fuer alle ein. */
d.exec("ALTER TABLE reaktion ADD COLUMN kamera_gross REAL NOT NULL DEFAULT 1");
d.exec("ALTER TABLE reaktion ADD COLUMN kamera_ecke TEXT NOT NULL DEFAULT 'ur'");
console.log("[reaktion] Spalten fuer die Kameragroesse angelegt.");
}
if (!da.has("tempo")) {
/* Die Geschwindigkeit gilt fuer die ganze Sendung.
REAL und nicht INTEGER: 0,75 und 1,25 sind die beiden, die man
+5
View File
@@ -196,6 +196,11 @@ buehneRouter.get("/workspace/api/buehne/lage", mitSchluessel, (req, res) => {
waeren das zweieinhalb Minuten Unterschied, und was Filipe
kommentiert, haetten die Zuschauer noch gar nicht gesehen. */
tempo: Number(s?.tempo) || 1,
/* DIE ANORDNUNG GEHOERT AUF DIE BUEHNE. Die Videoquelle zeigt
nur das Video -- aber sie muss wissen, ob es ueberhaupt zu
sehen sein soll: Bei „nur Kamera" darf im Stream kein
stehengebliebenes YouTube-Bild liegen. */
layout: s?.layout || "kino",
gesendet: Date.now(),
titel: s?.titel || "",
});
+52 -5
View File
@@ -48,6 +48,7 @@ import { einstellung } from "./workspace.js";
import { mitZugangsdaten, wegeAusfaechern } from "./workspace-turn.js";
import {
SICHT_PLAETZE, GAESTE_MAX, TEXT_MAX, SCHREIB_BREMSE, STAENDE, LAYOUTS, TEMPI,
KAMERA_GROESSEN, KAMERA_ECKEN,
MELDUNG_TAGE,
VERLAUF_TAGE, LISTE_MAX, CHAT_MODI, STUMM_MAX_MINUTEN, MASSNAHMEN,
} from "./reaktion-tabellen.js";
@@ -257,6 +258,14 @@ function lage(person) {
nach fuenf Minuten zwei Minuten woanders. */
tempo: Number(s.tempo) || 1,
tempi: TEMPI,
/* GROESSE UND ECKE DER KAMERAS GEHEN AN ALLE. Sie sind Teil des
Bildes, nicht der Regie -- wer sie nicht bekaeme, saehe die
Fenster woanders und in einer anderen Groesse als der, der
darueber redet. */
kamera_gross: Number(s.kamera_gross) || 1,
kamera_ecke: s.kamera_ecke || "ur",
kamera_groessen: KAMERA_GROESSEN,
kamera_ecken: KAMERA_ECKEN,
gestartet_am: s.gestartet_am,
/* WER GERADE SEIN BILD AUS HAT -- fuer den Host und fuer jeden
Gast. Ohne diese Auskunft waere ein abgeschaltetes Bild von
@@ -1265,11 +1274,49 @@ reaktionRouter.post("/workspace/api/reaktion/chatmodus", nurHost, express.json({
reaktionRouter.post("/workspace/api/reaktion/layout", nurHost, express.json({ limit: "2kb" }),
(req, res) => {
try {
const l = String(req.body?.layout ?? "");
if (!LAYOUTS.includes(l)) return res.status(400).json({ fehler: "layout_unbekannt" });
db().prepare("UPDATE reaktion SET layout = ? WHERE id = 1").run(l);
melden("reaktion", { was: "layout", layout: l });
res.json({ ok: true, layout: l });
/* ==== DREI EINSTELLUNGEN, EIN WEG =========================
Anordnung, Groesse und Ecke beantworten zusammen eine
einzige Frage: „Wie sieht das Bild aus?" Drei Wege dafuer
waeren drei Rundrufe, und zwischen ihnen gaebe es
Augenblicke, in denen die Zusehenden eine Mischung sehen --
neue Anordnung, alte Ecke.
JEDES FELD IST FREIWILLIG, aber eines muss dabei sein. Ein
Aufruf, der nichts aendert, ist ein Rundruf ohne Anlass. */
const aendern = [];
const werte = [];
if (req.body?.layout !== undefined) {
const l = String(req.body.layout);
if (!LAYOUTS.includes(l)) return res.status(400).json({ fehler: "layout_unbekannt" });
aendern.push("layout = ?"); werte.push(l);
}
if (req.body?.kamera_gross !== undefined) {
const g = Number(req.body.kamera_gross);
if (!KAMERA_GROESSEN.includes(g)) {
return res.status(400).json({ fehler: "kameragroesse_unklar" });
}
aendern.push("kamera_gross = ?"); werte.push(g);
}
if (req.body?.kamera_ecke !== undefined) {
const e = String(req.body.kamera_ecke);
if (!KAMERA_ECKEN.includes(e)) {
return res.status(400).json({ fehler: "ecke_unklar" });
}
aendern.push("kamera_ecke = ?"); werte.push(e);
}
if (!aendern.length) return res.status(400).json({ fehler: "nichts_zu_tun" });
db().prepare(`UPDATE reaktion SET ${aendern.join(", ")} WHERE id = 1`).run(...werte);
const n = sendung();
const bild = {
layout: n.layout,
kamera_gross: Number(n.kamera_gross) || 1,
kamera_ecke: n.kamera_ecke || "ur",
};
melden("reaktion", { was: "layout", ...bild });
res.json({ ok: true, ...bild });
} catch (fehler) {
console.error("[reaktion] Layout:", fehler?.message);
res.status(503).json({ fehler: "nicht_verfuegbar" });