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,95 +149,188 @@ 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 (
 | 
				
			||||||
      <>
 | 
					      <video
 | 
				
			||||||
        {showNotificationControls && (
 | 
					        ref={nativeRef}
 | 
				
			||||||
          <MediaSessionManager {...source.metadata} />
 | 
					        src={source?.uri as string | undefined}
 | 
				
			||||||
        )}
 | 
					        muted={muted}
 | 
				
			||||||
        <video
 | 
					        autoPlay={!paused}
 | 
				
			||||||
          ref={nativeRef}
 | 
					        controls={controls}
 | 
				
			||||||
          src={source.uri as string | undefined}
 | 
					        loop={repeat}
 | 
				
			||||||
          muted={muted}
 | 
					        playsInline
 | 
				
			||||||
          autoPlay={!paused}
 | 
					        poster={poster}
 | 
				
			||||||
          controls={controls}
 | 
					        onCanPlay={() => onBuffer?.({isBuffering: false})}
 | 
				
			||||||
          loop={repeat}
 | 
					        onWaiting={() => onBuffer?.({isBuffering: true})}
 | 
				
			||||||
          playsInline
 | 
					        onRateChange={() => {
 | 
				
			||||||
          poster={poster}
 | 
					          if (!nativeRef.current) {
 | 
				
			||||||
          onCanPlay={() => onBuffer?.({isBuffering: false})}
 | 
					            return;
 | 
				
			||||||
          onWaiting={() => onBuffer?.({isBuffering: true})}
 | 
					          }
 | 
				
			||||||
          onRateChange={() => {
 | 
					          onPlaybackRateChange?.({
 | 
				
			||||||
            if (!nativeRef.current) {
 | 
					            playbackRate: nativeRef.current?.playbackRate,
 | 
				
			||||||
              return;
 | 
					          });
 | 
				
			||||||
            }
 | 
					        }}
 | 
				
			||||||
            onPlaybackRateChange?.({
 | 
					        onDurationChange={() => {
 | 
				
			||||||
              playbackRate: nativeRef.current?.playbackRate,
 | 
					          if (!nativeRef.current) {
 | 
				
			||||||
            });
 | 
					            return;
 | 
				
			||||||
          }}
 | 
					          }
 | 
				
			||||||
          onDurationChange={() => {
 | 
					          onLoad?.({
 | 
				
			||||||
            if (!nativeRef.current) {
 | 
					            currentTime: nativeRef.current.currentTime,
 | 
				
			||||||
              return;
 | 
					            duration: nativeRef.current.duration,
 | 
				
			||||||
            }
 | 
					            videoTracks: [],
 | 
				
			||||||
            onLoad?.({
 | 
					            textTracks: [],
 | 
				
			||||||
              currentTime: nativeRef.current.currentTime,
 | 
					            audioTracks: [],
 | 
				
			||||||
              duration: nativeRef.current.duration,
 | 
					            naturalSize: {
 | 
				
			||||||
              videoTracks: [],
 | 
					              width: nativeRef.current.videoWidth,
 | 
				
			||||||
              textTracks: [],
 | 
					              height: nativeRef.current.videoHeight,
 | 
				
			||||||
              audioTracks: [],
 | 
					              orientation: 'landscape',
 | 
				
			||||||
              naturalSize: {
 | 
					            },
 | 
				
			||||||
                width: nativeRef.current.videoWidth,
 | 
					          });
 | 
				
			||||||
                height: nativeRef.current.videoHeight,
 | 
					        }}
 | 
				
			||||||
                orientation: 'landscape',
 | 
					        onTimeUpdate={() => {
 | 
				
			||||||
              },
 | 
					          if (!nativeRef.current) {
 | 
				
			||||||
            });
 | 
					            return;
 | 
				
			||||||
          }}
 | 
					          }
 | 
				
			||||||
          onTimeUpdate={() => {
 | 
					          onProgress?.({
 | 
				
			||||||
            if (!nativeRef.current) {
 | 
					            currentTime: nativeRef.current.currentTime,
 | 
				
			||||||
              return;
 | 
					            playableDuration: nativeRef.current.buffered.length
 | 
				
			||||||
            }
 | 
					              ? nativeRef.current.buffered.end(
 | 
				
			||||||
            onProgress?.({
 | 
					                  nativeRef.current.buffered.length - 1,
 | 
				
			||||||
              currentTime: nativeRef.current.currentTime,
 | 
					                )
 | 
				
			||||||
              playableDuration: nativeRef.current.buffered.length
 | 
					              : 0,
 | 
				
			||||||
                ? nativeRef.current.buffered.end(
 | 
					            seekableDuration: 0,
 | 
				
			||||||
                    nativeRef.current.buffered.length - 1,
 | 
					          });
 | 
				
			||||||
                  )
 | 
					        }}
 | 
				
			||||||
                : 0,
 | 
					        onLoadedData={() => onReadyForDisplay?.()}
 | 
				
			||||||
              seekableDuration: 0,
 | 
					        onError={() => {
 | 
				
			||||||
            });
 | 
					          if (!nativeRef.current?.error) {
 | 
				
			||||||
          }}
 | 
					            return;
 | 
				
			||||||
          onLoadedData={() => onReadyForDisplay?.()}
 | 
					          }
 | 
				
			||||||
          onError={() => {
 | 
					          onError?.({
 | 
				
			||||||
            if (
 | 
					            error: {
 | 
				
			||||||
              nativeRef?.current?.error?.code ===
 | 
					              errorString: nativeRef.current.error.message ?? 'Unknown error',
 | 
				
			||||||
              MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
 | 
					              code: nativeRef.current.error.code,
 | 
				
			||||||
            )
 | 
					            },
 | 
				
			||||||
              onMediaUnsupported?.call(undefined);
 | 
					          });
 | 
				
			||||||
            else {
 | 
					        }}
 | 
				
			||||||
              onError?.call(null, {
 | 
					        onLoadedMetadata={() => {
 | 
				
			||||||
                error: {
 | 
					          if (source?.startPosition) {
 | 
				
			||||||
                  errorString:
 | 
					            seek(source.startPosition / 1000);
 | 
				
			||||||
                    nativeRef.current?.error?.message ?? 'Unknown error',
 | 
					          }
 | 
				
			||||||
                },
 | 
					        }}
 | 
				
			||||||
              });
 | 
					        onPlay={() => onPlaybackStateChanged?.({isPlaying: true})}
 | 
				
			||||||
            }
 | 
					        onPause={() => onPlaybackStateChanged?.({isPlaying: false})}
 | 
				
			||||||
          }}
 | 
					        onVolumeChange={() => {
 | 
				
			||||||
          onLoadedMetadata={() => {
 | 
					          if (!nativeRef.current) {
 | 
				
			||||||
            if (source.startPosition) seek(source.startPosition / 1000);
 | 
					            return;
 | 
				
			||||||
          }}
 | 
					          }
 | 
				
			||||||
          onPlay={() => onPlaybackStateChanged?.({isPlaying: true})}
 | 
					          onVolumeChange?.({volume: nativeRef.current.volume});
 | 
				
			||||||
          onPause={() => onPlaybackStateChanged?.({isPlaying: false})}
 | 
					        }}
 | 
				
			||||||
          onVolumeChange={() => {
 | 
					        onEnded={onEnd}
 | 
				
			||||||
            if (!nativeRef.current) {
 | 
					        style={{position: 'absolute', inset: 0, objectFit: 'contain'}}
 | 
				
			||||||
              return;
 | 
					      />
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            onVolumeChange?.({volume: nativeRef.current.volume});
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
          onEnded={onEnd}
 | 
					 | 
				
			||||||
          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