diff --git a/server/mess-reaktion.mjs b/server/mess-reaktion.mjs
index ceec9ca6..389751de 100644
--- a/server/mess-reaktion.mjs
+++ b/server/mess-reaktion.mjs
@@ -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
diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs
index bbe12671..95e5fc26 100644
--- a/server/pruef-buehne.mjs
+++ b/server/pruef-buehne.mjs
@@ -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(", ")})`);
diff --git a/server/pruef-reaktion.mjs b/server/pruef-reaktion.mjs
index 89a89176..ebe9ba0d 100644
--- a/server/pruef-reaktion.mjs
+++ b/server/pruef-reaktion.mjs
@@ -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");
diff --git a/server/reaktion-tabellen.js b/server/reaktion-tabellen.js
index 8d3d7938..8af22ec8 100644
--- a/server/reaktion-tabellen.js
+++ b/server/reaktion-tabellen.js
@@ -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
diff --git a/server/workspace-buehne.js b/server/workspace-buehne.js
index 47bdf325..13f57333 100644
--- a/server/workspace-buehne.js
+++ b/server/workspace-buehne.js
@@ -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 || "",
});
diff --git a/server/workspace-reaktion.js b/server/workspace-reaktion.js
index b5435569..728c63d0 100644
--- a/server/workspace-reaktion.js
+++ b/server/workspace-reaktion.js
@@ -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" });
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index f5a5d74f..4598ba7d 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,8 +16,8 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+