Simple shaka

This commit is contained in:
Ivan Malison 2024-10-13 15:04:44 -06:00
parent dc61c3efea
commit 9191a06600

View File

@ -7,14 +7,14 @@ import React, {
type RefObject, type RefObject,
} from 'react'; } from 'react';
import shaka from 'shaka-player'; import shaka from 'shaka-player';
import type { VideoRef, ReactVideoProps, VideoMetadata } from './types'; import type {VideoRef, ReactVideoProps, VideoMetadata} from './types';
const Video = forwardRef<VideoRef, ReactVideoProps>( const Video = forwardRef<VideoRef, ReactVideoProps>(
( (
{ {
source, source,
paused, paused,
// muted, muted,
volume, volume,
rate, rate,
repeat, repeat,
@ -27,35 +27,32 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
onBuffer, onBuffer,
onLoad, onLoad,
onProgress, onProgress,
// onPlaybackRateChange, onPlaybackRateChange,
onError, onError,
onReadyForDisplay, onReadyForDisplay,
onSeek, onSeek,
// onVolumeChange, onVolumeChange,
onEnd, onEnd,
onPlaybackStateChanged,
}, },
ref, ref,
) => { ) => {
const nativeRef = useRef<HTMLVideoElement>(null); const nativeRef = useRef<HTMLVideoElement>(null);
const shakaPlayerRef = useRef<shaka.Player | null>(null); const shakaPlayerRef = useRef<shaka.Player | null>(null);
const isSeeking = useRef(false);
const seek = useCallback( const seek = useCallback(
(time: number, _tolerance?: number) => { async (time: number, _tolerance?: number) => {
if (isNaN(time)) { if (isNaN(time)) {
throw new Error('Specified time is not a number'); throw new Error('Specified time is not a number');
} }
if (!shakaPlayerRef.current) { if (!nativeRef.current) {
console.warn('Shaka Player is not initialized'); console.warn('Video Component is not mounted');
return; return;
} }
time = Math.max( time = Math.max(0, Math.min(time, nativeRef.current.duration));
0, nativeRef.current.currentTime = time;
Math.min(time, shakaPlayerRef.current.seekRange().end) onSeek?.({seekTime: time, currentTime: nativeRef.current.currentTime});
);
onSeek?.({
seekTime: time,
currentTime: nativeRef.current?.currentTime || 0,
});
}, },
[onSeek], [onSeek],
); );
@ -203,7 +200,26 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
setVolume(volume); setVolume(volume);
}, [volume, setVolume]); }, [volume, setVolume]);
// Handle playback rate changes // we use a ref to prevent triggerring the useEffect when the component rerender with a non-stable `onPlaybackStateChanged`.
const playbackStateRef = useRef(onPlaybackStateChanged);
playbackStateRef.current = onPlaybackStateChanged;
useEffect(() => {
// Not sure about how to do this but we want to wait for nativeRef to be initialized
setTimeout(() => {
if (!nativeRef.current) {
return;
}
// Set play state to the player's value (if autoplay is denied)
// This is useful if our UI is in a play state but autoplay got denied so
// the video is actaully in a paused state.
playbackStateRef.current?.({
isPlaying: !nativeRef.current.paused,
isSeeking: isSeeking.current,
});
}, 500);
}, []);
useEffect(() => { useEffect(() => {
if (!nativeRef.current || rate === undefined) { if (!nativeRef.current || rate === undefined) {
return; return;
@ -211,19 +227,15 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
nativeRef.current.playbackRate = rate; nativeRef.current.playbackRate = rate;
}, [rate]); }, [rate]);
// Initialize Shaka Player
useEffect(() => { useEffect(() => {
if (!nativeRef.current) { if (!nativeRef.current || rate === undefined) {
console.warn('Video component is not mounted');
return; return;
} }
if (shakaPlayerRef.current) {
// Initialize Shaka Player shakaPlayerRef.current.unload()
const player = new shaka.Player(nativeRef.current); }
shakaPlayerRef.current = player; shakaPlayerRef.current = new shaka.Player();
shakaPlayerRef.current.addEventListener("error", (event) => {
// Error handling
player.addEventListener('error', (event) => {
//@ts-ignore //@ts-ignore
const shakaError = event.detail; const shakaError = event.detail;
console.error('Shaka Player Error', shakaError); console.error('Shaka Player Error', shakaError);
@ -234,76 +246,54 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
}, },
}); });
}); });
shakaPlayerRef.current.attach(nativeRef.current, true);
// Buffering events if (source) {
player.addEventListener('buffering', (event) => {
//@ts-ignore //@ts-ignore
onBuffer?.({ isBuffering: event.buffering }); shakaPlayerRef.current.load(source?.uri).then(
}); () => console.log(`${source?.uri} finished loading`)
);
// player.attach(mediaElement)
// Load the video source
player
//@ts-ignore
.load(source?.uri)
.then(() => {
// Media loaded successfully
if (!nativeRef.current) return;
const duration = nativeRef.current.duration;
const naturalSize = {
width: nativeRef.current.videoWidth,
height: nativeRef.current.videoHeight,
orientation:
nativeRef.current.videoWidth > nativeRef.current.videoHeight
? 'landscape'
: 'portrait',
};
onLoad?.({
currentTime: nativeRef.current.currentTime,
duration,
//@ts-ignore
naturalSize,
//@ts-ignore
videoTracks: player.getVariantTracks(),
//@ts-ignore
audioTracks: player.getVariantTracks(),
//@ts-ignore
textTracks: player.getTextTracks(),
});
onReadyForDisplay?.();
})
.catch((error: any) => {
console.error('Error loading video', error);
onError?.({ error });
});
return () => {
// Cleanup
if (shakaPlayerRef.current) {
shakaPlayerRef.current.destroy();
shakaPlayerRef.current = null;
} }
}; }, [source])
}, [source?.uri]);
// Handle Media Session (if implemented)
useMediaSession(source?.metadata, nativeRef, showNotificationControls); useMediaSession(source?.metadata, nativeRef, showNotificationControls);
return ( return (
<video <video
ref={nativeRef} ref={nativeRef}
muted={true} muted={muted}
autoPlay={!paused}
controls={controls} controls={controls}
loop={repeat} loop={repeat}
playsInline playsInline
//@ts-ignore //@ts-ignore
poster={poster} poster={poster}
onCanPlay={() => onBuffer?.({ isBuffering: false })} onCanPlay={() => onBuffer?.({isBuffering: false})}
onWaiting={() => onBuffer?.({ isBuffering: true })} onWaiting={() => onBuffer?.({isBuffering: true})}
onRateChange={() => {
if (!nativeRef.current) {
return;
}
onPlaybackRateChange?.({
playbackRate: nativeRef.current?.playbackRate,
});
}}
onDurationChange={() => {
if (!nativeRef.current) {
return;
}
onLoad?.({
currentTime: nativeRef.current.currentTime,
duration: nativeRef.current.duration,
videoTracks: [],
textTracks: [],
audioTracks: [],
naturalSize: {
width: nativeRef.current.videoWidth,
height: nativeRef.current.videoHeight,
orientation: 'landscape',
},
});
}}
onTimeUpdate={() => { onTimeUpdate={() => {
if (!nativeRef.current) { if (!nativeRef.current) {
return; return;
@ -312,33 +302,54 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
currentTime: nativeRef.current.currentTime, currentTime: nativeRef.current.currentTime,
playableDuration: nativeRef.current.buffered.length playableDuration: nativeRef.current.buffered.length
? nativeRef.current.buffered.end( ? nativeRef.current.buffered.end(
nativeRef.current.buffered.length - 1 nativeRef.current.buffered.length - 1,
)
: 0,
seekableDuration: nativeRef.current.seekable.length
? nativeRef.current.seekable.end(
nativeRef.current.seekable.length - 1
) )
: 0, : 0,
seekableDuration: 0,
}); });
}} }}
onEnded={onEnd} onLoadedData={() => onReadyForDisplay?.()}
onError={() => { onError={() => {
if (!nativeRef.current?.error) { if (!nativeRef.current?.error) {
return; return;
} }
onError?.({ onError?.({
error: { error: {
errorString: errorString: nativeRef.current.error.message ?? 'Unknown error',
nativeRef.current.error.message || 'Unknown error',
code: nativeRef.current.error.code, code: nativeRef.current.error.code,
}, },
}); });
}} }}
onLoadedMetadata={() => {
if (source?.startPosition) {
seek(source.startPosition / 1000);
}
}}
onPlay={() =>
onPlaybackStateChanged?.({
isPlaying: true,
isSeeking: isSeeking.current,
})
}
onPause={() =>
onPlaybackStateChanged?.({
isPlaying: false,
isSeeking: isSeeking.current,
})
}
onSeeking={() => (isSeeking.current = true)}
onSeeked={() => (isSeeking.current = false)}
onVolumeChange={() => {
if (!nativeRef.current) {
return;
}
onVolumeChange?.({volume: nativeRef.current.volume});
}}
onEnded={onEnd}
style={videoStyle} style={videoStyle}
/> />
); );
} },
); );
const videoStyle = { const videoStyle = {