67
common/resources/client/player/providers/html-media/use-html-media-api.ts
Executable file
67
common/resources/client/player/providers/html-media/use-html-media-api.ts
Executable 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]
|
||||
);
|
||||
}
|
||||
125
common/resources/client/player/providers/html-media/use-html-media-events.ts
Executable file
125
common/resources/client/player/providers/html-media/use-html-media-events.ts
Executable 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,
|
||||
]);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user