Phase 3 abgeschlossen

Fix: Streamlink auth header entfernt, mpv-Pfad explizit aufgelöst
Livestream wird kann jetzt abgespielt werden
This commit is contained in:
Andre
2026-04-08 17:44:48 +02:00
parent 60d4b4ace7
commit 3e8e5c2078
15 changed files with 773 additions and 182 deletions
+113 -120
View File
@@ -1,155 +1,148 @@
# Twitch4SteamDeck — Fortschritts-Snapshot
> Stand: **Ende 2026-04-07**. Diese Datei dient als Übergabe zwischen Sessions.
> Stand: **Ende 2026-04-07 (Session 2)**. Diese Datei dient als Übergabe zwischen Sessions.
> **Lies zuerst:** diese Datei + `todo.md` + `plan.md`.
> Architekturplan-Quelle: `C:\Users\andre\.claude\plans\smooth-shimmying-parasol.md` (im Repo gespiegelt).
>
> **Aktueller Stand:** Phase 0–2 abgeschlossen & vom User verifiziert. Als nächstes Phase 3 (Live-Wiedergabe).
> **Aktueller Stand:** Phase 0–2 abgeschlossen & verifiziert. Phase 3 Code-complete, noch NICHT verifiziert (mpv PATH-Problem beim ersten Start).
---
## Bereits abgeschlossen
### ✅ Projekt-Skelett (Electron + Vite + React + TypeScript)
- `package.json`, `tsconfig.json`, `tsconfig.node.json`, `tsconfig.web.json`
- `electron.vite.config.ts`, `electron-builder.yml`, `.gitignore`
- Main-Prozess Stub: `src/main/index.ts` (BrowserWindow 1280×800, dunkles Theme)
- Preload-Stub: `src/preload/index.ts` (contextBridge → `window.t4sd`)
- Renderer: `src/renderer/index.html`, `src/renderer/src/main.tsx`, `App.tsx`,
`styles/global.css` (Big-Screen-Baseline)
- **Status:** vom User bestätigt — `npm run dev` öffnet Fenster mit „Twitch4SteamDeck".
### ✅ Phase 0 — Projekt-Skelett
Siehe vorherige Sessions.
### ✅ Twitch OAuth Device Code Flow
- `src/main/auth/deviceCodeFlow.ts` — `requestDeviceCode()`, `pollForToken()`, `refreshAccessToken()`.
Endpoints: `https://id.twitch.tv/oauth2/device`, `https://id.twitch.tv/oauth2/token`.
Polling respektiert `interval`, `slow_down`, `expired`, `denied`, `cancelled`.
- `src/main/auth/tokenStore.ts` — Persistenz via Electron `safeStorage` (kein keytar/native module),
Datei: `userData/twitch-tokens.bin`. Fallback unverschlüsselt mit `0o600` falls Encryption nicht verfügbar.
- `src/main/auth/authService.ts` — `EventEmitter`, Methoden: `init()`, `startDeviceFlow()`,
`cancelFlow()`, `logout()`, `getValidAccessToken()` (mit Auto-Refresh, 60s Safety-Margin).
Scope MVP: `['user:read:follows']`. Emittiert `'auth-event'`.
- `src/main/ipc/handlers.ts` — IPC-Kanäle: `auth:get-status`, `auth:start-device-flow`,
`auth:cancel`, `auth:logout`, `auth:is-configured`, `auth:event` (main → renderer).
- `src/preload/index.ts` — exponiert `window.t4sd.auth.{isConfigured, getStatus, startDeviceFlow,
cancel, logout, onEvent}`.
- `src/renderer/src/screens/LoginScreen.tsx` — Phasen `idle | starting | awaiting | error | not-configured`,
QR-Code via `qrcode`-Lib, Countdown, Cancel-Button.
- `src/renderer/src/App.tsx` — Statusbasiertes Routing (`logged-out` → LoginScreen,
`logged-in` → Platzhalter mit Logout-Button).
- `src/renderer/src/types/t4sd.d.ts` — globale Window-Typen (manuell synchron zu Preload).
- `.env.example` — Vorlage für `MAIN_VITE_TWITCH_CLIENT_ID`.
- **Status:** ✅ End-to-End vom User verifiziert (2026-04-07). Login, Token-Persistenz nach Neustart funktionieren. Logout noch nicht getestet (kommt später).
### ✅ Phase 1 — Twitch OAuth Device Code Flow
Siehe vorherige Sessions. End-to-End verifiziert 2026-04-07.
### ✅ Phase 2 — Twitch Helix-Client + Sidebar-Layout
Siehe vorherige Sessions. End-to-End verifiziert 2026-04-07.
---
### ✅ Phase 2 — Twitch Helix-Client + Sidebar-Layout (2026-04-07, code-complete)
- `src/main/twitch/types.ts`, `helixClient.ts` — Helix-Wrapper: eigener User, followed channels (paginiert), live streams (batched), Avatare
- IPC `twitch:get-followed` in `handlers.ts`, `index.ts` (HelixClient-Instanz)
- Preload + `t4sd.d.ts` um `window.t4sd.twitch.getFollowed` erweitert
- **Sidebar-Layout:** `AppShell.tsx` als Post-Auth-Container mit `Sidebar.tsx` (Heart / Compass / User icons)
- Tabs: "Du folgst" (Default), "Durchsuchen" (Placeholder), "Mein Account" (Placeholder + Logout)
- Region-basierte Spatial-Navigation (`sidebar` ↔ `main`), Arrow-Left am linken Card-Grid-Rand springt in Sidebar
- `FollowingScreen.tsx` — Karten-Grid, live-first Sortierung, [Y] Refresh
- `FocusableCard.tsx` — **Block-Layout** (kein flex → kein overlap), Thumbnail/Avatar, LIVE-Badge, Viewer-Badge unten-links, Avatar+Name-Zeile, Titel, Spiel
- `Icons.tsx` — inline SVG-Icons
- `gamepad.ts` — Gamepad-Polling → synthetische KeyboardEvents (DPad + linker Stick + A/B)
- CSS komplett überarbeitet: `.app-layout` (Grid 260px / 1fr), `.sidebar`, `.screen`, `.card-grid` mit `grid-auto-rows: max-content`, `.card` mit Block-Layout
- **Entfernt:** alte `HomeScreen.tsx` (ersetzt durch FollowingScreen)
- **Status:** ✅ End-to-End vom User verifiziert (2026-04-07). Karten-Grid sauber, Tastatur-Navigation funktioniert, Sidebar-Switching ok.
### 🔄 Phase 3 — Streamlink + mpv (Live-Wiedergabe) — Code complete, nicht verifiziert
**Offener Rest aus Phase 2 (nicht blockierend):**
- Logout-Test (`userData/twitch-tokens.bin` wird gelöscht) — kommt später beim nächsten vollständigen Durchlauf
- Gamepad-Test mit echtem Steam-Deck-Controller steht noch aus (Dev auf Windows)
#### Neu erstellte Dateien (Session 2, 2026-04-07):
**Main-Prozess:**
- `src/main/playback/types.ts` — `PlaybackEvent`, `Quality`
- `src/main/playback/streamlink.ts` — `spawnStreamlink()` mit `resolveStreamlinkBin()` (sucht
`.exe` in `%LOCALAPPDATA%\Programs\Streamlink\bin\` usw., Fallback `streamlink`)
- `src/main/playback/mpvController.ts` — `getMpvIpcPath()`, `MpvController` (Named Pipe Win32
/ Unix Socket), connect mit Retry, `quit()`, `disconnect()`
- `src/main/playback/playbackService.ts` — orchestriert streamlink + mpv, `startLive()`, `stop()`,
`stopAll()`, emittiert `'playback-event'`
- `src/main/ipc/handlers.ts` — erweitert um `playback:start-live`, `playback:stop`,
`playback:event` (main→renderer); `registerIpcHandlers` nimmt jetzt `PlaybackService` als 3. Arg
- `src/main/index.ts` — instanziiert `PlaybackService`, registriert
`app.on('before-quit', () => playback.stopAll())`
**Preload + Typen:**
- `src/preload/index.ts` — `window.t4sd.playback.{startLive, stop, onEvent}` hinzugefügt
- `src/renderer/src/types/t4sd.d.ts` — `PlaybackEvent`-Typ + `playback`-API hinzugefügt
**Renderer:**
- `src/renderer/src/screens/ChannelScreen.tsx` — Detail-Screen: Thumbnail/Avatar/Name/Titel/Spiel/
Viewer, Button „▶ Live ansehen", Zustandsanzeige (idle/starting/playing/error),
Escape/B → Stop oder Zurück
- `src/renderer/src/screens/AppShell.tsx` — `selectedChannel`-State, Routing zu `ChannelScreen`,
`onSelectChannel` an `FollowingScreen` weitergegeben
- `src/renderer/src/screens/FollowingScreen.tsx` — `onSelectChannel`-Prop verkabelt,
`onSelect` der Karte ruft jetzt `onSelectChannel(ch)` auf (nicht mehr nur `console.log`)
- `src/renderer/src/styles/global.css` — `.channel-screen` + alle Unter-Klassen ergänzt
#### Architektur-Entscheidungen in Phase 3:
- **Variante 2:** streamlink spawnt mpv selbst via `--player mpv --player-args "..."`.
Node steuert nur den streamlink-Prozess direkt.
- **mpv IPC** via Named Pipe (Win32) oder Unix Socket (Linux), verbunden nach `spawn` im Hintergrund
(nicht blockierend, nicht kritisch für MVP). Wird für `quit()` beim Stop genutzt.
- **`resolveStreamlinkBin()`** sucht die `.exe` unter bekannten Windows-Installationspfaden,
da Electron den User-PATH nicht vollständig erbt.
---
## Wo wir gerade stehen — Start Phase 3
## Offener Punkt beim Start morgen — mpv PATH-Problem
**Ziel Phase 3:** Live-Wiedergabe werbefrei via `streamlink | mpv` starten, vom `FollowingScreen` aus
per Kanal-Karte → `ChannelScreen` (Detail) → Button „Live ansehen" → externes mpv-Fenster.
### Was passiert ist:
Streamlink startet (ENOENT behoben via `resolveStreamlinkBin()`), aber streamlink
kann mpv nicht finden, weil **mpv nicht im PATH ist, den streamlink von Electron erbt**.
### Was morgen zuerst passieren muss
Fehlerbild: `[streamlink] exit code 1`, kein mpv-Fenster.
**1. Dev-Tooling verifizieren (User-Aktion nötig):**
Wir entwickeln auf Windows, deployen später nach Linux/Flatpak. Für Phase 3 muss der User lokal:
- `mpv` installieren: `winget install mpv` (danach `mpv --version` in neuer Shell prüfen)
- `streamlink` installieren: `winget install streamlink` oder `pip install streamlink`
- **Baseline-Test** ohne unseren Code:
```
streamlink --twitch-disable-ads twitch.tv/<live-kanal> best --player mpv
```
→ öffnet mpv-Fenster mit dem Stream. Wenn das klappt, wissen wir, dass die Pipeline auf der
Windows-Dev-Umgebung grundsätzlich funktioniert und wir können im Code darauf aufbauen.
### Wahrscheinlichste Lösung (zuerst probieren):
**VS Code (und damit das Electron-Dev-Fenster) neu starten.** Nach dem `setx PATH ...`
für mpv muss VS Code neu gestartet werden, damit es den neuen PATH erbt.
**Wenn der Baseline-Test scheitert**, haben wir entweder:
- fehlende Binaries (nicht im PATH)
- Twitch hat `--twitch-disable-ads` gebrochen → Fallback auf `streamlink-ttvlol`-Plugin
- Region-/Geo-Blocking
```
1. VS Code schließen
2. VS Code neu öffnen
3. npm run dev
4. Kanal auswählen → "Live ansehen"
```
→ In diesem Fall **erst den Baseline fixen**, bevor Code geschrieben wird.
### Falls das nicht reicht — Fallback:
mpv-Pfad ebenfalls resolven wie streamlink. In `streamlink.ts` eine `resolveMpvBin()`-Funktion
ergänzen und den vollen Pfad an `--player` übergeben:
**2. Architektur-Entscheidung bestätigen (kurz abfragen):**
- Direkter Sprung von Karte → mpv **oder** dazwischen `ChannelScreen` als Detail-View?
- Plan sagt: `ChannelScreen` mit Detail + „Live ansehen"-Button. Das ist auch sinnvoll, weil
in Phase 4 derselbe Screen die VOD-Liste hostet. Default: so umsetzen.
- Quality-Picker im MVP erstmal mit Fixed-Default `best`, UI-Dropdown nachziehen wenn Rest steht.
```typescript
// In streamlink.ts hinzufügen:
function resolveMpvBin(): string {
if (process.platform === 'win32') {
const candidates = [
join(process.env.LOCALAPPDATA ?? '', 'Programs', 'mpv', 'mpv.exe'),
// shinchiro winget installiert nach: %LOCALAPPDATA%\Microsoft\WinGet\Packages\...
// Dort nach mpv.exe suchen wenn oben nicht gefunden
]
for (const p of candidates) {
if (existsSync(p)) return p
}
}
return 'mpv'
}
```
**3. Dateien, die in Phase 3 neu entstehen (siehe `todo.md` Phase 3 für Details):**
Dann in `spawnStreamlink()`:
```typescript
'--player', resolveMpvBin(), // statt '--player', 'mpv'
```
Main-Prozess:
- `src/main/playback/types.ts`
- `src/main/playback/streamlink.ts` — `spawnStreamlink({channelLogin, quality, oauthToken})`
- `src/main/playback/mpvController.ts` — Plattform-Weiche Named Pipe / Unix Socket, JSON-IPC
- `src/main/playback/playbackService.ts` — orchestriert streamlink + mpv, räumt auf
- Erweiterung `src/main/ipc/handlers.ts`: `playback:start-live`, `playback:stop`, `playback:event`
**Wichtig:** `where mpv` in der Shell ausführen um den exakten Pfad zu kennen, bevor
du `resolveMpvBin()` implementierst.
Renderer:
- `src/renderer/src/screens/ChannelScreen.tsx` — Detail-Screen mit „Live ansehen"-Button
- Erweiterung `AppShell.tsx`: `selectedChannel`-State, Routing FollowingScreen ↔ ChannelScreen
- Erweiterung Preload + `t4sd.d.ts`: `window.t4sd.playback.{startLive, stop, onEvent}`
- `FocusableCard.onSelect` verkabeln (aktuell nur `console.log`)
### Falls auch das nicht reicht:
streamlink selbst via `--player-args` testen ohne `--input-ipc-server` (vereinfachter Test),
um zu isolieren ob das Problem mpv-start oder IPC-Argument ist.
### Windows-spezifische Stolpersteine, die Claude morgen wissen muss
---
- **mpv IPC-Socket:** auf Windows Named Pipe im Format `\\.\pipe\twitch4sd-mpv-<pid>`, NICHT
`/tmp/...sock`. In `mpvController.ts` mit `process.platform === 'win32'` verzweigen.
- **`net.createConnection` unter Windows mit Named Pipes:** funktioniert, erwartet aber den Pfad
als `{path: '\\\\.\\pipe\\twitch4sd-mpv-...'}`.
- **hwdec-Flag:** auf Windows `--hwdec=auto` oder `--hwdec=d3d11va`, nicht `vaapi` (das ist Linux).
- **Pipe streamlink → mpv:** funktioniert unter Windows mit Node `spawn` und `stdio: ['pipe', ...]`
und `streamlink.stdout.pipe(mpv.stdin)`. Oder alternativ: streamlink mit `--player mpv
--player-args "..."` aufrufen und mpv selbst die Pipe machen lassen. Variante 1 gibt uns mehr
Kontrolle, Variante 2 ist einfacher — **Vorschlag: mit Variante 2 starten**, später
refactoren falls wir mpv-IPC direkt aus unserem Code brauchen.
- **Achtung bei Variante 2:** mpv-IPC-Socket muss als `--player-args` durchgereicht werden,
z. B. `--player-args="--input-ipc-server=\\\\.\\pipe\\twitch4sd-mpv --fullscreen"`.
- **Prozess-Cleanup:** Beim App-Quit alle spawned Subprozesse killen (sonst Zombie-mpv-Fenster).
`app.on('before-quit', () => playbackService.stopAll())`.
## Verifikations-Checkliste Phase 3 (noch offen)
### Technische Baseline aus Phase 0–2 (Wiederholung für Sanity-Check)
- Electron 33, electron-vite, TypeScript 5.5, React 18
- Auth: `authService.getValidAccessToken()` liefert einen frischen OAuth-Token
(Auto-Refresh via Refresh-Token, 60s Safety-Margin) — für `--twitch-api-header` direkt nutzbar
- HelixClient liefert `FollowedChannelInfo[]` mit `broadcasterLogin`, was als Channel-Name für
`twitch.tv/<login>` passt
- IPC-Pattern: `ipcMain.handle` im Main, `ipcRenderer.invoke` im Preload, `contextBridge` exponiert
`window.t4sd.*`. Main → Renderer Events via `webContents.send` + `ipcRenderer.on`
### Wichtiges Projekt-Memo
- Bei **allen Architekturänderungen** `plan.md` updaten (nicht nur `~/.claude/plans/...`).
- Nach **jeder Subaufgabe** Checkbox in `todo.md` setzen und 1-Zeilen-Update in dieser Datei.
- Dev-Host ist **Windows** (siehe Shell bash/Unix-Syntax in `/dev/null` etc.), Target ist
**Steam Deck (Linux / Flatpak)** — plattform-spezifische Weichen einbauen, wo es um Pfade,
Sockets und Hardware-Decoding geht.
- User akzeptiert „gut genug für MVP"-Entscheidungen (Quality-Picker später, kein Chat, etc.).
**Nicht** mit Speculative Abstractions aufblähen.
- [ ] 5 min Live-Stream ohne Werbe-Unterbrechung auf Windows (Dev-Setup)
- [ ] Beim Stop: mpv-Fenster schließt sich sauber, streamlink-Prozess terminiert
- [ ] Keine Zombie-Prozesse im Task-Manager nach Stop
- [ ] Logout/Login und erneutes Starten funktioniert ohne App-Neustart
---
## Erste Anweisung morgen an Claude (copy-paste)
> „Lies CLAUDE_PROGRESS.md, todo.md und plan.md. Wir wollen Phase 3 (Live-Wiedergabe via
> streamlink + mpv) starten. Frag mich zuerst, ob ich streamlink und mpv lokal installiert habe
> und ob der Baseline-Test aus CLAUDE_PROGRESS.md funktioniert, dann leg los."
> „Lies CLAUDE_PROGRESS.md, todo.md und plan.md. Phase 3 ist code-complete. Das mpv
> PATH-Problem war der letzte offene Punkt — ich habe VS Code neugestartet (oder den
> Fallback umgesetzt). Sag mir ob der Stream jetzt läuft."
---
## Technische Baseline (Sanity-Check)
- Electron 33, electron-vite, TypeScript 5.5, React 18
- streamlink 8.2.1 installiert unter `%LOCALAPPDATA%\Programs\Streamlink\bin\streamlink.exe`
- mpv v0.41.0 installiert, via `setx` zum PATH hinzugefügt (VS Code-Neustart nötig!)
- IPC-Pattern: `ipcMain.handle` → `ipcRenderer.invoke` → `contextBridge` → `window.t4sd.*`
- Main→Renderer Events via `webContents.send` + `ipcRenderer.on`
### Wichtiges Projekt-Memo
- Bei **allen Architekturänderungen** `plan.md` updaten.
- Nach **jeder Subaufgabe** Checkbox in `todo.md` setzen und Update hier.
- Dev-Host: **Windows**. Target: **Steam Deck (Linux / Flatpak)**.
- **Nicht** mit Speculative Abstractions aufblähen.