import {useCallback, useContext, useEffect, useRef, useState} from 'react'; import {PlayerStoreContext} from '@common/player/player-context'; import {usePlayerStore} from '@common/player/hooks/use-player-store'; import Hls, {LevelLoadedData} from 'hls.js'; import {useHtmlMediaInternalState} from '@common/player/providers/html-media/use-html-media-internal-state'; import {useHtmlMediaEvents} from '@common/player/providers/html-media/use-html-media-events'; import {useHtmlMediaApi} from '@common/player/providers/html-media/use-html-media-api'; import {HlsMediaItem} from '@common/player/media-item'; import {AudioTrack} from '@common/player/state/player-state'; export default function HlsProvider() { const store = useContext(PlayerStoreContext); const cuedMedia = usePlayerStore(s => s.cuedMedia) as HlsMediaItem; // html medial element state const videoRef = useRef(null!); const htmlMediaState = useHtmlMediaInternalState(videoRef); const htmlMediaEvents = useHtmlMediaEvents(htmlMediaState); const htmlMediaApi = useHtmlMediaApi(htmlMediaState); // need both so we can "loadSource" when hls is ready, while keeping other callbacks stable const hls = useRef(); const [hlsReady, setHlsReady] = useState(false); const destroyHls = useCallback(() => { if (hls) { hls.current?.destroy(); hls.current = undefined; setHlsReady(false); } }, []); const setupHls = useCallback(() => { if (!Hls.isSupported()) { store.getState().emit('error', {fatal: true}); return; } const hlsInstance = new Hls({ startLevel: -1, }); hlsInstance.on(Hls.Events.ERROR, (event: any, data: any) => { if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: hlsInstance.startLoad(); break; case Hls.ErrorTypes.MEDIA_ERROR: hlsInstance.recoverMediaError(); break; default: destroyHls(); break; } } store.getState().emit('error', {sourceEvent: data, fatal: data.fatal}); }); hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => { if (!hlsInstance.levels?.length) return; store.getState().emit('playbackQualities', { qualities: ['auto', ...hlsInstance.levels.map(levelToPlaybackQuality)], }); store.getState().emit('playbackQualityChange', {quality: 'auto'}); }); hlsInstance.on(Hls.Events.AUDIO_TRACK_SWITCHED, (eventType, data) => { const track = store.getState().audioTracks.find(t => t.id === data.id); if (track) { store.getState().emit('currentAudioTrackChange', {trackId: track.id}); } }); hlsInstance.on( Hls.Events.LEVEL_LOADED, (eventType: string, data: LevelLoadedData) => { if (!store.getState().providerReady) { const {type, live, totalduration: duration} = data.details; const inferredStreamType = live ? type === 'EVENT' && Number.isFinite(duration) ? 'live:dvr' : 'live' : 'on-demand'; store.getState().emit('streamTypeChange', { streamType: (store.getState().cuedMedia as HlsMediaItem)?.streamType || inferredStreamType, }); store.getState().emit('durationChange', {duration}); const audioTracks: AudioTrack[] = hlsInstance.audioTracks.map( track => ({ id: track.id, label: track.name, language: track.lang || '', kind: 'main', }) ); store.getState().emit('audioTracks', {tracks: audioTracks}); } } ); hlsInstance.attachMedia(videoRef.current); hls.current = hlsInstance; // trigger initial load source setHlsReady(true); }, [destroyHls, store]); // setup and destroy hls on mount and unmount useEffect(() => { setupHls(); return () => { destroyHls(); }; }, [setupHls, destroyHls]); // load source via hls when media src changes and hls is ready useEffect(() => { if ( hls.current && cuedMedia?.src && (hls.current as any).url !== cuedMedia?.src ) { hls.current.loadSource(cuedMedia.src); } }, [cuedMedia?.src, hlsReady]); useEffect(() => { if (!hlsReady) return; store.setState({ providerApi: { ...htmlMediaApi, setCurrentAudioTrack: (trackId: number) => { if (!hls.current) return; hls.current.audioTrack = trackId; }, setPlaybackQuality: (quality: string) => { if (!hls.current) return; hls.current.currentLevel = hls.current.levels.findIndex( level => levelToPlaybackQuality(level) === quality ); store.getState().emit('playbackQualityChange', {quality}); }, }, }); }, [htmlMediaApi, store, hlsReady]); return (