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:
1 parent
e18336c57f
commit
3b606e36fb
8 files changed
+985
-10
No files matched your search
+185
-1
@@ -646,6 +646,177 @@ Was jetzt noch von Hand gehoert:
|
||||
verbinden, dann bekommen Subs automatisch eine Rolle.`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- overlay
|
||||
|
||||
/**
|
||||
* Overlay-Server allein starten. Zum Ansehen und Einrichten, ohne dass der
|
||||
* ganze Hub laeuft. Mit --demo laeuft erfundener Betrieb: anders koennte man
|
||||
* eine Raid-Einblendung nur pruefen, indem man auf einen echten Raid wartet.
|
||||
*/
|
||||
async function overlayOnly(args: string[]): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
const { startOverlayServer } = await import('../overlay-server/src/server.ts');
|
||||
const { createBus } = await import('../overlay-server/src/bus.ts');
|
||||
|
||||
const bus = createBus();
|
||||
const abort = new AbortController();
|
||||
process.on('SIGINT', () => { abort.abort(); process.exit(0); });
|
||||
|
||||
startOverlayServer({
|
||||
port: cfg.overlay.port,
|
||||
channel: cfg.channel.login,
|
||||
publicUrl: cfg.publicUrl,
|
||||
bus,
|
||||
});
|
||||
|
||||
if (args.includes('--demo')) {
|
||||
const { startDemo } = await import('../overlay-server/src/demo.ts');
|
||||
startDemo(bus, abort.signal);
|
||||
}
|
||||
|
||||
const wo = cfg.publicUrl || `http://127.0.0.1:${cfg.overlay.port}`;
|
||||
console.log(`
|
||||
Uebersicht mit allen Adressen fuer OBS: ${wo}/`);
|
||||
console.log(`${C.dim}Beenden mit Strg+C.${C.off}`);
|
||||
await new Promise(() => { /* laeuft bis Strg+C */ });
|
||||
}
|
||||
|
||||
/** Passwort fuer die Weboberflaeche setzen. Nur hier, nie ueber das Netz. */
|
||||
async function webPasswort(args: string[]): Promise<void> {
|
||||
const { setzePasswort } = await import('../overlay-server/src/auth.ts');
|
||||
const pw = args[0] || process.env.ABDULIVE_WEB_PASSWORT ||
|
||||
await promptHidden('Neues Passwort (Eingabe wird nicht angezeigt): ');
|
||||
setzePasswort(pw.trim());
|
||||
console.log(`${C.ok}Passwort gesetzt.${C.off} Angemeldet bleibt man 30 Tage.`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- golive
|
||||
|
||||
/**
|
||||
* Probelauf der Go-Live-Ankuendigung.
|
||||
*
|
||||
* Ohne Argument geht der Post in den internen Kanal und pingt niemanden -
|
||||
* damit laesst sich das Aussehen pruefen, ohne den Server aufzuwecken. Mit
|
||||
* `--echt` geht er dorthin, wo er im Betrieb hinginge, mit Ping. Das ist der
|
||||
* einzige Weg, vor dem ersten Stream zu sehen, ob der Ping wirklich ankommt;
|
||||
* eine Erwaehnung ohne `allowed_mentions` sieht naemlich genauso aus wie eine
|
||||
* mit, benachrichtigt aber niemanden.
|
||||
*/
|
||||
async function goLiveTest(args: string[]): Promise<void> {
|
||||
const echt = args.includes('--echt');
|
||||
const cfg = loadConfig();
|
||||
const { createAnnouncer } = await import('../broadcaster/src/golive.ts');
|
||||
const { repos } = await import('#db/repos.ts');
|
||||
const { helix } = await import('#twitch/helix.ts');
|
||||
const { readSecrets } = await import('#twitch/secrets.ts');
|
||||
|
||||
const discord = readSecrets().discord;
|
||||
const ziel = echt ? discord?.goLiveWebhook : discord?.technikWebhook;
|
||||
if (!ziel) {
|
||||
throw new Error(echt
|
||||
? 'Kein Go-Live-Webhook hinterlegt. Einmal `abdulive discord:setup` laufen lassen.'
|
||||
: 'Kein Webhook fuer den internen Kanal. Einmal `abdulive discord:setup` laufen lassen ' +
|
||||
'- der Technik-Webhook ist seit Fassung 0.6 im Bauplan.');
|
||||
}
|
||||
|
||||
// Wenn Abdu gerade live ist, wird der echte Stream angekuendigt. Sonst ein
|
||||
// erfundener - das prueft denselben Weg, nur mit anderem Text.
|
||||
let stream = {
|
||||
id: `probe-${Date.now()}`,
|
||||
title: 'Probelauf der Go-Live-Ankuendigung',
|
||||
category: 'Just Chatting',
|
||||
login: cfg.channel.login,
|
||||
startedAt: new Date().toISOString(),
|
||||
} as import('../broadcaster/src/golive.ts').StreamInfo;
|
||||
|
||||
try {
|
||||
const user = await helix.userByLogin(cfg.channel.login);
|
||||
const live = user ? await helix.streamByUserId(user.id) : null;
|
||||
if (live) {
|
||||
stream = {
|
||||
id: `probe-${live.id}-${Date.now()}`,
|
||||
title: live.title,
|
||||
category: live.game_name,
|
||||
login: cfg.channel.login,
|
||||
startedAt: live.started_at,
|
||||
thumbnailUrl: live.thumbnail_url,
|
||||
};
|
||||
console.log(`${C.dim}Stream laeuft gerade - der Probelauf nimmt die echten Daten.${C.off}`);
|
||||
} else {
|
||||
console.log(`${C.dim}Kein laufender Stream - der Probelauf nimmt erfundene Daten.${C.off}`);
|
||||
}
|
||||
} catch (err) {
|
||||
// Haeufigster Fall ist nicht "Netz weg", sondern "noch keine Twitch-App
|
||||
// hinterlegt". Der Satz soll das sagen, sonst sucht man am falschen Ende.
|
||||
console.log(`${C.warn}Twitch nicht abgefragt:${C.off} ${(err as Error).message}`);
|
||||
console.log(`${C.dim}Der Probelauf nimmt erfundene Daten - der Weg zum Webhook ` +
|
||||
`wird trotzdem vollstaendig geprueft.${C.off}`);
|
||||
}
|
||||
|
||||
// OBS nur dazunehmen, wenn es laeuft: sonst wartet der Screenshot ins Leere.
|
||||
let obs: Awaited<ReturnType<typeof withObs>> | null = null;
|
||||
if (cfg.broadcaster.obsScreenshot && await tcpProbe(OBS_HOST, OBS_PORT, 1500)) {
|
||||
obs = await withObs();
|
||||
}
|
||||
|
||||
const announcer = createAnnouncer({
|
||||
cfg: {
|
||||
enabled: true,
|
||||
mode: echt ? 'live' : 'draft',
|
||||
delaySeconds: 0,
|
||||
cooldownHours: cfg.broadcaster.cooldownHours,
|
||||
pingRole: cfg.broadcaster.pingRole,
|
||||
obsScreenshot: Boolean(obs),
|
||||
},
|
||||
repo: repos(openDb()),
|
||||
...(obs ? { obs: obs.client } : {}),
|
||||
// Ein Probelauf geht durch die Abklingzeit hindurch, sonst koennte man ihn
|
||||
// nach einem echten Stream sechs Stunden lang nicht wiederholen.
|
||||
refresh: async (s) => s,
|
||||
});
|
||||
|
||||
// Als eigene Art protokollieren: sonst sperrt der Probelauf ueber die
|
||||
// Abklingzeit die naechste echte Ankuendigung.
|
||||
const result = await announcer.announceNow(stream, { force: true, art: 'golive-test' });
|
||||
obs?.stop();
|
||||
|
||||
if (result.status === 'gesendet') {
|
||||
const wohin = result.mode === 'draft' ? 'interner Kanal' : 'Ankuendigungen';
|
||||
const ping = result.pinged ? `mit Ping an "${cfg.broadcaster.pingRole}"` : 'ohne Ping';
|
||||
console.log(`
|
||||
${C.ok}Post ist raus${C.off} (${wohin}, ${ping}).`);
|
||||
if (result.url) console.log(` ${result.url}`);
|
||||
if (echt && !result.pinged) {
|
||||
console.log(`
|
||||
${C.warn}Kein Ping gesetzt.${C.off} Pruefen: gibt es die Rolle ` +
|
||||
`"${cfg.broadcaster.pingRole}" auf dem Server, und ist ein Bot-Token hinterlegt?`);
|
||||
}
|
||||
if (!echt) console.log(`
|
||||
${C.dim}Mit --echt geht derselbe Post in die Ankuendigungen, ` +
|
||||
`mit Ping.${C.off}`);
|
||||
} else {
|
||||
console.log(`
|
||||
${C.bad}${result.status}${C.off}: ${result.reason}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** OBS kurz verbinden, damit ein Screenshot moeglich ist. */
|
||||
async function withObs() {
|
||||
const { createObs } = await import('#obs/client.ts');
|
||||
const { readSecrets } = await import('#twitch/secrets.ts');
|
||||
const abort = new AbortController();
|
||||
const client = createObs(`ws://${OBS_HOST}:${OBS_PORT}`,
|
||||
() => readSecrets().obs?.password, {});
|
||||
void client.run(abort.signal);
|
||||
// Kurz auf die Identifikation warten; ohne sie gibt es keinen Screenshot,
|
||||
// und das ist kein Grund zum Abbruch.
|
||||
for (let i = 0; i < 30 && !client.isConnected(); i++) {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
return { client, stop: () => abort.abort() };
|
||||
}
|
||||
|
||||
async function discordBot(): Promise<void> {
|
||||
const { createBot } = await import('#discord/bot.ts');
|
||||
const bot = createBot();
|
||||
@@ -673,7 +844,10 @@ function help(): void {
|
||||
console.log(`
|
||||
abdulive - Werkzeugkasten fuer Abdus Stream
|
||||
|
||||
ingest Daemon starten (Chat, Zuschauerzahlen, Einrichtungsseite)
|
||||
hub Zentraler Dienst: Chat, Discord, Go-Live, Overlay
|
||||
ingest Daemon auf dem Stream-PC (OBS, !clip, Rendern)
|
||||
overlay [--demo] Nur den Overlay-Server, --demo mit erfundenem Betrieb
|
||||
web:passwort [wort] Passwort fuer die Weboberflaeche setzen
|
||||
check-env Umgebung pruefen
|
||||
db:migrate Migrationen anwenden
|
||||
fetch-model [name] Whisper-Modell laden (Vorgabe: ggml-small.bin)
|
||||
@@ -684,6 +858,8 @@ abdulive - Werkzeugkasten fuer Abdus Stream
|
||||
discord:setup [--server ID] [--vorschau]
|
||||
Discord-Server nach docs/projects/discord-community.md einrichten
|
||||
discord:bot Rollen-Knoepfe bedienen (laeuft sonst im Daemon mit)
|
||||
golive:test [--echt] Go-Live-Post proben: ohne Argument in den internen
|
||||
Kanal ohne Ping, mit --echt in die Ankuendigungen
|
||||
help Diese Uebersicht
|
||||
|
||||
Einrichtung nach dem Start: http://localhost:4792/setup
|
||||
@@ -715,6 +891,14 @@ try {
|
||||
}
|
||||
case 'render': await renderOne(args[0], args[1]); break;
|
||||
case 'discord:setup': await discordSetup(args); break;
|
||||
case 'golive:test': await goLiveTest(args); break;
|
||||
case 'hub': {
|
||||
const { runHub } = await import('../hub/src/index.ts');
|
||||
await runHub();
|
||||
break;
|
||||
}
|
||||
case 'overlay': await overlayOnly(args); break;
|
||||
case 'web:passwort': await webPasswort(args); break;
|
||||
case 'discord:bot': await discordBot(); break;
|
||||
case 'help': case undefined: help(); break;
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user