From ed790dc3cd3deac82e58804efb4eee3ab1af4210 Mon Sep 17 00:00:00 2001 From: Andre <200930841+Andre-Bzk@users.noreply.github.com> Date: Tue, 12 May 2026 21:02:13 +0200 Subject: [PATCH] fix: surface playback errors and stabilize disk cache size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Playback errors in the global overlay (direct-start from Browse/Category/ StreamList screens) were silently discarded — the stream disappeared with no user feedback. Now shows an error overlay with the streamlink message and a dismiss hint; hls.js fatal errors use the same path. Chromium computes disk-cache-size dynamically from free disk space, so the value differs between runs. The cache index then contains a size that no longer matches, producing "Invalid cache (current) size" on shutdown. A fixed 500 MB cap keeps the value stable across restarts. The cache classification thresholds (500 MB / 1 GB) were designed for an uncapped cache. With a fixed 500 MB limit, the cache can never enter those states, leaving dead UI states and unused logic behind. --- src/main/index.ts | 3 ++ src/renderer/src/screens/AppShell.tsx | 38 ++++++++++++++++++--- src/renderer/src/screens/SettingsScreen.tsx | 19 +---------- src/renderer/src/styles/global.css | 22 ------------ 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index a2afcfa..aa3f8f9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -12,6 +12,9 @@ const isDev = !app.isPackaged // Erlaubt video.play() ohne vorangehende User-Geste (nötig, weil play() in hls.js-Callbacks // aufgerufen wird, die asynchron weit nach dem ursprünglichen Click-Event stattfinden). app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required') +// Explizite Cache-Größe verhindert Chromiums dynamische Größenberechnung, die bei +// wechselndem freiem Speicherplatz zu "Invalid cache (current) size"-Fehlern führt. +app.commandLine.appendSwitch('disk-cache-size', String(500 * 1024 * 1024)) function createWindow(): BrowserWindow { const isLinux = process.platform === 'linux' diff --git a/src/renderer/src/screens/AppShell.tsx b/src/renderer/src/screens/AppShell.tsx index 5fa01fe..c667b4c 100644 --- a/src/renderer/src/screens/AppShell.tsx +++ b/src/renderer/src/screens/AppShell.tsx @@ -14,7 +14,7 @@ import type { FollowedChannelInfo, GameInfo, HlsUrlPayload } from '../types/t4sd type Region = 'sidebar' | 'main' type QuitChoice = 'yes' | 'no' -type GlobalPlayState = 'idle' | 'starting' | 'playing' | 'paused' +type GlobalPlayState = 'idle' | 'starting' | 'playing' | 'paused' | 'error' interface Props { onLogout: () => void @@ -42,6 +42,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element { const [liveCurrentQuality, setLiveCurrentQuality] = useState('best') const [liveQualityPanelOpen, setLiveQualityPanelOpen] = useState(false) const [liveQualityFocusedIndex, setLiveQualityFocusedIndex] = useState(0) + const [liveErrorMsg, setLiveErrorMsg] = useState('') const isGlobalPlaying = livePlayState !== 'idle' @@ -56,7 +57,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element { if (!isGlobalPlaybackInitiated.current) return if (ev.kind === 'started') { setLivePlayState('playing') - } else if (ev.kind === 'stopped' || ev.kind === 'error') { + } else if (ev.kind === 'stopped') { isGlobalPlaybackInitiated.current = false setLiveHlsPayload(null) setLivePlayState('idle') @@ -65,6 +66,14 @@ export default function AppShell({ onLogout }: Props): JSX.Element { setLiveAvailableQualities(undefined) setLiveCurrentQuality('best') setLiveQualityPanelOpen(false) + setLiveErrorMsg('') + } else if (ev.kind === 'error') { + isGlobalPlaybackInitiated.current = false + setLiveHlsPayload(null) + setLivePlayState('error') + setLiveErrorMsg(ev.message ?? 'Stream konnte nicht gestartet werden.') + setLiveAvailableQualities(undefined) + setLiveQualityPanelOpen(false) } }) return () => { unsubHls(); unsubEvent() } @@ -106,6 +115,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element { setLiveAvailableQualities(undefined) setLiveCurrentQuality('best') setLiveQualityPanelOpen(false) + setLiveErrorMsg('') void window.t4sd.playback.startLive(ch.broadcasterLogin).then(() => { window.t4sd.playback.getQualities(`twitch.tv/${ch.broadcasterLogin}`) .then((qs) => setLiveAvailableQualities(qs.length > 0 ? qs : [])) @@ -114,10 +124,15 @@ export default function AppShell({ onLogout }: Props): JSX.Element { }, []) const handleStopLive = useCallback(() => { + isGlobalPlaybackInitiated.current = false liveVideoRef.current?.stop() + setLiveHlsPayload(null) + setLivePlayState('idle') + setLiveChannel(null) setLiveAvailableQualities(undefined) setLiveCurrentQuality('best') setLiveQualityPanelOpen(false) + setLiveErrorMsg('') void window.t4sd.playback.stop() }, []) @@ -352,6 +367,21 @@ export default function AppShell({ onLogout }: Props): JSX.Element { Starte Wiedergabe… )} + {/* Fehler-Overlay */} + {livePlayState === 'error' && ( +
+

