Eigener obs-websocket-v5-Client (das Protokoll ist klein genug, und das Projekt bleibt ohne Abhaengigkeiten). Anmeldung, Wiederverbindung mit wachsender Pause, Ereignisse fuer ReplayBufferSaved und Szenenwechsel. Der Clipper loest zwei Wege gleichzeitig aus: OBS-Wiedergabepuffer fuer die Rohdatei in Aufnahmequalitaet, Twitch-Clip fuer die teilbare URL. Faellt einer aus, ist der andere trotzdem etwas wert. Ein direkt in OBS gedrueckter Hotkey wird ohne unseren Umweg erkannt und als vollwertiger Ausloeser behandelt. Die Pfaduebersetzung Windows -> Container hat eine eigene Datei und eigene Tests - das ist die Stelle, an der die Pipeline am ehesten scheitert. Dazu Warten auf den abgeschlossenen Schreibvorgang, bevor die Datei uebernommen wird. Zum Testvektor der OBS-Anmeldung: die Spezifikation nennt Salt und Challenge, aber kein erwartetes Ergebnis. Der Erwartungswert wurde unabhaengig mit Pythons hashlib aus derselben Beschreibung gerechnet; der Test sagt das im Kommentar, statt einen amtlichen Vektor vorzutaeuschen. 29 Tests gruen. Die Abnahme von S2 braucht ein laufendes OBS und steht aus. Ausserdem: ADR-0006 haelt fest, dass alles unter einem Dach laeuft - eine lokale Weboberflaeche plus Kindprozesse fuer Rechenarbeit. Die Prozessliste in der Architekturdoku ist entsprechend ersetzt. Der Onboarding-Assistent ist als Teil von S5 aufgenommen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
124 lines
6.0 KiB
Markdown
124 lines
6.0 KiB
Markdown
# Architektur
|
|
|
|
## Bild
|
|
|
|
```
|
|
Twitch OBS
|
|
┌──────────────────┐ ┌───────────┐
|
|
│ EventSub (WS) │ │ obs- │
|
|
│ IRC / Chat │ │ websocket │
|
|
│ Helix REST │ └─────┬─────┘
|
|
└────────┬─────────┘ │
|
|
│ │
|
|
┌────────▼────────────────────────▼────────┐
|
|
│ apps/ingest (Daemon) │
|
|
│ normalisiert alles zu Events │
|
|
└────────┬─────────────────────┬───────────┘
|
|
│ │
|
|
schreibt │ │ publiziert
|
|
▼ ▼
|
|
┌─────────────────┐ ┌──────────────────────┐
|
|
│ SQLite │ │ Event-Bus │
|
|
│ data/db/ │ │ (in-proc + WS 4790) │
|
|
└────────┬────────┘ └──────────┬───────────┘
|
|
│ │
|
|
┌───────────┬───────┼───────────┬───────────┼───────────┬──────────┐
|
|
▼ ▼ ▼ ▼ ▼ ▼ ▼
|
|
dashboard recap raid-scout vod-chapters community overlay- clipper
|
|
server │
|
|
│ │
|
|
overlay-web ffmpeg
|
|
(Browser- whisper
|
|
Source) │
|
|
data/clips
|
|
│
|
|
broadcaster
|
|
```
|
|
|
|
## Prozesse
|
|
|
|
**Ein Prozess.** Oberflaeche, Ingest und die Werkzeugmodule laufen gemeinsam in
|
|
einem Container; schwere Arbeit (ffmpeg, whisper.cpp) laeuft als Kindprozess und
|
|
darf abstuerzen, ohne die Chat-Aufnahme mitzureissen. Begruendung und Grenzen:
|
|
[ADR-0006](decisions/ADR-0006-ein-dach.md).
|
|
|
|
| Port | Wofuer |
|
|
|------|--------|
|
|
| 4792 | Weboberflaeche: Einrichtung, OAuth-Rueckruf, Sichtung, Dashboard, `/health` |
|
|
| 4791 | Overlay-Server fuer die OBS-Browser-Quellen (ab M3) |
|
|
|
|
Beide nur auf `127.0.0.1` des Hosts veroeffentlicht.
|
|
|
|
Die Werkzeuge sind Module, keine eigenen Dienste:
|
|
|
|
| Modul | laeuft | Ausloeser |
|
|
|-------|--------|-----------|
|
|
| Ingest | dauernd | IRC, EventSub, Helix-Polling |
|
|
| Community, Loyalty | dauernd | Chat-Ereignisse |
|
|
| Clipper | bei Bedarf | `!clip`, OBS-Hotkey, spaeter Chat-Spitze |
|
|
| Broadcaster | bei Bedarf | `stream.online` |
|
|
| Recap, VOD-Kapitel | bei Bedarf | `stream.offline` |
|
|
| Raid-Scout | bei Bedarf | `!raidziel`, Oberflaeche |
|
|
|
|
## Event-Bus
|
|
|
|
Der Ingest normalisiert alles auf ein einheitliches Event-Format:
|
|
|
|
```ts
|
|
type StreamEvent = {
|
|
id: string; // ULID
|
|
ts: string; // ISO-8601, UTC
|
|
type: EventType; // 'chat.message' | 'stream.online' | ...
|
|
streamId: string | null;
|
|
userId: string | null;
|
|
payload: unknown; // typisiert pro EventType
|
|
};
|
|
```
|
|
|
|
Transport: ein `EventEmitter` im Prozess. Solange alles unter einem Dach laeuft
|
|
(ADR-0006), braucht es keinen Netzwerk-Bus. Die `events`-Tabelle ist trotzdem
|
|
die Wahrheit: sie wird immer geschrieben, und ein spaeter herausgeloester
|
|
Prozess kann seinen Rueckstand ueber `ts > lastSeen` aufholen.
|
|
|
|
Vollständige Event-Liste: siehe [`03-data-model.md`](03-data-model.md).
|
|
|
|
## Pakete
|
|
|
|
| Paket | Inhalt |
|
|
|-------|--------|
|
|
| `packages/shared` | Ereignistypen, ULID |
|
|
| `packages/core` | Config, Layout, Logger mit Maskierung, Retry, YAML-Leser |
|
|
| `packages/twitch` | Secret-Speicher, OAuth, Helix-Client, IRC-Client |
|
|
| `packages/db` | SQLite-Verbindung, Migrationen, Repositories |
|
|
|
|
Regel: Apps dürfen von Paketen abhängen, Pakete nie von Apps. Apps hängen nicht
|
|
voneinander ab — Kommunikation läuft über Bus und DB.
|
|
|
|
Importiert wird über die `imports`-Map der Wurzel-`package.json`
|
|
(`#core/config.ts`, `#db/repos.ts`). Das ist eine Node-Funktion, kein
|
|
Build-Werkzeug — es gibt keinen Bundler und keine Pfad-Aliasse im Compiler.
|
|
|
|
## Technologie
|
|
|
|
- **TypeScript auf Node 24**, ohne Build-Schritt und ohne
|
|
Laufzeitabhängigkeiten — Node führt die `.ts`-Dateien direkt aus. Siehe
|
|
[ADR-0001](decisions/ADR-0001-monorepo-typescript.md)
|
|
- **SQLite** über das eingebaute `node:sqlite` — siehe
|
|
[ADR-0002](decisions/ADR-0002-sqlite.md)
|
|
- **EventSub über WebSocket**, nicht Webhooks — siehe
|
|
[ADR-0003](decisions/ADR-0003-eventsub-websocket.md)
|
|
- **ffmpeg** (CLI) für Videoverarbeitung
|
|
- **whisper.cpp** (CLI) für Untertitel — kein Python, keine GPU
|
|
- **Serverseitiges HTML** ohne Framework für Einrichtung und Sichtungsseite;
|
|
für Dashboard und Overlays ab M3 ggf. Vite + Preact
|
|
- **Docker Compose** für den Betrieb — siehe [ADR-0004](decisions/ADR-0004-docker.md)
|
|
|
|
## Fehlerverhalten
|
|
|
|
- Jeder Netzwerkaufruf hat Retry mit exponentiellem Backoff (`packages/core/retry`).
|
|
- Der Ingest darf niemals wegen eines Konsumentenfehlers sterben. Konsumenten
|
|
laufen in eigenen Prozessen; ein Fehler dort schreibt in `logs/` und wird im
|
|
Dashboard als Warnung angezeigt.
|
|
- Wenn EventSub die Verbindung verliert: Reconnect mit Session-Resume, danach
|
|
Lücke über Helix-Polling schließen (`stream.online`-Status, Follower-Liste).
|