mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
268 lines
14 KiB
Markdown
268 lines
14 KiB
Markdown
# Twitch4SteamDeck — CLAUDE.md
|
||
|
||
Ad-freier Twitch-Client für das Steam Deck. Electron + React, gamepad-navigierbar, Big-Screen-UI (1280x800). Unterstützt Live-Streams und VODs mit Resume. Deployment als Flatpak auf dem Steam Deck.
|
||
|
||
---
|
||
|
||
## Tech-Stack
|
||
|
||
| Schicht | Technologie |
|
||
|---|---|
|
||
| Framework | Electron 33, electron-vite, React 18, TypeScript 5.5 |
|
||
| Playback | mpv 0.41.0 (direkt gespawnt), Streamlink 6.11.0 im Flatpak-Build / 8.2.1 im aktuellen Windows-Dev-Setup |
|
||
| DB | better-sqlite3 (VOD-Verlauf + Resume-Positionen) |
|
||
| Packaging | Flatpak (freedesktop SDK 24.08, Electron2 BaseApp) |
|
||
| Build-Host | Windows, Flatpak-Build läuft in WSL2 |
|
||
| Target | Steam Deck (Linux x86_64, Gaming Mode) |
|
||
|
||
---
|
||
|
||
## Architektur
|
||
|
||
```
|
||
Electron Main Process
|
||
├── AuthService — Twitch Device Code Flow, Token-Verwaltung (safeStorage)
|
||
├── HelixClient — Twitch Helix REST API + GQL (Kapitel)
|
||
├── PlaybackService — Live-/VOD-Orchestrierung
|
||
│ ├── streamlink.ts — Prozess-Spawning (Streamlink + mpv)
|
||
│ ├── MpvController — mpv JSON-IPC (Unix-Socket / Named Pipe)
|
||
│ └── hlsTrimmer.ts — Lokale HLS-Snapshots für Live-VODs + fMP4-Seek-Workaround
|
||
├── historyRepo — SQLite VOD-Verlauf (Resume, Completed)
|
||
└── gamepadReader — Linux /dev/input/js* Joystick-Leser
|
||
|
||
Preload (contextBridge)
|
||
└── window.t4sd — Typisierte IPC-Bridge (auth, twitch, history, playback, gamepad)
|
||
|
||
Renderer (React)
|
||
├── App.tsx — Auth-Gate: LoginScreen | AppShell
|
||
├── AppShell.tsx — Tab-Routing, Sidebar/Main-Fokus
|
||
├── screens/ — FollowingScreen, BrowseScreen, CategoryScreen,
|
||
│ ChannelScreen (Hauptscreen), StreamListScreen, Settings, Account
|
||
├── input/gamepad.ts — Browser Gamepad API (Windows, Dev-Mode)
|
||
└── context/SettingsContext.tsx — localStorage-Settings (streamBadgeMode, sidebarWidth, …)
|
||
```
|
||
|
||
### Datenfluss
|
||
|
||
- **Auth:** LoginScreen → IPC → AuthService → Device Code Flow → Token (safeStorage)
|
||
- **Browsing:** Screen → IPC → HelixClient → Twitch Helix API → Screen
|
||
- **Live-Playback:** ChannelScreen → IPC → PlaybackService → `spawnStreamlink()` (startet mpv intern)
|
||
- **VOD-Playback:** ChannelScreen → IPC → PlaybackService → `getStreamUrl()` → Playlist-Inspektion (`fMP4`, `EVENT`, `ENDLIST`) → optional lokaler Snapshot (`hlsTrimmer.ts`) → `spawnMpv()` + MpvController-IPC
|
||
- **Gamepad:** `/dev/input/js*` (Linux/Gaming-Mode) **oder** `navigator.getGamepads()` (Windows/Dev) → synthetische `KeyboardEvent`s → alle Screen-Handler reagieren auf Keys
|
||
|
||
---
|
||
|
||
## Entwicklung
|
||
|
||
### Voraussetzungen
|
||
- Node.js, npm
|
||
- `.env` mit `MAIN_VITE_TWITCH_CLIENT_ID=<deine-client-id>` (Twitch Application, Public, Device Code Flow)
|
||
- Windows: mpv unter `C:\Program Files\MPV Player\mpv.exe`, Streamlink unter `%LOCALAPPDATA%\Programs\Streamlink\bin\streamlink.exe`
|
||
|
||
### Build-Befehle
|
||
|
||
```bash
|
||
npm install # Dependencies installieren
|
||
npm run dev # Electron Dev-Server mit Hot-Reload starten
|
||
npm run build # Produktions-Build (out/)
|
||
npm run typecheck # TypeScript prüfen (npx tsc --noEmit)
|
||
npm run package # Linux AppImage bauen (dist/)
|
||
```
|
||
|
||
### Flatpak-Build (WSL2)
|
||
Muss aus dem WSL2-Dateisystem (NICHT `/mnt/`) laufen:
|
||
```bash
|
||
bash flatpak/build-flatpak.sh
|
||
```
|
||
Das Skript: prüft deps → npm-Build → Python-Deps generieren → mpv/Streamlink-Tarballs hashen → `flatpak-builder` → SCP zum Steam Deck.
|
||
|
||
---
|
||
|
||
## Codebase-Struktur
|
||
|
||
```
|
||
src/main/
|
||
index.ts — Electron-Einstiegspunkt, Service-Wiring, Window
|
||
env.d.ts — Typ für MAIN_VITE_TWITCH_CLIENT_ID
|
||
auth/
|
||
authService.ts — Auth-Lifecycle, Token-Refresh, Event-Emitter
|
||
deviceCodeFlow.ts — OAuth Device Code + Polling (https://id.twitch.tv/oauth2/)
|
||
tokenStore.ts — safeStorage-Verschlüsselung, Fallback auf plaintext
|
||
input/
|
||
gamepadReader.ts — /dev/input/js* Leser, Hotplug (3s-Scan), Deduplizierung (40ms)
|
||
ipc/
|
||
handlers.ts — Alle ipcMain.handle()-Registrierungen
|
||
playback/
|
||
playbackService.ts — Zentraler Orchestrator (startLive, startVod, seek, stop)
|
||
mpvController.ts — mpv JSON-IPC-Client (Socket-Verbindung, Property-Observer)
|
||
streamlink.ts — Streamlink/mpv Prozess-Spawning, Pfad-Auflösung
|
||
hlsTrimmer.ts — HLS-Playlist-Parser, lokaler HTTP-Server für Snapshot-/Trim-Playlists
|
||
types.ts — Quality-Typ, PlaybackEvent
|
||
store/
|
||
db.ts — SQLite-Init, WAL-Modus, Migration (vod_history-Tabelle)
|
||
historyRepo.ts — upsertVod, updatePosition, markCompleted, getProgressMap
|
||
twitch/
|
||
helixClient.ts — Helix-API (followed, streams, videos, games) + GQL (Kapitel)
|
||
types.ts — Helix-Typen + vereinfachte Renderer-Typen
|
||
|
||
src/preload/
|
||
index.ts — contextBridge: window.t4sd (auth, twitch, history, playback, gamepad)
|
||
|
||
src/renderer/src/
|
||
App.tsx — Auth-Gate, Gamepad-Init
|
||
main.tsx — React-DOM-Bootstrap, SettingsProvider
|
||
screens/
|
||
AppShell.tsx — Tab-Routing, Sidebar/Main-Fokus-Split
|
||
LoginScreen.tsx — Device-Code-Login, QR-Code, Countdown
|
||
FollowingScreen.tsx — Gefolgte Kanäle (Grid), Live/Offline-Sort
|
||
BrowseScreen.tsx — Top-Streams-Shelf + Kategorien-Grid (Infinite Scroll)
|
||
CategoryScreen.tsx — Streams einer Kategorie
|
||
ChannelScreen.tsx — Channel-Detail, VOD-Shelf, Playback-Controls, Kapitel-Panel
|
||
StreamListScreen.tsx — Sprachgefilterte Stream-Liste (DE/EN-Tabs)
|
||
SettingsScreen.tsx — streamBadgeMode, sidebarWidth, badgeGap, mpvLogging
|
||
AccountScreen.tsx — Nutzerinfo, Logout
|
||
components/
|
||
FocusableCard.tsx — Wiederverwendbare Kanal-Karte (Thumbnail, Badge, Progress)
|
||
Sidebar.tsx — Navigationssidebar (6 Tabs)
|
||
LanguageBadge.tsx — Sprach-/Flagge-Badge
|
||
Icons.tsx — SVG-Icons
|
||
context/
|
||
SettingsContext.tsx — localStorage-Settings, CSS-Custom-Properties-Sync
|
||
input/
|
||
gamepad.ts — Browser Gamepad API, rAF-Poll, Achsen-Debounce
|
||
lib/
|
||
languageBadge.ts — ISO-Code → Flagge/Kürzel Mapping (~40 Sprachen)
|
||
styles/
|
||
global.css — Dark Theme (#0e0e10, #9147ff Akzent), 1280x800-optimiert
|
||
types/
|
||
t4sd.d.ts — window.t4sd-Typ-Deklaration
|
||
|
||
flatpak/
|
||
tv.twitch4steamdeck.App.yml — Flatpak-Manifest (mpv, Streamlink, alle Native-Deps)
|
||
build-flatpak.sh — 6-Schritt Build-Pipeline (WSL2)
|
||
twitch4steamdeck.sh — Launcher mit zypak-wrapper
|
||
tv.twitch4steamdeck.App.desktop
|
||
patches/mpv-ffmpeg7-avio-const.patch
|
||
```
|
||
|
||
---
|
||
|
||
## Wichtige Patterns & Architektur-Entscheidungen
|
||
|
||
### IPC-Bridge Pattern
|
||
Alle Renderer→Main-Calls gehen über `window.t4sd.*` (definiert in `src/preload/index.ts`). Neue Features brauchen:
|
||
1. Handler in `src/main/ipc/handlers.ts` (`ipcMain.handle('kanal:aktion', ...)`)
|
||
2. Bridge-Methode in `src/preload/index.ts`
|
||
3. Typ-Deklaration in `src/renderer/src/types/t4sd.d.ts`
|
||
|
||
### Gamepad Dual-Path
|
||
- **Linux/Gaming-Mode:** `src/main/input/gamepadReader.ts` liest `/dev/input/js*` direkt (Chromium Gamepad API funktioniert nicht im Flatpak-Sandbox ohne udev)
|
||
- **Windows/Dev:** `src/renderer/src/input/gamepad.ts` nutzt `navigator.getGamepads()` via rAF-Loop
|
||
- Beide Pfade erzeugen synthetische `KeyboardEvent('keydown')`. Alle UI-Komponenten reagieren nur auf Key-Events — kein Gamepad-Code in Screens.
|
||
- Button-Mapping: A=Enter, B=Escape, X=x, Y=y, LB=l1, RB=r1, LT=l2, RT=r2, DPad=Arrows
|
||
|
||
### Live-VOD- und fMP4-Snapshot-Logik
|
||
`PlaybackService` unterscheidet vor dem VOD-Start zwischen abgeschlossenen VODs und laufenden Live-VODs:
|
||
- `inspectVodPlaylist()` prüft das HLS-Manifest auf `#EXT-X-MAP`, `#EXT-X-PLAYLIST-TYPE` und `#EXT-X-ENDLIST`
|
||
- **Laufende Live-VODs** kommen von Twitch oft als offene `EVENT`-/DVR-Playlist zurück; diese würde mpv sonst wie einen Live-Stream behandeln und am Live-Rand starten
|
||
- **fMP4-VODs** sind zusätzlich für HLS-Seeks im FFmpeg/mpv-Demuxer unzuverlässig
|
||
|
||
Workaround in `src/main/playback/hlsTrimmer.ts`:
|
||
- erzeugt aus der aktuellen Twitch-Playlist einen lokalen statischen Snapshot ab Zielsegment
|
||
- setzt immer `#EXT-X-PLAYLIST-TYPE:VOD`
|
||
- setzt immer `#EXT-X-ENDLIST`
|
||
- serviert die Playlist via lokalem HTTP-Server (zufälliger Port auf `127.0.0.1`)
|
||
- mpv lädt neue Snapshots via `loadfile replace`
|
||
|
||
Wichtig:
|
||
- Warum HTTP statt `file://`: FFmpeg's Protokoll-Whitelist blockiert HTTPS-Subrequests von file://-HLS-Playlists
|
||
- `streamlink --stream-url` liefert bei laufenden VODs nur die rohe Twitch-DVR-URL; erst der lokale Snapshot macht daraus ein statisches VOD
|
||
- `PlaybackService` hält eine **absolute VOD-Zeitachse** über `playbackOffsetSeconds` + `lastKnownAbsolutePositionSeconds`, damit Kapitelwahl, Resume und Folge-Seeks nicht auf die lokale Zeit des Snapshots driften
|
||
- Reload-Races werden über `seekGeneration` / `loadGeneration` abgefedert, damit alte `time-pos`-Events keine falschen Offsets in die History schreiben
|
||
|
||
### mpv JSON-IPC
|
||
`src/main/playback/mpvController.ts` kommuniziert via Unix-Socket (`/tmp/twitch4sd-mpv.sock`) oder Windows Named Pipe (`\\.\pipe\twitch4sd-mpv`). Wichtig:
|
||
- Verbindung wird mit 12 Retries × 250ms hergestellt (mpv braucht Zeit zum Starten)
|
||
- `observeTimePos()` abonniert `time-pos`-Events; falls in 10s kein Event kommt → Fallback auf `pollTimePos()` (Intervall-Polling)
|
||
- `playback-restart` markiert nach `loadfile` den aktiven Reload als abgeschlossen; bei klassischen TS-VODs triggert es außerdem den initialen Resume-Seek
|
||
|
||
### VOD vs. Live Playback
|
||
- **Live:** `streamlink --player mpv --player-args '...'` — Streamlink managed mpv intern. Kein IPC nötig (kein Seeking bei Live).
|
||
- **VOD:** `streamlink --stream-url` liefert eine HLS-URL → mpv wird **direkt** mit `--input-ipc-server` gespawnt. Ermöglicht Seeking + Position-Tracking.
|
||
- **Laufendes Live-VOD:** Die von Streamlink gelieferte URL ist oft eine offene `index-dvr.m3u8` (`EVENT`, ohne `ENDLIST`). Diese darf nicht direkt an mpv gehen, sondern wird zuerst lokal als Snapshot eingefroren.
|
||
- **Geschlossenes fMP4-VOD:** Seeks laufen ueber Snapshot-Reload statt mpv-internem HLS-Seek.
|
||
- Reason: Piped Streams (streamlink mit stdout) sind nicht seekable.
|
||
|
||
### Twitch API
|
||
- Helix REST API für alle Standard-Daten: `https://api.twitch.tv/helix`
|
||
- Twitch GraphQL für Kapitel-Daten (`getVodChapters`): `https://gql.twitch.tv/gql` mit public client-id `kimne78kx3ncx6brgo4mv6wki5h1ko` (kein eigener Token nötig)
|
||
- Auth-Scope: nur `user:read:follows`
|
||
- `getTopGames()` macht 40 parallele `/streams?game_id=<id>&first=100` Calls zur Zuschauerzahl-Schätzung
|
||
|
||
### Settings-Persistenz
|
||
- UI-Settings (Badge-Mode, Sidebar-Breite, etc.) → `localStorage` unter Key `t4sd:settings` (kein IPC, kein Flicker beim Start)
|
||
- VOD-Verlauf + Resume → SQLite `history.db` in `userData/`
|
||
- Twitch-Tokens → Electron `safeStorage` (OS-Keystore), Datei: `userData/twitch-tokens.bin`
|
||
|
||
---
|
||
|
||
## Bekannte Einschränkungen
|
||
|
||
| Problem | Status | Details |
|
||
|---|---|---|
|
||
| Live-VOD startet am Live-Rand | Workaround aktiv | Offene `EVENT`-/DVR-Playlists werden vor dem Start als lokaler VOD-Snapshot eingefroren (`inspectVodPlaylist()` + `hlsTrimmer.ts`). |
|
||
| fMP4-Seek-Bug | Workaround aktiv | fMP4-Seeks laufen über Snapshot-Reload statt mpv-internem HLS-Seek (`reloadFmp4AtAbsolutePosition()` in `playbackService.ts`). |
|
||
| Streamlink `--twitch-api-header` | Nicht verwendet | Twitch lehnt Device-Code-Token für Streamlinks interne GQL-API ab. Öffentliche Streams funktionieren ohne. |
|
||
| Ad-Bypass | Nicht implementiert | `--twitch-disable-ads` von Streamlink deprecated. Post-MVP. |
|
||
| Flaggen-Emojis auf Windows | Nur Rechtecke | Unicode Regional Indicators brauchen Noto Color Emoji (Linux). Im Dev-Mode ignorieren. |
|
||
| Browser Gamepad API in Gaming Mode | Nicht nutzbar | Steam Input + Flatpak-Sandbox blockiert udev-Events für Chromium. Deshalb `/dev/input/js*` direkt. |
|
||
| Bluetooth-Controller-Deduplizierung | Aktiv | Mehrere `/dev/input/js*`-Devices melden gleiche Inputs → 40ms-Deduplizierungsfenster in `gamepadReader.ts` |
|
||
|
||
---
|
||
|
||
## Deployment auf dem Steam Deck
|
||
|
||
```bash
|
||
# Auf Steam Deck installieren (einmalig)
|
||
flatpak install --user twitch4steamdeck.flatpak
|
||
|
||
# Starten
|
||
flatpak run tv.twitch4steamdeck.App
|
||
|
||
# Update (Neuinstallation)
|
||
flatpak uninstall tv.twitch4steamdeck.App
|
||
flatpak install --user twitch4steamdeck.flatpak
|
||
```
|
||
|
||
Flatpak-Berechtigungen (aus Manifest): `--share=network`, `--socket=x11`, `--socket=pulseaudio`, `--device=all` (GPU + `/dev/input/*`), `--share=ipc`.
|
||
|
||
---
|
||
|
||
## Typische Debugging-Workflows
|
||
|
||
### mpv startet nicht / kein Playback
|
||
1. mpv-Logging aktivieren (SettingsScreen → "mpv-Logging")
|
||
2. Log-Pfad via IPC: `window.t4sd.playback.getLogPath()`
|
||
3. Prüfen: `spawnMpv()` in `src/main/playback/streamlink.ts:77`
|
||
4. Bei VODs prüfen, ob mpv mit einer lokalen `http://127.0.0.1/...m3u8` oder direkt mit einer Twitch-/CloudFront-`index-dvr.m3u8` gestartet wurde
|
||
|
||
### Live-VOD spielt Livestream statt VOD
|
||
1. `streamlink --stream-url https://www.twitch.tv/videos/<id> best` prüfen: liefert es eine nackte `index-dvr.m3u8`, ist das noch keine statische VOD-Quelle
|
||
2. Manifest prüfen: `#EXT-X-PLAYLIST-TYPE:EVENT` oder fehlendes `#EXT-X-ENDLIST` bedeutet offenes Live-DVR
|
||
3. `inspectVodPlaylist()` und `createTrimmedPlaylist()` in `src/main/playback/playbackService.ts` / `src/main/playback/hlsTrimmer.ts` prüfen
|
||
4. In `mpv.log` muss beim korrekten Pfad die Start-URL auf `127.0.0.1` zeigen; eine direkte CloudFront-`index-dvr.m3u8` bedeutet, dass der Snapshot-Pfad nicht gegriffen hat
|
||
|
||
### Gamepad-Eingaben kommen nicht an (Steam Deck)
|
||
1. `gamepadReader.ts` liest `/dev/input/js*` — prüfen ob Device vorhanden
|
||
2. Deduplizierungs-Fenster: 40ms — bei Doppel-Inputs erhöhen
|
||
3. Axis-Mapping: DPad ist Axis 6/7 (binary), Stick ist 0/1 (threshold 16384)
|
||
|
||
### IPC-Channel fehlt / unbekannt
|
||
Prüfen: `handlers.ts` (Main), `preload/index.ts` (Bridge), `t4sd.d.ts` (Renderer-Typen) — alle drei müssen übereinstimmen.
|
||
|
||
### TypeScript-Fehler
|
||
```bash
|
||
npm run typecheck
|
||
```
|
||
Zwei unabhängige tsconfig: `tsconfig.node.json` (Main+Preload) und `tsconfig.web.json` (Renderer).
|