+ {liveErrorMsg} +

+

+ B / Esc — Schließen +

+
+ )} {/* VideoPlayer + Overlay sobald HLS-URL da ist */} {liveHlsPayload && ( <> @@ -366,8 +396,8 @@ export default function AppShell({ onLogout }: Props): JSX.Element { onError={() => { isGlobalPlaybackInitiated.current = false setLiveHlsPayload(null) - setLivePlayState('idle') - setLiveChannel(null) + setLivePlayState('error') + setLiveErrorMsg('Wiedergabefehler — Stream konnte nicht geladen werden.') }} onTimeUpdate={(s) => setLivePosition(s)} /> diff --git a/src/renderer/src/screens/SettingsScreen.tsx b/src/renderer/src/screens/SettingsScreen.tsx index d1dfc4d..dd3c137 100644 --- a/src/renderer/src/screens/SettingsScreen.tsx +++ b/src/renderer/src/screens/SettingsScreen.tsx @@ -34,10 +34,6 @@ const SIDEBAR_STEP = 10 const BADGE_GAP_STEP = 2 const MB = 1024 * 1024 -const CACHE_THRESHOLD_UNUSUAL = 500 * MB -const CACHE_THRESHOLD_HIGH = 1024 * MB - -type CacheStatus = 'normal' | 'unusual' | 'high' function formatCacheSize(bytes: number): string { if (bytes < MB) return `${(bytes / 1024).toFixed(0)} KB` @@ -46,12 +42,6 @@ function formatCacheSize(bytes: number): string { return `${(mb / 1024).toFixed(2)} GB` } -function classifyCache(bytes: number): { status: CacheStatus; label: string } { - if (bytes >= CACHE_THRESHOLD_HIGH) return { status: 'high', label: 'Leeren empfohlen' } - if (bytes >= CACHE_THRESHOLD_UNUSUAL) return { status: 'unusual', label: 'Ungewöhnlich groß' } - return { status: 'normal', label: 'Normal' } -} - /** Total focusable rows: badge options + sidebar slider + badge gap slider + hls toggle + cache action */ const SIDEBAR_SLIDER_ROW = OPTIONS.length const BADGE_GAP_SLIDER_ROW = OPTIONS.length + 1 @@ -162,8 +152,6 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J const hlsCacheToggleFocused = hasFocus && focusedIndex === HLS_CACHE_TOGGLE_ROW const cacheActionFocused = hasFocus && focusedIndex === CACHE_ACTION_ROW - const cacheClassification = cacheSize !== null ? classifyCache(cacheSize) : null - return (
@@ -288,7 +276,7 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J HLS-Segmente (Live & VODs) sind Single-Use — sie werden nach dem Abspielen nie wieder abgerufen. Caching erzeugt auf dem Steam Deck unnötiges Disk-Wachstum und Akku-Belastung. Thumbnails und API-Antworten bleiben immer gecacht. - Cache-Größe: Normal unter 500 MB · ungewöhnlich ab 500 MB · Leeren empfohlen ab 1 GB. + Maximale Cache-Größe: 500 MB.