Hub: der Dienst laeuft zentral statt auf dem Stream-PC

Zwei Dinge, die Reichweite bringen sollen, hingen bisher daran, dass jemand
vor seinem Rechner sitzt: der Discord-Rollen-Knopf und die Go-Live-
Ankuendigung. Beide brauchen nur Internet.

Der Schnitt verlaeuft entlang einer Frage: braucht die Aufgabe eine Datei
oder ein Programm, das nur auf dem Stream-PC existiert?

  Hub (LXC)                    Ingest (Abdus PC)
  ---------                    -----------------
  Chat, Zuschauerzahlen        OBS-Wiedergabepuffer
  Discord: Knopf und Go-Live   !clip, Rendern, Untertitel
  Overlay-Seiten fuer OBS

Beide schreiben in ihre eigene Datenbank. Eine gemeinsame waere eine
Netzwerkfreigabe fuer SQLite - der zuverlaessigste bekannte Weg zu einer
beschaedigten Datei.

Das kostet ADR-0005 seine Hauptbegruendung: "bindet nur auf 127.0.0.1" gilt
fuer den Hub nicht mehr. An die Stelle treten ein Passwort mit signiertem
Sitzungs-Cookie fuer alles, was ein Mensch bedient, und ein zufaelliger
Schluessel in der Adresse fuer die Browser-Sources. Der Schluessel ist kein
schoenes Verfahren - er steht in jedem OBS-Profil -, aber eine Browser-Source
kann kein Formular ausfuellen und keinen Login durchhalten.

`publicUrl` haengt der OAuth-Rueckruf daran. Twitch vergleicht die
Redirect-URI zeichengenau, und ein fehlender Schraegstrich sieht im Fehlertext
aus wie ein falsches Client-Secret.

Die Routen der Einrichtungsseite sind aus ingest/server.ts herausgeloest,
weil sie jetzt an zwei Servern haengen.

Begruendung und Konsequenzen in ADR-0008.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZeasyAlexandClaude Opus 5 committed 2026-09-18 00:12:58 +02:00
1 parent e18336c57f
commit 3b606e36fb
8 files changed
+985 -10

No files matched your search

+71 -1
View File
@@ -45,6 +45,37 @@ export type Config = {
ffmpeg: { encoder: string; preset: string; crf: number; niceness: number };
whisper: { model: string; language: string; threads: number };
};
broadcaster: {
enabled: boolean;
/** `live` postet in die Ankuendigungen, `draft` in den internen Kanal. */
mode: 'live' | 'draft';
delaySecondsAfterOnline: number;
cooldownHours: number;
/** Rollenname aus dem Discord-Bauplan. Leer heisst: Post ohne Ping. */
pingRole: string;
obsScreenshot: boolean;
};
/** Kanalpunkt-Belohnungen und was sie ausloesen. Siehe apps/hub/src/rewards.ts. */
rewards: {
enabled: boolean;
belohnungen: Array<{
titel?: string; id?: string;
aktion: 'konfetti' | 'emote-regen' | 'einblendung' | 'tts' | 'nichts';
freigabe?: boolean; maxZeichen?: number;
}>;
tts: {
freigabe: boolean; maxZeichen: number;
blockliste: string[]; proPersonProStunde: number;
};
};
overlay: { port: number; enabled: boolean };
/**
* Oeffentliche Adresse des Dienstes, ohne Schraegstrich am Ende. Nur noetig,
* wenn er nicht auf demselben Rechner wie der Browser laeuft - dann haengt
* daran der OAuth-Rueckruf von Twitch, der exakt zur eingetragenen
* Redirect-URI passen muss.
*/
publicUrl: string;
ingestServer: { port: number };
dashboard: { port: number };
raw: YamlMap;
@@ -81,6 +112,40 @@ const DEFAULTS = {
ffmpeg: { encoder: 'libx264', preset: 'veryfast', crf: 20, niceness: 15 },
whisper: { model: 'ggml-small.bin', language: 'de', threads: 4 },
},
broadcaster: {
enabled: true,
// Vorgabe ist `live`: der Sinn der Sache ist der Ping. Wer erst zusehen
// will, stellt `draft` ein - dann landet dieselbe Nachricht ohne Ping im
// internen Kanal. Zum Ausprobieren gibt es `abdulive golive:test`.
mode: 'live' as const,
// Twitch setzt Titel und Kategorie erst kurz nach dem Start. Wer sofort
// postet, kuendigt regelmaessig den Titel des letzten Streams an.
delaySecondsAfterOnline: 60,
cooldownHours: 6,
pingRole: 'Live-Ping',
obsScreenshot: true,
},
rewards: {
// Aus, bis jemand bewusst Belohnungen eintraegt. Eine Aktion, die
// niemand erwartet, ist im Stream schlimmer als gar keine.
enabled: false,
belohnungen: [] as Array<{
titel?: string; id?: string;
aktion: 'konfetti' | 'emote-regen' | 'einblendung' | 'tts' | 'nichts';
freigabe?: boolean; maxZeichen?: number;
}>,
tts: {
// Vorgabe ist die Freigabe durch Abdu. Kanalpunkte sammeln sich beim
// Zugucken von allein an und sind damit kaum eine Huerde; ohne diesen
// Schalter hat man eine Vorlesefunktion fuer Fremde im eigenen Stream.
freigabe: true,
maxZeichen: 200,
blockliste: [] as string[],
proPersonProStunde: 3,
},
},
overlay: { port: 4791, enabled: true },
publicUrl: '',
ingestServer: { port: 4790 },
dashboard: { port: 4792 },
};
@@ -105,7 +170,12 @@ export function loadConfig(): Config {
if (fs.existsSync(file)) {
raw = parseYaml(fs.readFileSync(file, 'utf8'));
}
cached = { ...merge(DEFAULTS, raw), raw } as Config;
const merged = merge(DEFAULTS, raw) as Omit<Config, 'raw'>;
// Die Umgebung gewinnt: im Container steht die oeffentliche Adresse in der
// Unit-Datei, nicht in einer YAML, die mitversioniert wird.
if (process.env.PUBLIC_URL) merged.publicUrl = process.env.PUBLIC_URL;
merged.publicUrl = merged.publicUrl.replace(/\/+$/, '');
cached = { ...merged, raw } as Config;
return cached;
}