Files
abdulive/docs/02-twitch-integration.md
ZeasyAlexandClaude Opus 5 5f6301268a Native Windows-Auslieferung statt Container
Docker faellt weg. Das Projekt hat keine npm-Abhaengigkeiten und Node fuehrt
TypeScript direkt aus - der Container schrieb nur noch drei Programme fest,
und dafuer braucht es keine Virtualisierung, sondern einen Ordner.

Der Anlass war, dass ADR-0004 zwar `docker compose pull` versprach, es aber
nie ein veroeffentlichtes Image gab. Wer installieren wollte, musste
whisper.cpp aus dem Quelltext uebersetzen.

Plattformschicht (packages/core/src/platform.ts):
- Programme werden neben der Anwendung gesucht, nicht im PATH
- Daten unter %LOCALAPPDATA%, damit Installation und Update ohne
  Administratorrechte auskommen
- Schriften mitgeliefert: ohne fontsdir setzt libass wortlos eine
  Ersatzschrift ein - gemessen 720 statt 826 Pixel Zeilenbreite

Windows-Eigenheiten, an echtem ffmpeg 8.1 durchprobiert:
- Pfade im Filtergraphen brauchen alle drei Schritte (Vorwaertsschraegstriche,
  escapter Doppelpunkt, Anfuehrungszeichen); je zwei davon reichen nicht
- `nice` gibt es nicht, ersetzt durch os.setPriority

Auslieferung:
- scripts/build-package.ts baut das Paket, laeuft auch unter Linux
- install.ps1 ohne Administratorrechte, Autostart ueber den Aufgabenplaner
- update legt neue Fassungen bereit; eingespielt wird beim naechsten Start,
  nie mitten im Stream
- ZIP lesen und schreiben ohne Abhaengigkeiten (packages/core/src/zip.ts)

Kein `irm ... | iex`-Einzeiler: git.zeasy.dev antwortete instanzweit mit einer
Umleitung auf /user/login, und `iex` fuehrte die Anmeldeseite als PowerShell
aus. Der Updater verfolgt Umleitungen jetzt nicht und hat keine erfundene
Vorgabeadresse mehr.

Ausserdem: backup.ps1 war an zwei Stellen dokumentiert, aber nie geschrieben.

ADR-0007 ersetzt ADR-0004. 84 Tests, Typpruefung sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ
2026-09-09 17:39:26 +02:00

