diff --git a/package.json b/package.json index 900ffd3c..4fa7ff3b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "release-it": "^16.2.1", "typescript": "5.1.6" }, - "dependencies": {}, + "dependencies": { + "shaka-player": "^4.11.7" + }, "peerDependencies": { "react": "*", "react-native": "*" diff --git a/src/Video.web.tsx b/src/Video.web.tsx index ca37e049..f8a9c8b6 100644 --- a/src/Video.web.tsx +++ b/src/Video.web.tsx @@ -6,7 +6,8 @@ import React, { useRef, type RefObject, } from 'react'; -import type {VideoRef, ReactVideoProps, VideoMetadata} from './types'; +import shaka from 'shaka-player/dist/shaka-player.compiled.js'; +import type { VideoRef, ReactVideoProps, VideoMetadata } from './types'; const Video = forwardRef( ( @@ -37,20 +38,28 @@ const Video = forwardRef( ref, ) => { const nativeRef = useRef(null); + const shakaPlayerRef = useRef(null); const isSeeking = useRef(false); + const seek = useCallback( - async (time: number, _tolerance?: number) => { + (time: number, _tolerance?: number) => { if (isNaN(time)) { throw new Error('Specified time is not a number'); } - if (!nativeRef.current) { - console.warn('Video Component is not mounted'); + if (!shakaPlayerRef.current) { + console.warn('Shaka Player is not initialized'); return; } - time = Math.max(0, Math.min(time, nativeRef.current.duration)); - nativeRef.current.currentTime = time; - onSeek?.({seekTime: time, currentTime: nativeRef.current.currentTime}); + time = Math.max( + 0, + Math.min(time, shakaPlayerRef.current.seekRange().end) + ); + shakaPlayerRef.current.seek(time); + onSeek?.({ + seekTime: time, + currentTime: nativeRef.current?.currentTime || 0, + }); }, [onSeek], ); @@ -198,26 +207,7 @@ const Video = forwardRef( setVolume(volume); }, [volume, setVolume]); - // 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); - }, []); - + // Handle playback rate changes useEffect(() => { if (!nativeRef.current || rate === undefined) { return; @@ -225,46 +215,90 @@ const Video = forwardRef( nativeRef.current.playbackRate = rate; }, [rate]); + // Initialize Shaka Player + useEffect(() => { + if (!nativeRef.current) { + console.warn('Video component is not mounted'); + return; + } + + // Initialize Shaka Player + const player = new shaka.Player(nativeRef.current); + shakaPlayerRef.current = player; + + // Error handling + player.addEventListener('error', (event) => { + const shakaError = event.detail; + console.error('Shaka Player Error', shakaError); + onError?.({ + error: { + errorString: shakaError.message, + code: shakaError.code, + }, + }); + }); + + // Buffering events + player.addEventListener('buffering', (event) => { + onBuffer?.({ isBuffering: event.buffering }); + }); + + // Load the video source + player + .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, + naturalSize, + videoTracks: player.getVariantTracks(), + audioTracks: player.getVariantTracks(), + textTracks: player.getTextTracks(), + }); + + onReadyForDisplay?.(); + }) + .catch((error) => { + console.error('Error loading video', error); + onError?.({ error }); + }); + + return () => { + // Cleanup + if (shakaPlayerRef.current) { + shakaPlayerRef.current.destroy(); + shakaPlayerRef.current = null; + } + }; + }, [source?.uri]); + + // Handle Media Session (if implemented) useMediaSession(source?.metadata, nativeRef, showNotificationControls); return (