14 KiB
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) odernavigator.getGamepads()(Windows/Dev) → synthetischeKeyboardEvents → alle Screen-Handler reagieren auf Keys
Entwicklung
Voraussetzungen
- Node.js, npm
.envmitMAIN_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
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 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:
- Handler in
src/main/ipc/handlers.ts(ipcMain.handle('kanal:aktion', ...)) - Bridge-Methode in
src/preload/index.ts - Typ-Deklaration in
src/renderer/src/types/t4sd.d.ts
Gamepad Dual-Path
- Linux/Gaming-Mode:
src/main/input/gamepadReader.tsliest/dev/input/js*direkt (Chromium Gamepad API funktioniert nicht im Flatpak-Sandbox ohne udev) - Windows/Dev:
src/renderer/src/input/gamepad.tsnutztnavigator.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-TYPEund#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-urlliefert bei laufenden VODs nur die rohe Twitch-DVR-URL; erst der lokale Snapshot macht daraus ein statisches VODPlaybackServicehält eine absolute VOD-Zeitachse überplaybackOffsetSeconds+lastKnownAbsolutePositionSeconds, damit Kapitelwahl, Resume und Folge-Seeks nicht auf die lokale Zeit des Snapshots driften- Reload-Races werden über
seekGeneration/loadGenerationabgefedert, damit altetime-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()abonnierttime-pos-Events; falls in 10s kein Event kommt → Fallback aufpollTimePos()(Intervall-Polling)playback-restartmarkiert nachloadfileden 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-urlliefert eine HLS-URL → mpv wird direkt mit--input-ipc-servergespawnt. Ermöglicht Seeking + Position-Tracking. - Laufendes Live-VOD: Die von Streamlink gelieferte URL ist oft eine offene
index-dvr.m3u8(EVENT, ohneENDLIST). 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/gqlmit public client-idkimne78kx3ncx6brgo4mv6wki5h1ko(kein eigener Token nötig) - Auth-Scope: nur
user:read:follows getTopGames()macht 40 parallele/streams?game_id=<id>&first=100Calls zur Zuschauerzahl-Schätzung
Settings-Persistenz
- UI-Settings (Badge-Mode, Sidebar-Breite, etc.) →
localStorageunter Keyt4sd:settings(kein IPC, kein Flicker beim Start) - VOD-Verlauf + Resume → SQLite
history.dbinuserData/ - 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
# 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
- mpv-Logging aktivieren (SettingsScreen → "mpv-Logging")
- Log-Pfad via IPC:
window.t4sd.playback.getLogPath() - Prüfen:
spawnMpv()insrc/main/playback/streamlink.ts:77 - Bei VODs prüfen, ob mpv mit einer lokalen
http://127.0.0.1/...m3u8oder direkt mit einer Twitch-/CloudFront-index-dvr.m3u8gestartet wurde
Live-VOD spielt Livestream statt VOD
streamlink --stream-url https://www.twitch.tv/videos/<id> bestprüfen: liefert es eine nackteindex-dvr.m3u8, ist das noch keine statische VOD-Quelle- Manifest prüfen:
#EXT-X-PLAYLIST-TYPE:EVENToder fehlendes#EXT-X-ENDLISTbedeutet offenes Live-DVR inspectVodPlaylist()undcreateTrimmedPlaylist()insrc/main/playback/playbackService.ts/src/main/playback/hlsTrimmer.tsprüfen- In
mpv.logmuss beim korrekten Pfad die Start-URL auf127.0.0.1zeigen; eine direkte CloudFront-index-dvr.m3u8bedeutet, dass der Snapshot-Pfad nicht gegriffen hat
Gamepad-Eingaben kommen nicht an (Steam Deck)
gamepadReader.tsliest/dev/input/js*— prüfen ob Device vorhanden- Deduplizierungs-Fenster: 40ms — bei Doppel-Inputs erhöhen
- 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
npm run typecheck
Zwei unabhängige tsconfig: tsconfig.node.json (Main+Preload) und tsconfig.web.json (Renderer).