Files
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

168 lines
5.1 KiB
TypeScript
Executable File

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<HTMLVideoElement>(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<Hls | undefined>();
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 (
<video
className="h-full w-full"
ref={videoRef}
playsInline
poster={cuedMedia?.poster}
{...htmlMediaEvents}
/>
);
}
const levelToPlaybackQuality = (level: any) => {
return level === -1 ? 'auto' : `${level.height}p`;
};