first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
import {HtmlMediaInternalStateReturn} from '@common/player/providers/html-media/use-html-media-internal-state';
import {useContext, useMemo} from 'react';
import {PlayerStoreContext} from '@common/player/player-context';
import {PlayerProviderApi} from '@common/player/state/player-provider-api';
export function useHtmlMediaApi({
ref,
internalState,
toggleTextTrackModes,
}: HtmlMediaInternalStateReturn): PlayerProviderApi {
const store = useContext(PlayerStoreContext);
return useMemo(
() => ({
play: async () => {
try {
await ref.current?.play();
} catch (e) {
store.getState().emit('error', {sourceEvent: e});
}
internalState.current.timeRafLoop.start();
},
pause: () => {
ref.current?.pause();
internalState.current.timeRafLoop.stop();
},
stop: () => {
if (ref.current) {
ref.current.pause();
ref.current.currentTime = 0;
}
},
seek: (time: number) => {
if (time !== internalState.current.currentTime && ref.current) {
ref.current.currentTime = time;
}
},
setVolume: (volume: number) => {
if (ref.current) {
ref.current.volume = volume / 100;
}
},
setMuted: (muted: boolean) => {
if (ref.current) {
ref.current.muted = muted;
}
},
setPlaybackRate: (value: number) => {
if (ref.current) {
ref.current.playbackRate = value;
}
},
setTextTrackVisibility: isVisible => {
toggleTextTrackModes(store.getState().currentTextTrack, isVisible);
},
setCurrentTextTrack: newTrackId => {
toggleTextTrackModes(newTrackId, store.getState().textTrackIsVisible);
},
getCurrentTime: () => {
return internalState.current.currentTime;
},
getSrc: () => {
return ref.current?.src;
},
}),
[ref, store, internalState, toggleTextTrackModes]
);
}

View File

@@ -0,0 +1,125 @@
import {
HTMLAttributes,
useCallback,
useContext,
useEffect,
useMemo,
} from 'react';
import {PlayerStoreContext} from '@common/player/player-context';
import {HtmlMediaInternalStateReturn} from '@common/player/providers/html-media/use-html-media-internal-state';
const defaultPlaybackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
export function useHtmlMediaEvents({
ref,
updateCurrentTime,
updateBuffered,
internalState,
}: HtmlMediaInternalStateReturn): HTMLAttributes<HTMLMediaElement> {
const store = useContext(PlayerStoreContext);
const onTextTracksChange = useCallback(() => {
if (!ref.current) return;
const tracks = Array.from(ref.current.textTracks).filter(
t => t.label && (t.kind === 'subtitles' || t.kind === 'captions')
);
let trackId = -1;
for (let id = 0; id < tracks.length; id += 1) {
if (tracks[id].mode === 'hidden') {
// Do not break in case there is a following track with showing.
trackId = id;
} else if (tracks[id].mode === 'showing') {
trackId = id;
break;
}
}
const isVisible = trackId !== -1 && tracks[trackId].mode === 'showing';
store.getState().emit('currentTextTrackChange', {trackId});
store.getState().emit('textTrackVisibilityChange', {isVisible});
store.getState().emit('textTracks', {tracks});
}, [ref, store]);
useEffect(() => {
const el = ref.current;
return () => {
el?.textTracks.removeEventListener('change', onTextTracksChange);
};
}, [ref, onTextTracksChange]);
return useMemo(() => {
const emit = store.getState().emit;
return {
// set some common props used on audio/video/hls/dash providers
autoPlay: false,
onContextMenu: e => e.preventDefault(),
controlsList: 'nodownload',
preload: 'metadata',
'x-webkit-airplay': 'allow',
onEnded: () => {
emit('playbackEnd');
updateCurrentTime();
internalState.current.timeRafLoop.stop();
},
onStalled: e => {
if (e.currentTarget.readyState < 3) {
emit('buffering', {isBuffering: true});
}
},
onWaiting: () => {
emit('buffering', {isBuffering: true});
},
onPlaying: () => {
emit('play');
emit('buffering', {isBuffering: false});
},
onPause: e => {
emit('pause');
emit('buffering', {isBuffering: false});
internalState.current.timeRafLoop.stop();
},
onSuspend: () => {
emit('buffering', {isBuffering: false});
},
onSeeking: () => {
updateCurrentTime();
},
onSeeked: () => {
updateCurrentTime();
},
onTimeUpdate: () => {
updateCurrentTime();
},
onError: e => {
emit('error', {sourceEvent: e});
},
onDurationChange: e => {
updateCurrentTime();
emit('durationChange', {duration: e.currentTarget.duration});
},
onRateChange: e => {
emit('playbackRateChange', {rate: e.currentTarget.playbackRate});
},
onLoadedMetadata: e => {
if (!internalState.current.playbackReady) {
emit('providerReady', {el: e.currentTarget});
internalState.current.playbackReady = true;
updateBuffered();
onTextTracksChange();
e.currentTarget.textTracks.addEventListener('change', () => {
onTextTracksChange();
});
}
emit('cued');
emit('playbackRates', {rates: defaultPlaybackRates});
},
};
}, [
internalState,
store,
updateCurrentTime,
onTextTracksChange,
updateBuffered,
]);
}