129 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Twitch-Integration
## Zwei Identitäten
Es werden zwei getrennte Twitch-Apps/Tokens gebraucht:
1. **Broadcaster-Token** (Konto `abdulive_`) — für alles, was den eigenen Kanal
betrifft: Clips erstellen, Titel ändern, Raids starten, EventSub abonnieren,
Analytics lesen.
2. **Bot-Token** (eigenes Bot-Konto, z. B. `abdulive_bot`) — für Chat-Nachrichten.
Trennung, damit Bot-Nachrichten als Bot erkennbar sind und der Broadcaster-
Token nicht im Chat-Client hängt.
Client-IDs, Client-Secrets und beide Tokens liegen verschlüsselt in
`/db/secrets.json` im Docker-Volume und werden über Refresh Tokens automatisch
erneuert (`packages/twitch/secrets.ts`). Eingegeben werden sie im
Einrichtungs-Assistenten unter `http://localhost:4792/setup`, **nicht** in einer
`.env` und nie im Repo — siehe [ADR-0005](decisions/ADR-0005-zugangsdaten.md).
## Scopes
| Scope | Wofür | Token |
|-------|-------|-------|
| `chat:read` | IRC lesen | Bot |
| `chat:edit` | IRC schreiben | Bot |
| `user:write:chat` | Chat über Helix senden (Alternative zu IRC) | Bot |
| `moderator:read:chatters` | Zuschauerliste im Chat | Broadcaster |
| `clips:edit` | Clips erstellen | Broadcaster |
| `channel:manage:broadcast` | Titel/Kategorie ändern, Marker setzen | Broadcaster |
| `channel:read:subscriptions` | Sub-Events | Broadcaster |
| `moderator:read:followers` | Follow-Events (EventSub) | Broadcaster |
| `channel:manage:raids` | Raid starten/abbrechen | Broadcaster |
| `channel:read:redemptions` | Channel-Point-Einlösungen | Broadcaster |
| `channel:manage:redemptions` | eigene Punkte-Rewards verwalten | Broadcaster |
| `channel:manage:polls` | Umfragen | Broadcaster |
| `analytics:read:games` | Game-Analytics-Report | Broadcaster |
| `bits:read` | Bits-Events | Broadcaster |
Der Autorisierungs-Flow läuft über den Einrichtungs-Assistenten:
`Start-ScheduledTask -TaskName abdulive`, dann `http://localhost:4792/setup`
öffnen und je einmal
für Broadcaster- und Bot-Konto auf „Mit Twitch verbinden" klicken. Die
Redirect-URI, die in beiden Twitch-Apps hinterlegt sein muss, lautet
`http://localhost:4792/callback`.
## EventSub (WebSocket)
Abonniert werden beim Start:
| Subscription | Version | Nutzen für |
|---|---|---|
| `stream.online` | 1 | Broadcaster, Analytics, Recap |
| `stream.offline` | 1 | Recap, VOD-Kapitel |
| `channel.update` | 2 | VOD-Kapitel (Kategorie/Titel), Analytics |
| `channel.follow` | 2 | Community, Recap |
| `channel.subscribe` / `.message` / `.gift` | 1 | Recap, Overlay |
| `channel.cheer` | 1 | Recap, Overlay |
| `channel.raid` (to + from) | 1 | Raid-Tool |
| `channel.channel_points_custom_reward_redemption.add` | 1 | Loyalty, Overlay |
| `channel.poll.begin/progress/end` | 1 | Overlay |
| `channel.chat.message` | 1 | **optional**, siehe unten |
| `channel.chat.notification` | 1 | Community (Erstzuschauer-Marker) |
**Chat: IRC oder EventSub?** Beides funktioniert. Wir lesen den Chat über
**IRC (`irc-ws.chat.twitch.tv:443`)**, weil die Tags dort (`first-msg`,
`returning-chatter`, `emotes`, `badges`, `msg-id`) reichhaltiger und stabiler
sind als im EventSub-Payload, und weil Rate Limits beim Senden dort klar
dokumentiert sind. EventSub übernimmt alles Nicht-Chat.
**Verbindungsregeln:** Eine WS-Session hält max. 300 Subscriptions. Twitch
sendet alle ~10 s ein Keepalive; bleibt es aus, neu verbinden.
`session_reconnect` liefert eine neue URL — dort verbinden, *dann* erst die
alte Verbindung schließen. Nach jedem Reconnect: Lücke per Helix-Polling füllen.
## Helix — die relevanten Endpunkte
| Endpunkt | Tool |
|---|---|
| `GET /helix/streams?user_id=` | Ingest (Viewer-Sampling, alle 60 s) |
| `GET /helix/streams?game_id=&first=100` | Raid-Scout |
| `GET /helix/channels` / `PATCH /helix/channels` | Loyalty (Titel-Shop), VOD-Kapitel |
| `POST /helix/clips` | Clipper |
| `GET /helix/clips?id=` | Clipper (Metadaten + Thumbnail-URL) |
| `GET /helix/videos?user_id=&type=archive` | VOD-Kapitel |
| `POST /helix/streams/markers` | VOD-Kapitel, Clipper |
| `GET /helix/chat/chatters` | Community (Anwesenheit, alle 5 min) |
| `GET /helix/channels/followers` | Recap, Community |
| `POST /helix/raids` | Raid-Tool |
| `GET /helix/users?login=` | überall (Login → ID) |
**Rate Limits:** 800 Punkte/Minute pro Client-ID (Standard: 1 Punkt pro Request).
Der Helix-Client in `packages/twitch` führt einen Token-Bucket und drosselt
selbst; `Ratelimit-Remaining` aus dem Response-Header wird ausgewertet.
Bei `429` → Backoff nach `Ratelimit-Reset`.
## Clips: die Fallstricke
Das ist der Teil, an dem die Clip-Pipeline scheitert, wenn man ihn nicht kennt:
1. `POST /helix/clips` schneidet **rückwirkend ~30 s** ab dem Aufrufzeitpunkt
(minus Stream-Delay). Man kann keinen Zeitbereich wählen.
2. Der Clip ist **nicht sofort verfügbar**. Nach dem POST bekommt man `id` und
`edit_url`; die Verarbeitung dauert typischerweise 5–20 s. Danach liefert
`GET /helix/clips?id=` erst Metadaten.
3. Die MP4-URL steht nirgends in der API. Sie wird aus `thumbnail_url`
abgeleitet: alles ab `-preview-` abschneiden, `.mp4` anhängen.
```
https://clips-media-assets2.twitch.tv/AT-cm%7C123-preview-480x272.jpg
→ https://clips-media-assets2.twitch.tv/AT-cm%7C123.mp4
```
Das ist undokumentiert und kann sich ändern — deshalb im Code an genau einer
Stelle (`packages/twitch/clips.ts`) gekapselt und mit Fallback versehen.
4. Der Clip enthält das **fertig komposite Bild** (1920×1080), nicht die
Einzelquellen. Facecam und Gameplay lassen sich nur per Crop trennen, und nur
wenn das OBS-Layout fest ist.
5. Bitrate/Qualität entspricht dem Transcode, nicht dem Original.
**Deshalb: OBS-Replay-Buffer ist der bessere Primärweg.** Details in
[`docs/projects/clip-shorts.md`](projects/clip-shorts.md).
## OBS-WebSocket
obs-websocket v5 auf `ws://127.0.0.1:4455`. Genutzt für:
- `CurrentProgramSceneChanged` → VOD-Kapitel
- `SaveReplayBuffer` → Clipper
- `TriggerHotkeyByName`, `SetInputSettings` → Chat-Steuerung, Loyalty-Shop
- `GetSourceScreenshot` → Go-Live-Broadcaster (Screenshot für den Post)