refactor: extract usePlaybackSession hook shared by AppShell and ChannelScreen

Both components managed identical playback state — HLS payload, play state,
quality panel, error message, videoRef — plus duplicate IPC subscriptions
and duplicate startLive/startVod/stop/getQualities sequences.

A shared hook removes that duplication and makes the two-context architecture
legible at a glance: both components call usePlaybackSession, AppShell with
active: !selectedChannel and ChannelScreen with active: true. The ownership
rule is now visible in two lines instead of requiring a cross-file search.
This commit is contained in:
Andre
2026-05-13 19:59:05 +02:00
parent 7a6a58054a
commit 086bda9b9c
3 changed files with 261 additions and 246 deletions
@@ -0,0 +1,156 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import type { Dispatch, RefObject, SetStateAction } from 'react'
import type { HlsUrlPayload, PlaybackEvent } from '../types/t4sd'
import type { VideoPlayerHandle } from '../components/VideoPlayer'
export type PlayState = 'idle' | 'starting' | 'playing' | 'paused' | 'error'
export interface PlaybackSession {
hlsPayload: HlsUrlPayload | null
playState: PlayState
videoRef: RefObject<VideoPlayerHandle>
availableQualities: string[] | undefined
currentQuality: string
qualityPanelOpen: boolean
qualityFocusedIndex: number
errorMsg: string
isActive: boolean
setPlayState: Dispatch<SetStateAction<PlayState>>
setQualityPanelOpen: Dispatch<SetStateAction<boolean>>
setQualityFocusedIndex: Dispatch<SetStateAction<number>>
startLive: (login: string, quality?: string) => Promise<void>
startVod: (vodId: string, login: string, title: string, duration: number, startSeconds?: number, quality?: string) => Promise<void>
stop: () => void
handleVideoError: (msg: string) => void
}
interface Options {
active: boolean
onStarted?: (ev: PlaybackEvent) => void
onStopped?: () => void
onError?: () => void
}
export function usePlaybackSession({ active, onStarted, onStopped, onError }: Options): PlaybackSession {
const [hlsPayload, setHlsPayload] = useState<HlsUrlPayload | null>(null)
const [playState, setPlayState] = useState<PlayState>('idle')
const [availableQualities, setAvailableQualities] = useState<string[] | undefined>()
const [currentQuality, setCurrentQuality] = useState('best')
const [qualityPanelOpen, setQualityPanelOpen] = useState(false)
const [qualityFocusedIndex, setQualityFocusedIndex] = useState(0)
const [errorMsg, setErrorMsg] = useState('')
const videoRef = useRef<VideoPlayerHandle>(null)
// Refs keep callbacks current inside the IPC subscription without resubscribing on every render
const onStartedRef = useRef(onStarted)
const onStoppedRef = useRef(onStopped)
const onErrorRef = useRef(onError)
useEffect(() => { onStartedRef.current = onStarted })
useEffect(() => { onStoppedRef.current = onStopped })
useEffect(() => { onErrorRef.current = onError })
const reset = useCallback(() => {
setHlsPayload(null)
setPlayState('idle')
setAvailableQualities(undefined)
setCurrentQuality('best')
setQualityPanelOpen(false)
setErrorMsg('')
}, [])
useEffect(() => {
if (!active) return
const unsubHls = window.t4sd.playback.onHlsUrl(setHlsPayload)
const unsubEvent = window.t4sd.playback.onEvent((ev: PlaybackEvent) => {
if (ev.kind === 'started') {
setPlayState('playing')
onStartedRef.current?.(ev)
} else if (ev.kind === 'stopped') {
reset()
onStoppedRef.current?.()
} else if (ev.kind === 'error') {
setHlsPayload(null)
setPlayState('error')
setErrorMsg(ev.message ?? 'Unbekannter Fehler')
setAvailableQualities(undefined)
setQualityPanelOpen(false)
onErrorRef.current?.()
}
})
return () => { unsubHls(); unsubEvent() }
}, [active, reset])
const startLive = useCallback(async (login: string, quality?: string) => {
videoRef.current?.stop()
setPlayState('starting')
setHlsPayload(null)
setAvailableQualities(undefined)
setCurrentQuality(quality ?? 'best')
setQualityPanelOpen(false)
setErrorMsg('')
try {
await window.t4sd.playback.startLive(login, quality)
} catch (e) {
setPlayState('error')
setErrorMsg(String(e))
return
}
void window.t4sd.playback.getQualities(`twitch.tv/${login}`)
.then(qs => setAvailableQualities(qs.length > 0 ? qs : []))
.catch(() => setAvailableQualities([]))
}, [])
const startVod = useCallback(async (
vodId: string, login: string, title: string, duration: number,
startSeconds?: number, quality?: string
) => {
videoRef.current?.stop()
setPlayState('starting')
setHlsPayload(null)
setAvailableQualities(undefined)
setCurrentQuality(quality ?? 'best')
setQualityPanelOpen(false)
setErrorMsg('')
try {
await window.t4sd.playback.startVod(vodId, login, title, duration, startSeconds, quality)
} catch (e) {
setPlayState('error')
setErrorMsg(String(e))
return
}
void window.t4sd.playback.getQualities(`twitch.tv/videos/${vodId}`)
.then(qs => setAvailableQualities(qs.length > 0 ? qs : []))
.catch(() => setAvailableQualities([]))
}, [])
const stop = useCallback(() => {
videoRef.current?.stop()
reset()
void window.t4sd.playback.stop()
}, [reset])
const handleVideoError = useCallback((msg: string) => {
setHlsPayload(null)
setPlayState('error')
setErrorMsg(msg)
}, [])
return {
hlsPayload,
playState,
videoRef,
availableQualities,
currentQuality,
qualityPanelOpen,
qualityFocusedIndex,
errorMsg,
isActive: playState !== 'idle',
setPlayState,
setQualityPanelOpen,
setQualityFocusedIndex,
startLive,
startVod,
stop,
handleVideoError,
}
}
+35 -79
View File
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { PlaybackOverlay } from '../components/PlaybackOverlay'
import { GamepadHintItem } from '../components/GamepadPrompt'
import Sidebar, { SIDEBAR_ITEMS, type TabKey } from '../components/Sidebar'
import { VideoPlayer, type VideoPlayerHandle } from '../components/VideoPlayer'
import { VideoPlayer } from '../components/VideoPlayer'
import AccountScreen from './AccountScreen'
import BrowseScreen from './BrowseScreen'
import CategoryScreen from './CategoryScreen'
@@ -10,11 +10,11 @@ import ChannelScreen from './ChannelScreen'
import FollowingScreen from './FollowingScreen'
import SettingsScreen from './SettingsScreen'
import StreamListScreen from './StreamListScreen'
import type { FollowedChannelInfo, GameInfo, HlsUrlPayload } from '../types/t4sd'
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
import { usePlaybackSession } from '../hooks/usePlaybackSession'
type Region = 'sidebar' | 'main'
type QuitChoice = 'yes' | 'no'
type GlobalPlayState = 'idle' | 'starting' | 'playing' | 'paused' | 'error'
interface Props {
onLogout: () => void
@@ -29,47 +29,32 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
const [quitDialogOpen, setQuitDialogOpen] = useState(false)
const [quitDialogChoice, setQuitDialogChoice] = useState<QuitChoice>('no')
// Globaler Playback-State — für Direkt-Start aus BrowseScreen/StreamListScreen/CategoryScreen
// Kanal-Info für den globalen Overlay — AppShell-spezifisch, kein Playback-State
const [liveChannel, setLiveChannel] = useState<FollowedChannelInfo | null>(null)
const [liveHlsPayload, setLiveHlsPayload] = useState<HlsUrlPayload | null>(null)
const [livePlayState, setLivePlayState] = useState<GlobalPlayState>('idle')
const [livePosition, setLivePosition] = useState(0)
const liveVideoRef = useRef<VideoPlayerHandle>(null)
// Quality state für globalen Overlay
const [liveAvailableQualities, setLiveAvailableQualities] = useState<string[] | undefined>(undefined)
const [liveCurrentQuality, setLiveCurrentQuality] = useState('best')
const [liveQualityPanelOpen, setLiveQualityPanelOpen] = useState(false)
const [liveQualityFocusedIndex, setLiveQualityFocusedIndex] = useState(0)
const [liveErrorMsg, setLiveErrorMsg] = useState('')
const isGlobalPlaying = livePlayState !== 'idle'
// IPC-Listener für globalen Playback — nur aktiv wenn kein ChannelScreen offen ist
useEffect(() => {
if (selectedChannel) return
const unsubHls = window.t4sd.playback.onHlsUrl(setLiveHlsPayload)
const unsubEvent = window.t4sd.playback.onEvent((ev) => {
if (ev.kind === 'started') {
setLivePlayState('playing')
} else if (ev.kind === 'stopped') {
setLiveHlsPayload(null)
setLivePlayState('idle')
setLiveChannel(null)
setLivePosition(0)
setLiveAvailableQualities(undefined)
setLiveCurrentQuality('best')
setLiveQualityPanelOpen(false)
setLiveErrorMsg('')
} else if (ev.kind === 'error') {
setLiveHlsPayload(null)
setLivePlayState('error')
setLiveErrorMsg(ev.message ?? 'Stream konnte nicht gestartet werden.')
setLiveAvailableQualities(undefined)
setLiveQualityPanelOpen(false)
}
})
return () => { unsubHls(); unsubEvent() }
}, [!!selectedChannel])
// Globale Playback-Session — aktiv wenn kein ChannelScreen offen ist
const globalSession = usePlaybackSession({
active: !selectedChannel,
onStopped: () => { setLiveChannel(null); setLivePosition(0) },
})
const {
hlsPayload: liveHlsPayload,
playState: livePlayState,
videoRef: liveVideoRef,
availableQualities: liveAvailableQualities,
currentQuality: liveCurrentQuality,
qualityPanelOpen: liveQualityPanelOpen,
qualityFocusedIndex: liveQualityFocusedIndex,
errorMsg: liveErrorMsg,
isActive: isGlobalPlaying,
setPlayState: setLivePlayState,
setQualityPanelOpen: setLiveQualityPanelOpen,
setQualityFocusedIndex: setLiveQualityFocusedIndex,
startLive: startGlobalLive,
stop: stopGlobal,
handleVideoError: handleLiveVideoError,
} = globalSession
const requestSidebar = useCallback(() => {
const idx = SIDEBAR_ITEMS.findIndex((t) => t.key === tab)
@@ -102,43 +87,18 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
// Direkt-Start: kein ChannelScreen — globaler Overlay startet sofort
const handleStartLive = useCallback((ch: FollowedChannelInfo) => {
setLiveChannel(ch)
setLivePlayState('starting')
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 : []))
.catch(() => setLiveAvailableQualities([]))
})
}, [])
void startGlobalLive(ch.broadcasterLogin)
}, [startGlobalLive])
const handleStopLive = useCallback(() => {
liveVideoRef.current?.stop()
setLiveHlsPayload(null)
setLivePlayState('idle')
setLiveChannel(null)
setLiveAvailableQualities(undefined)
setLiveCurrentQuality('best')
setLiveQualityPanelOpen(false)
setLiveErrorMsg('')
void window.t4sd.playback.stop()
}, [])
setLivePosition(0)
stopGlobal()
}, [stopGlobal])
const handleLiveQualityChange = useCallback((quality: string, channelLogin: string) => {
setLiveQualityPanelOpen(false)
setLiveCurrentQuality(quality)
setLiveAvailableQualities(undefined)
liveVideoRef.current?.stop()
setLiveHlsPayload(null)
setLivePlayState('starting')
void window.t4sd.playback.startLive(channelLogin, quality).then(() => {
window.t4sd.playback.getQualities(`twitch.tv/${channelLogin}`)
.then((qs) => setLiveAvailableQualities(qs.length > 0 ? qs : []))
.catch(() => setLiveAvailableQualities([]))
})
}, [])
void startGlobalLive(channelLogin, quality)
}, [startGlobalLive])
const handleBack = useCallback(() => {
setSelectedChannel(null)
@@ -383,11 +343,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
onPlaying={() => setLivePlayState('playing')}
onPaused={() => setLivePlayState('paused')}
onEnded={handleStopLive}
onError={() => {
setLiveHlsPayload(null)
setLivePlayState('error')
setLiveErrorMsg('Wiedergabefehler — Stream konnte nicht geladen werden.')
}}
onError={handleLiveVideoError}
onTimeUpdate={(s) => setLivePosition(s)}
/>
{(livePlayState === 'playing' || livePlayState === 'paused') && (
+70 -167
View File
@@ -2,15 +2,15 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { GamepadHintItem, GamepadPrompt } from '../components/GamepadPrompt'
import { EyeIcon } from '../components/Icons'
import { PlaybackOverlay } from '../components/PlaybackOverlay'
import { VideoPlayer, type VideoPlayerHandle } from '../components/VideoPlayer'
import type { FollowedChannelInfo, HlsUrlPayload, PlaybackEvent, VodChapter, VodInfo, VodProgress } from '../types/t4sd'
import { VideoPlayer } from '../components/VideoPlayer'
import type { FollowedChannelInfo, VodChapter, VodInfo, VodProgress } from '../types/t4sd'
import { usePlaybackSession } from '../hooks/usePlaybackSession'
interface Props {
channel: FollowedChannelInfo
onBack: () => void
}
type PlayState = 'idle' | 'starting' | 'playing' | 'paused' | 'error'
type FocusRegion = 'hero' | 'shelf' | 'chapters'
const CARD_W = 260
@@ -67,8 +67,6 @@ function formatTimestamp(s: number): string {
}
export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
const [playState, setPlayState] = useState<PlayState>('idle')
const [errorMsg, setErrorMsg] = useState('')
const [vods, setVods] = useState<VodInfo[]>([])
const [vodsLoading, setVodsLoading] = useState(true)
const [progressMap, setProgressMap] = useState<Record<string, VodProgress>>({})
@@ -81,17 +79,66 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
const [chapterIndex, setChapterIndex] = useState(0)
const [currentVod, setCurrentVod] = useState<VodInfo | null>(null)
const [pendingChapterVod, setPendingChapterVod] = useState<VodInfo | null>(null)
// Overlay state
const [currentPosition, setCurrentPosition] = useState(0)
const [vodDuration, setVodDuration] = useState(0)
const [isLivePlayback, setIsLivePlayback] = useState(false)
// HLS-Player state
const [hlsPayload, setHlsPayload] = useState<HlsUrlPayload | null>(null)
// Quality state
const [availableQualities, setAvailableQualities] = useState<string[] | undefined>(undefined)
const [currentQuality, setCurrentQuality] = useState('best')
const [qualityPanelOpen, setQualityPanelOpen] = useState(false)
const [qualityFocusedIndex, setQualityFocusedIndex] = useState(0)
const watchBtnRef = useRef<HTMLButtonElement>(null)
const chapterListRef = useRef<HTMLUListElement>(null)
const loadChapters = async (vodId: string): Promise<VodChapter[]> => {
return window.t4sd.twitch.getVodChapters(vodId)
}
const showChapterPanel = (vod: VodInfo): void => {
setChapterPanelVod(vod)
setChapters([])
setChapterIndex(0)
setChaptersLoading(true)
setFocusRegion('chapters')
void loadChapters(vod.id)
.then((loaded) => setChapters(loaded))
.catch(() => {})
.finally(() => setChaptersLoading(false))
}
// Playback-Session — ChannelScreen ist immer der aktive Eigentümer wenn gemountet
const session = usePlaybackSession({
active: true,
onStarted: (ev) => {
setVodDuration(ev.durationSeconds ?? 0)
setIsLivePlayback(ev.isLive ?? false)
setCurrentPosition(0)
},
onStopped: () => {
setChapterPanelVod(null)
setCurrentVod(null)
if (vods.length > 0) {
void window.t4sd.history
.getProgress(vods.map((v) => v.id))
.then(setProgressMap)
.catch(() => {})
}
if (pendingChapterVod) {
setPendingChapterVod(null)
showChapterPanel(pendingChapterVod)
} else {
setFocusRegion('shelf')
}
},
onError: () => {
setChapterPanelVod(null)
setCurrentVod(null)
setPendingChapterVod(null)
},
})
const {
hlsPayload, playState, videoRef,
availableQualities, currentQuality, qualityPanelOpen, qualityFocusedIndex,
errorMsg,
setPlayState, setQualityPanelOpen, setQualityFocusedIndex,
startLive, startVod, stop, handleVideoError,
} = session
// Derive active chapter name from chapters list + current playback position.
// Three states: loading → undefined (chip hidden), loaded+empty → 'Unbekannt', loaded+chapters → active chapter.
@@ -111,10 +158,6 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
return 1
}, [chapters, vodChaptersLoaded, currentPosition, isLivePlayback])
const watchBtnRef = useRef<HTMLButtonElement>(null)
const chapterListRef = useRef<HTMLUListElement>(null)
const videoRef = useRef<VideoPlayerHandle>(null)
useEffect(() => {
window.t4sd.twitch
.getVideos(channel.broadcasterId)
@@ -224,7 +267,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
break
case 'Escape':
e.preventDefault()
void handleStop()
stop()
break
case 'y': {
e.preventDefault()
@@ -298,86 +341,16 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
return () => window.removeEventListener('keydown', onKey)
}, [focusRegion, playState, vods, shelfIndex, onBack, chapters, chapterIndex, chapterPanelVod, currentVod, qualityPanelOpen, availableQualities, qualityFocusedIndex, currentQuality])
// playback:event — verarbeitet started / stopped / error
useEffect(() => {
const unsub = window.t4sd.playback.onEvent((ev: PlaybackEvent) => {
if (ev.kind === 'started') {
setPlayState('playing')
setVodDuration(ev.durationSeconds ?? 0)
setIsLivePlayback(ev.isLive ?? false)
setCurrentPosition(0)
} else if (ev.kind === 'stopped') {
const nextChapterVod = pendingChapterVod
setPlayState('idle')
setHlsPayload(null)
setChapterPanelVod(null)
setCurrentVod(null)
setAvailableQualities(undefined)
setCurrentQuality('best')
setQualityPanelOpen(false)
// Progress nach Stop neu laden
if (vods.length > 0) {
void window.t4sd.history
.getProgress(vods.map((v) => v.id))
.then(setProgressMap)
.catch(() => {})
}
if (nextChapterVod) {
setPendingChapterVod(null)
showChapterPanel(nextChapterVod)
} else {
setFocusRegion('shelf')
}
} else if (ev.kind === 'error') {
setPlayState('error')
setHlsPayload(null)
setChapterPanelVod(null)
setCurrentVod(null)
setPendingChapterVod(null)
setAvailableQualities(undefined)
setCurrentQuality('best')
setQualityPanelOpen(false)
setErrorMsg(ev.message ?? 'Unbekannter Fehler')
}
})
return unsub
}, [pendingChapterVod, vods])
// playback:hls-url — HLS-URL vom Main-Prozess empfangen und an VideoPlayer weiterleiten
useEffect(() => {
const unsub = window.t4sd.playback.onHlsUrl((payload: HlsUrlPayload) => {
setHlsPayload(payload)
})
return unsub
}, [])
useEffect(() => {
watchBtnRef.current?.focus()
}, [])
useEffect(() => {
if (!chapterListRef.current) return
const item = chapterListRef.current.children[chapterIndex] as HTMLElement | undefined
item?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
}, [chapterIndex])
const loadChapters = async (vodId: string): Promise<VodChapter[]> => {
return window.t4sd.twitch.getVodChapters(vodId)
}
const showChapterPanel = (vod: VodInfo): void => {
setChapterPanelVod(vod)
setChapters([])
setChapterIndex(0)
setChaptersLoading(true)
setFocusRegion('chapters')
void loadChapters(vod.id)
.then((loaded) => setChapters(loaded))
.catch(() => {})
.finally(() => setChaptersLoading(false))
}
const jumpChapter = async (direction: 1 | -1): Promise<void> => {
if (!currentVod) return
@@ -396,93 +369,27 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
}
const handleWatch = async (): Promise<void> => {
setPlayState('starting')
setErrorMsg('')
setAvailableQualities(undefined)
setCurrentQuality('best')
setQualityPanelOpen(false)
try {
await window.t4sd.playback.startLive(channel.broadcasterLogin)
} catch (e) {
setPlayState('error')
setErrorMsg(String(e))
return
}
window.t4sd.playback.getQualities(`twitch.tv/${channel.broadcasterLogin}`)
.then((qs) => setAvailableQualities(qs.length > 0 ? qs : []))
.catch(() => setAvailableQualities([]))
await startLive(channel.broadcasterLogin)
}
const handleWatchVod = async (vod: VodInfo, startSeconds?: number): Promise<void> => {
setPlayState('starting')
setErrorMsg('')
setFocusRegion('hero')
setChapterPanelVod(null)
setCurrentVod(vod)
setPendingChapterVod(null)
setVodChaptersLoaded(false)
setAvailableQualities(undefined)
setCurrentQuality('best')
setQualityPanelOpen(false)
void loadChapters(vod.id)
.then((loaded) => { setChapters(loaded); setVodChaptersLoaded(true) })
.catch(() => { setVodChaptersLoaded(true) })
try {
await window.t4sd.playback.startVod(
vod.id,
channel.broadcasterLogin,
vod.title,
vod.durationSeconds,
startSeconds
)
} catch (e) {
setPlayState('error')
setErrorMsg(String(e))
return
}
window.t4sd.playback.getQualities(`twitch.tv/videos/${vod.id}`)
.then((qs) => setAvailableQualities(qs.length > 0 ? qs : []))
.catch(() => setAvailableQualities([]))
}
const handleStop = async (): Promise<void> => {
videoRef.current?.stop()
setAvailableQualities(undefined)
setCurrentQuality('best')
setQualityPanelOpen(false)
await window.t4sd.playback.stop()
await startVod(vod.id, channel.broadcasterLogin, vod.title, vod.durationSeconds, startSeconds)
}
const handleQualityChange = async (quality: string): Promise<void> => {
setQualityPanelOpen(false)
setCurrentQuality(quality)
setAvailableQualities(undefined)
const currentPos = videoRef.current?.getCurrentTime() ?? 0
videoRef.current?.stop()
setHlsPayload(null)
setPlayState('starting')
try {
if (isLivePlayback) {
await window.t4sd.playback.startLive(channel.broadcasterLogin, quality)
window.t4sd.playback.getQualities(`twitch.tv/${channel.broadcasterLogin}`)
.then((qs) => setAvailableQualities(qs.length > 0 ? qs : []))
.catch(() => setAvailableQualities([]))
} else if (currentVod) {
await window.t4sd.playback.startVod(
currentVod.id,
channel.broadcasterLogin,
currentVod.title,
currentVod.durationSeconds,
currentPos,
quality
)
window.t4sd.playback.getQualities(`twitch.tv/videos/${currentVod.id}`)
.then((qs) => setAvailableQualities(qs.length > 0 ? qs : []))
.catch(() => setAvailableQualities([]))
}
} catch (e) {
setPlayState('error')
setErrorMsg(String(e))
if (isLivePlayback) {
await startLive(channel.broadcasterLogin, quality)
} else if (currentVod) {
await startVod(currentVod.id, channel.broadcasterLogin, currentVod.title, currentVod.durationSeconds, currentPos, quality)
}
}
@@ -733,12 +640,8 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
onTimeUpdate={(s) => setCurrentPosition(s)}
onPlaying={() => setPlayState('playing')}
onPaused={() => setPlayState('paused')}
onEnded={() => void handleStop()}
onError={(msg) => {
setPlayState('error')
setErrorMsg(msg)
setHlsPayload(null)
}}
onEnded={stop}
onError={handleVideoError}
/>
)}
@@ -782,7 +685,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
}}
onSeek={(s) => videoRef.current?.seek(s)}
onSeekTo={(s) => videoRef.current?.seekTo(s)}
onStop={() => void handleStop()}
onStop={stop}
/>
)}
</div>