View File

@@ -0,0 +1,121 @@
import {
MutableRefObject,
RefObject,
useCallback,
useContext,
useEffect,
useRef,
} from 'react';
import {createRafLoop} from '@common/utils/dom/create-ref-loop';
import {PlayerStoreContext} from '@common/player/player-context';
import {usePlayerStore} from '@common/player/hooks/use-player-store';
export interface HtmlMediaInternalStateReturn {
ref: RefObject<HTMLMediaElement>;
updateCurrentTime: () => void;
updateBuffered: () => void;
toggleTextTrackModes: (newTrackId: number, isVisible: boolean) => void;
internalState: MutableRefObject<{
currentTime: number;
playbackReady: boolean;
timeRafLoop: ReturnType<typeof createRafLoop>;
}>;
}
export function useHtmlMediaInternalState(
ref: RefObject<HTMLMediaElement>
): HtmlMediaInternalStateReturn {
const store = useContext(PlayerStoreContext);
const cuedMedia = usePlayerStore(s => s.cuedMedia);
const internalState = useRef({
currentTime: 0,
buffered: 0,
isMediaWaiting: false,
playbackReady: false,
/**
* The `timeupdate` event fires surprisingly infrequently during playback, meaning your progress
* bar (or whatever else is synced to the currentTime) moves in a choppy fashion. This helps
* resolve that by retrieving time updates in a request animation frame loop.
*/
timeRafLoop: createRafLoop(() => {
updateCurrentTime();
updateBuffered();
}),
});
const updateBuffered = useCallback(() => {
const timeRange = ref.current?.buffered;
const seconds =
!timeRange || timeRange.length === 0
? 0
: timeRange.end(timeRange.length - 1);
if (internalState.current.buffered !== seconds) {
store.getState().emit('buffered', {seconds});
internalState.current.buffered = seconds;
}
}, [ref, store]);
const updateCurrentTime = useCallback(() => {
const newTime = ref.current?.currentTime || 0;
if (
internalState.current.currentTime !== newTime &&
!store.getState().isSeeking
) {
store.getState().emit('progress', {currentTime: newTime});
internalState.current.currentTime = newTime;
}
}, [internalState, store, ref]);
const toggleTextTrackModes = useCallback(
(newTrackId: number, isVisible: boolean) => {
if (!ref.current) return;
const {textTracks} = ref.current;
if (newTrackId === -1) {
Array.from(textTracks).forEach(track => {
track.mode = 'disabled';
});
} else {
const oldTrack = textTracks[store.getState().currentTextTrack];
if (oldTrack) oldTrack.mode = 'disabled';
}
const nextTrack = textTracks[newTrackId];
if (nextTrack) {
nextTrack.mode = isVisible ? 'showing' : 'hidden';
}
store.getState().emit('currentTextTrackChange', {
trackId: !isVisible ? -1 : newTrackId,
});
store
.getState()
.emit('textTrackVisibilityChange', {isVisible: isVisible});
},
[ref, store]
);
// stop current time loop on unmount
useEffect(() => {
const timeRafLoop = internalState.current.timeRafLoop;
return () => {
timeRafLoop.stop();
};
}, []);
// reload metadata when new media is cued
useEffect(() => {
ref.current?.load();
}, [cuedMedia?.src, ref]);
return {
ref,
internalState,
updateCurrentTime,
toggleTextTrackModes,
updateBuffered,
};
}