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`. --> - - + + - +