mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
fix: surface playback errors and stabilize disk cache size
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.
This commit is contained in:
@@ -12,6 +12,9 @@ const isDev = !app.isPackaged
|
|||||||
// Erlaubt video.play() ohne vorangehende User-Geste (nötig, weil play() in hls.js-Callbacks
|
// 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).
|
// aufgerufen wird, die asynchron weit nach dem ursprünglichen Click-Event stattfinden).
|
||||||
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
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 {
|
function createWindow(): BrowserWindow {
|
||||||
const isLinux = process.platform === 'linux'
|
const isLinux = process.platform === 'linux'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { FollowedChannelInfo, GameInfo, HlsUrlPayload } from '../types/t4sd
|
|||||||
|
|
||||||
type Region = 'sidebar' | 'main'
|
type Region = 'sidebar' | 'main'
|
||||||
type QuitChoice = 'yes' | 'no'
|
type QuitChoice = 'yes' | 'no'
|
||||||
type GlobalPlayState = 'idle' | 'starting' | 'playing' | 'paused'
|
type GlobalPlayState = 'idle' | 'starting' | 'playing' | 'paused' | 'error'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onLogout: () => void
|
onLogout: () => void
|
||||||
@@ -42,6 +42,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
const [liveCurrentQuality, setLiveCurrentQuality] = useState('best')
|
const [liveCurrentQuality, setLiveCurrentQuality] = useState('best')
|
||||||
const [liveQualityPanelOpen, setLiveQualityPanelOpen] = useState(false)
|
const [liveQualityPanelOpen, setLiveQualityPanelOpen] = useState(false)
|
||||||
const [liveQualityFocusedIndex, setLiveQualityFocusedIndex] = useState(0)
|
const [liveQualityFocusedIndex, setLiveQualityFocusedIndex] = useState(0)
|
||||||
|
const [liveErrorMsg, setLiveErrorMsg] = useState('')
|
||||||
|
|
||||||
const isGlobalPlaying = livePlayState !== 'idle'
|
const isGlobalPlaying = livePlayState !== 'idle'
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
if (!isGlobalPlaybackInitiated.current) return
|
if (!isGlobalPlaybackInitiated.current) return
|
||||||
if (ev.kind === 'started') {
|
if (ev.kind === 'started') {
|
||||||
setLivePlayState('playing')
|
setLivePlayState('playing')
|
||||||
} else if (ev.kind === 'stopped' || ev.kind === 'error') {
|
} else if (ev.kind === 'stopped') {
|
||||||
isGlobalPlaybackInitiated.current = false
|
isGlobalPlaybackInitiated.current = false
|
||||||
setLiveHlsPayload(null)
|
setLiveHlsPayload(null)
|
||||||
setLivePlayState('idle')
|
setLivePlayState('idle')
|
||||||
@@ -65,6 +66,14 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
setLiveAvailableQualities(undefined)
|
setLiveAvailableQualities(undefined)
|
||||||
setLiveCurrentQuality('best')
|
setLiveCurrentQuality('best')
|
||||||
setLiveQualityPanelOpen(false)
|
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() }
|
return () => { unsubHls(); unsubEvent() }
|
||||||
@@ -106,6 +115,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
setLiveAvailableQualities(undefined)
|
setLiveAvailableQualities(undefined)
|
||||||
setLiveCurrentQuality('best')
|
setLiveCurrentQuality('best')
|
||||||
setLiveQualityPanelOpen(false)
|
setLiveQualityPanelOpen(false)
|
||||||
|
setLiveErrorMsg('')
|
||||||
void window.t4sd.playback.startLive(ch.broadcasterLogin).then(() => {
|
void window.t4sd.playback.startLive(ch.broadcasterLogin).then(() => {
|
||||||
window.t4sd.playback.getQualities(`twitch.tv/${ch.broadcasterLogin}`)
|
window.t4sd.playback.getQualities(`twitch.tv/${ch.broadcasterLogin}`)
|
||||||
.then((qs) => setLiveAvailableQualities(qs.length > 0 ? qs : []))
|
.then((qs) => setLiveAvailableQualities(qs.length > 0 ? qs : []))
|
||||||
@@ -114,10 +124,15 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleStopLive = useCallback(() => {
|
const handleStopLive = useCallback(() => {
|
||||||
|
isGlobalPlaybackInitiated.current = false
|
||||||
liveVideoRef.current?.stop()
|
liveVideoRef.current?.stop()
|
||||||
|
setLiveHlsPayload(null)
|
||||||
|
setLivePlayState('idle')
|
||||||
|
setLiveChannel(null)
|
||||||
setLiveAvailableQualities(undefined)
|
setLiveAvailableQualities(undefined)
|
||||||
setLiveCurrentQuality('best')
|
setLiveCurrentQuality('best')
|
||||||
setLiveQualityPanelOpen(false)
|
setLiveQualityPanelOpen(false)
|
||||||
|
setLiveErrorMsg('')
|
||||||
void window.t4sd.playback.stop()
|
void window.t4sd.playback.stop()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -352,6 +367,21 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
Starte Wiedergabe…
|
Starte Wiedergabe…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Fehler-Overlay */}
|
||||||
|
{livePlayState === 'error' && (
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed', inset: 0, zIndex: 100,
|
||||||
|
background: '#000', display: 'flex', flexDirection: 'column',
|
||||||
|
alignItems: 'center', justifyContent: 'center', gap: '1rem'
|
||||||
|
}}>
|
||||||
|
<p style={{ color: '#ff6868', fontSize: '1.1rem', maxWidth: '60ch', textAlign: 'center', margin: 0 }}>
|
||||||
|
{liveErrorMsg}
|
||||||
|
</p>
|
||||||
|
<p style={{ color: '#888', fontSize: '0.9rem', margin: 0 }}>
|
||||||
|
B / Esc — Schließen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* VideoPlayer + Overlay sobald HLS-URL da ist */}
|
{/* VideoPlayer + Overlay sobald HLS-URL da ist */}
|
||||||
{liveHlsPayload && (
|
{liveHlsPayload && (
|
||||||
<>
|
<>
|
||||||
@@ -366,8 +396,8 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
onError={() => {
|
onError={() => {
|
||||||
isGlobalPlaybackInitiated.current = false
|
isGlobalPlaybackInitiated.current = false
|
||||||
setLiveHlsPayload(null)
|
setLiveHlsPayload(null)
|
||||||
setLivePlayState('idle')
|
setLivePlayState('error')
|
||||||
setLiveChannel(null)
|
setLiveErrorMsg('Wiedergabefehler — Stream konnte nicht geladen werden.')
|
||||||
}}
|
}}
|
||||||
onTimeUpdate={(s) => setLivePosition(s)}
|
onTimeUpdate={(s) => setLivePosition(s)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ const SIDEBAR_STEP = 10
|
|||||||
const BADGE_GAP_STEP = 2
|
const BADGE_GAP_STEP = 2
|
||||||
|
|
||||||
const MB = 1024 * 1024
|
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 {
|
function formatCacheSize(bytes: number): string {
|
||||||
if (bytes < MB) return `${(bytes / 1024).toFixed(0)} KB`
|
if (bytes < MB) return `${(bytes / 1024).toFixed(0)} KB`
|
||||||
@@ -46,12 +42,6 @@ function formatCacheSize(bytes: number): string {
|
|||||||
return `${(mb / 1024).toFixed(2)} GB`
|
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 */
|
/** Total focusable rows: badge options + sidebar slider + badge gap slider + hls toggle + cache action */
|
||||||
const SIDEBAR_SLIDER_ROW = OPTIONS.length
|
const SIDEBAR_SLIDER_ROW = OPTIONS.length
|
||||||
const BADGE_GAP_SLIDER_ROW = OPTIONS.length + 1
|
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 hlsCacheToggleFocused = hasFocus && focusedIndex === HLS_CACHE_TOGGLE_ROW
|
||||||
const cacheActionFocused = hasFocus && focusedIndex === CACHE_ACTION_ROW
|
const cacheActionFocused = hasFocus && focusedIndex === CACHE_ACTION_ROW
|
||||||
|
|
||||||
const cacheClassification = cacheSize !== null ? classifyCache(cacheSize) : null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="screen settings-screen">
|
<div className="screen settings-screen">
|
||||||
<header className="screen__header">
|
<header className="screen__header">
|
||||||
@@ -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
|
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
|
wieder abgerufen. Caching erzeugt auf dem Steam Deck unnötiges Disk-Wachstum und
|
||||||
Akku-Belastung. Thumbnails und API-Antworten bleiben immer gecacht.
|
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.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -332,11 +320,6 @@ export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): J
|
|||||||
<span className="settings-cache__size">
|
<span className="settings-cache__size">
|
||||||
{cacheSize === null ? '…' : formatCacheSize(cacheSize)}
|
{cacheSize === null ? '…' : formatCacheSize(cacheSize)}
|
||||||
</span>
|
</span>
|
||||||
{cacheClassification && (
|
|
||||||
<span className={`settings-cache__pill settings-cache__pill--${cacheClassification.status}`}>
|
|
||||||
{cacheClassification.label}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="settings-cache__action">
|
<span className="settings-cache__action">
|
||||||
{clearing ? 'Wird geleert …' : 'Cache leeren'}
|
{clearing ? 'Wird geleert …' : 'Cache leeren'}
|
||||||
|
|||||||
@@ -1351,28 +1351,6 @@ body,
|
|||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-cache__pill {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-cache__pill--normal {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: var(--color-fg-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-cache__pill--unusual {
|
|
||||||
background: rgba(255, 184, 60, 0.18);
|
|
||||||
color: #ffc56b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-cache__pill--high {
|
|
||||||
background: rgba(255, 95, 95, 0.18);
|
|
||||||
color: #ff8585;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-cache__action {
|
.settings-cache__action {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
Reference in New Issue
Block a user