Add media session support
This commit is contained in:
		| @@ -4,8 +4,10 @@ import React, { | |||||||
|   useEffect, |   useEffect, | ||||||
|   useImperativeHandle, |   useImperativeHandle, | ||||||
|   useRef, |   useRef, | ||||||
|  |   type RefObject, | ||||||
|  |   useMemo, | ||||||
| } from 'react'; | } from 'react'; | ||||||
| import type {VideoRef, ReactVideoProps} from './types'; | import type {VideoRef, ReactVideoProps, VideoMetadata} from './types'; | ||||||
|  |  | ||||||
| const Video = forwardRef<VideoRef, ReactVideoProps>( | const Video = forwardRef<VideoRef, ReactVideoProps>( | ||||||
|   ( |   ( | ||||||
| @@ -17,7 +19,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|       rate, |       rate, | ||||||
|       repeat, |       repeat, | ||||||
|       controls, |       controls, | ||||||
|       showNotificationControls, |       showNotificationControls = false, | ||||||
|       poster, |       poster, | ||||||
|       onBuffer, |       onBuffer, | ||||||
|       onLoad, |       onLoad, | ||||||
| @@ -45,6 +47,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|           console.warn('Video Component is not mounted'); |           console.warn('Video Component is not mounted'); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|  |         time = Math.max(0, Math.min(time, nativeRef.current.duration)); | ||||||
|         nativeRef.current.currentTime = time; |         nativeRef.current.currentTime = time; | ||||||
|         onSeek?.({seekTime: time, currentTime: nativeRef.current.currentTime}); |         onSeek?.({seekTime: time, currentTime: nativeRef.current.currentTime}); | ||||||
|       }, |       }, | ||||||
| @@ -98,8 +101,17 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|         setFullScreen: unsupported, |         setFullScreen: unsupported, | ||||||
|         save: unsupported, |         save: unsupported, | ||||||
|         restoreUserInterfaceForPictureInPictureStopCompleted: unsupported, |         restoreUserInterfaceForPictureInPictureStopCompleted: unsupported, | ||||||
|  |         nativeHtmlRef: nativeRef, | ||||||
|       }), |       }), | ||||||
|       [seek, pause, resume, unsupported, setVolume, getCurrentPosition], |       [ | ||||||
|  |         seek, | ||||||
|  |         pause, | ||||||
|  |         resume, | ||||||
|  |         unsupported, | ||||||
|  |         setVolume, | ||||||
|  |         getCurrentPosition, | ||||||
|  |         nativeRef, | ||||||
|  |       ], | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
| @@ -137,14 +149,24 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|       nativeRef.current.playbackRate = rate; |       nativeRef.current.playbackRate = rate; | ||||||
|     }, [rate]); |     }, [rate]); | ||||||
|  |  | ||||||
|  |     useMediaSession( | ||||||
|  |       source?.metadata, | ||||||
|  |       nativeRef, | ||||||
|  |       useMemo( | ||||||
|  |         () => ({ | ||||||
|  |           seek, | ||||||
|  |           resume, | ||||||
|  |           pause, | ||||||
|  |         }), | ||||||
|  |         [seek, resume, pause], | ||||||
|  |       ), | ||||||
|  |       showNotificationControls, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <> |  | ||||||
|         {showNotificationControls && ( |  | ||||||
|           <MediaSessionManager {...source.metadata} /> |  | ||||||
|         )} |  | ||||||
|       <video |       <video | ||||||
|         ref={nativeRef} |         ref={nativeRef} | ||||||
|           src={source.uri as string | undefined} |         src={source?.uri as string | undefined} | ||||||
|         muted={muted} |         muted={muted} | ||||||
|         autoPlay={!paused} |         autoPlay={!paused} | ||||||
|         controls={controls} |         controls={controls} | ||||||
| @@ -194,22 +216,20 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|         }} |         }} | ||||||
|         onLoadedData={() => onReadyForDisplay?.()} |         onLoadedData={() => onReadyForDisplay?.()} | ||||||
|         onError={() => { |         onError={() => { | ||||||
|             if ( |           if (!nativeRef.current?.error) { | ||||||
|               nativeRef?.current?.error?.code === |             return; | ||||||
|               MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED |           } | ||||||
|             ) |           onError?.({ | ||||||
|               onMediaUnsupported?.call(undefined); |  | ||||||
|             else { |  | ||||||
|               onError?.call(null, { |  | ||||||
|             error: { |             error: { | ||||||
|                   errorString: |               errorString: nativeRef.current.error.message ?? 'Unknown error', | ||||||
|                     nativeRef.current?.error?.message ?? 'Unknown error', |               code: nativeRef.current.error.code, | ||||||
|             }, |             }, | ||||||
|           }); |           }); | ||||||
|             } |  | ||||||
|         }} |         }} | ||||||
|         onLoadedMetadata={() => { |         onLoadedMetadata={() => { | ||||||
|             if (source.startPosition) seek(source.startPosition / 1000); |           if (source?.startPosition) { | ||||||
|  |             seek(source.startPosition / 1000); | ||||||
|  |           } | ||||||
|         }} |         }} | ||||||
|         onPlay={() => onPlaybackStateChanged?.({isPlaying: true})} |         onPlay={() => onPlaybackStateChanged?.({isPlaying: true})} | ||||||
|         onPause={() => onPlaybackStateChanged?.({isPlaying: false})} |         onPause={() => onPlaybackStateChanged?.({isPlaying: false})} | ||||||
| @@ -222,10 +242,95 @@ const Video = forwardRef<VideoRef, ReactVideoProps>( | |||||||
|         onEnded={onEnd} |         onEnded={onEnd} | ||||||
|         style={{position: 'absolute', inset: 0, objectFit: 'contain'}} |         style={{position: 'absolute', inset: 0, objectFit: 'contain'}} | ||||||
|       /> |       /> | ||||||
|       </> |  | ||||||
|     ); |     ); | ||||||
|   }, |   }, | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | const useMediaSession = ( | ||||||
|  |   metadata: VideoMetadata | undefined, | ||||||
|  |   nativeRef: RefObject<HTMLVideoElement>, | ||||||
|  |   actions: { | ||||||
|  |     seek: (time: number) => void; | ||||||
|  |     pause: () => void; | ||||||
|  |     resume: () => void; | ||||||
|  |   }, | ||||||
|  |   showNotification: boolean, | ||||||
|  | ) => { | ||||||
|  |   const isPlaying = !nativeRef.current?.paused ?? false; | ||||||
|  |   const progress = nativeRef.current?.currentTime ?? 0; | ||||||
|  |   const duration = nativeRef.current?.duration ?? 100; | ||||||
|  |   const playbackRate = nativeRef.current?.playbackRate ?? 1; | ||||||
|  |  | ||||||
|  |   const enabled = 'mediaSession' in navigator && showNotification; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (enabled) { | ||||||
|  |       navigator.mediaSession.metadata = new MediaMetadata({ | ||||||
|  |         title: metadata?.title, | ||||||
|  |         artist: metadata?.artist, | ||||||
|  |         artwork: metadata?.imageUri ? [{src: metadata.imageUri}] : undefined, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, [enabled, metadata]); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!enabled) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const seekRelative = (offset: number) => { | ||||||
|  |       if (!nativeRef.current) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       actions.seek(nativeRef.current.currentTime + offset); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const mediaActions: [ | ||||||
|  |       MediaSessionAction, | ||||||
|  |       MediaSessionActionHandler | null, | ||||||
|  |     ][] = [ | ||||||
|  |       ['play', () => actions.resume()], | ||||||
|  |       ['pause', () => actions.pause()], | ||||||
|  |       [ | ||||||
|  |         'seekbackward', | ||||||
|  |         (evt: MediaSessionActionDetails) => | ||||||
|  |           seekRelative(evt.seekOffset ? -evt.seekOffset : -10), | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         'seekforward', | ||||||
|  |         (evt: MediaSessionActionDetails) => | ||||||
|  |           seekRelative(evt.seekOffset ? evt.seekOffset : 10), | ||||||
|  |       ], | ||||||
|  |       [ | ||||||
|  |         'seekto', | ||||||
|  |         (evt: MediaSessionActionDetails) => actions.seek(evt.seekTime!), | ||||||
|  |       ], | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     for (const [action, handler] of mediaActions) { | ||||||
|  |       try { | ||||||
|  |         navigator.mediaSession.setActionHandler(action, handler); | ||||||
|  |       } catch { | ||||||
|  |         // ignored | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, [enabled, nativeRef, actions]); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (enabled) { | ||||||
|  |       navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused'; | ||||||
|  |     } | ||||||
|  |   }, [isPlaying, enabled]); | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (enabled && duration !== undefined) { | ||||||
|  |       navigator.mediaSession.setPositionState({ | ||||||
|  |         position: Math.min(progress, duration), | ||||||
|  |         duration, | ||||||
|  |         playbackRate: playbackRate, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, [progress, duration, playbackRate, enabled]); | ||||||
|  | }; | ||||||
|  |  | ||||||
| Video.displayName = 'Video'; | Video.displayName = 'Video'; | ||||||
| export default Video; | export default Video; | ||||||
|   | |||||||
| @@ -264,12 +264,12 @@ type OnReceiveAdEventData = Readonly<{ | |||||||
|  |  | ||||||
| export type OnVideoErrorData = Readonly<{ | export type OnVideoErrorData = Readonly<{ | ||||||
|   error: Readonly<{ |   error: Readonly<{ | ||||||
|     errorString?: string; // android |     errorString?: string; // android | web | ||||||
|     errorException?: string; // android |     errorException?: string; // android | ||||||
|     errorStackTrace?: string; // android |     errorStackTrace?: string; // android | ||||||
|     errorCode?: string; // android |     errorCode?: string; // android | ||||||
|     error?: string; // ios |     error?: string; // ios | ||||||
|     code?: Int32; // ios |     code?: Int32; // ios | web | ||||||
|     localizedDescription?: string; // ios |     localizedDescription?: string; // ios | ||||||
|     localizedFailureReason?: string; // ios |     localizedFailureReason?: string; // ios | ||||||
|     localizedRecoverySuggestion?: string; // ios |     localizedRecoverySuggestion?: string; // ios | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | import type {RefObject} from 'react'; | ||||||
|  |  | ||||||
| export type VideoSaveData = { | export type VideoSaveData = { | ||||||
|   uri: string; |   uri: string; | ||||||
| }; | }; | ||||||
| @@ -15,4 +17,5 @@ export interface VideoRef { | |||||||
|   setVolume: (volume: number) => void; |   setVolume: (volume: number) => void; | ||||||
|   getCurrentPosition: () => Promise<number>; |   getCurrentPosition: () => Promise<number>; | ||||||
|   setFullScreen: (fullScreen: boolean) => void; |   setFullScreen: (fullScreen: boolean) => void; | ||||||
|  |   nativeHtmlRef?: RefObject<HTMLVideoElement>; // web only | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user