import React, { useState, useCallback, useMemo, useRef, forwardRef, useImperativeHandle, type ComponentRef, } from 'react'; import {View, StyleSheet, Image, Platform} from 'react-native'; import NativeVideoComponent, { type VideoComponentType, } from './VideoNativeComponent'; import type {StyleProp, ImageStyle, NativeSyntheticEvent} from 'react-native'; import {getReactTag, resolveAssetSourceForVideo} from './utils'; import {VideoManager} from './VideoNativeComponent'; import type { OnAudioFocusChangedData, OnAudioTracksData, OnBandwidthUpdateData, OnBufferData, OnExternalPlaybackChangeData, OnGetLicenseData, OnLoadData, OnLoadStartData, OnPictureInPictureStatusChangedData, OnPlaybackStateChangedData, OnProgressData, OnReceiveAdEventData, OnSeekData, OnTextTrackDataChangedData, OnTextTracksData, OnTimedMetadataData, OnVideoAspectRatioData, OnVideoErrorData, OnVideoTracksData, ReactVideoProps, } from './types'; export type VideoSaveData = { uri: string; }; export interface VideoRef { seek: (time: number, tolerance?: number) => void; resume: () => void; pause: () => void; presentFullscreenPlayer: () => void; dismissFullscreenPlayer: () => void; restoreUserInterfaceForPictureInPictureStopCompleted: ( restore: boolean, ) => void; save: (options: object) => Promise; } const Video = forwardRef( ( { source, style, resizeMode, posterResizeMode, poster, fullscreen, drm, textTracks, selectedVideoTrack, selectedAudioTrack, selectedTextTrack, onLoadStart, onLoad, onError, onProgress, onSeek, onEnd, onBuffer, onBandwidthUpdate, onExternalPlaybackChange, onFullscreenPlayerWillPresent, onFullscreenPlayerDidPresent, onFullscreenPlayerWillDismiss, onFullscreenPlayerDidDismiss, onReadyForDisplay, onPlaybackRateChange, onVolumeChange, onAudioBecomingNoisy, onPictureInPictureStatusChanged, onRestoreUserInterfaceForPictureInPictureStop, onReceiveAdEvent, onPlaybackStateChanged, onAudioFocusChanged, onIdle, onTimedMetadata, onAudioTracks, onTextTracks, onTextTrackDataChanged, onVideoTracks, onAspectRatio, ...rest }, ref, ) => { const nativeRef = useRef>(null); const [showPoster, setShowPoster] = useState(!!poster); const [isFullscreen, setIsFullscreen] = useState(fullscreen); const [ _restoreUserInterfaceForPIPStopCompletionHandler, setRestoreUserInterfaceForPIPStopCompletionHandler, ] = useState(); const posterStyle = useMemo>( () => ({ ...StyleSheet.absoluteFillObject, resizeMode: posterResizeMode && posterResizeMode !== 'none' ? posterResizeMode : 'contain', }), [posterResizeMode], ); const src = useMemo(() => { if (!source) { return undefined; } const resolvedSource = resolveAssetSourceForVideo(source); let uri = resolvedSource.uri || ''; if (uri && uri.match(/^\//)) { uri = `file://${uri}`; } if (!uri) { console.log('Trying to load empty source'); } const isNetwork = !!(uri && uri.match(/^https?:/)); const isAsset = !!( uri && uri.match( /^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/, ) ); return { uri, isNetwork, isAsset, shouldCache: resolvedSource.shouldCache || false, type: resolvedSource.type || '', mainVer: resolvedSource.mainVer || 0, patchVer: resolvedSource.patchVer || 0, requestHeaders: resolvedSource.headers || {}, startPosition: resolvedSource.startPosition ?? -1, cropStart: resolvedSource.cropStart || 0, cropEnd: resolvedSource.cropEnd, title: resolvedSource.title, subtitle: resolvedSource.subtitle, description: resolvedSource.description, customImageUri: resolvedSource.customImageUri, }; }, [source]); const _drm = useMemo(() => { if (!drm) { return; } return { type: drm.type, licenseServer: drm.licenseServer, headers: drm.headers, contentId: drm.contentId, certificateUrl: drm.certificateUrl, base64Certificate: drm.base64Certificate, useExternalGetLicense: !!drm.getLicense, }; }, [drm]); const _selectedTextTrack = useMemo(() => { if (!selectedTextTrack) { return; } return { type: selectedTextTrack?.type, value: selectedTextTrack?.value, }; }, [selectedTextTrack]); const _selectedAudioTrack = useMemo(() => { if (!selectedAudioTrack) { return; } return { type: selectedAudioTrack?.type, value: selectedAudioTrack?.value, }; }, [selectedAudioTrack]); const _selectedVideoTrack = useMemo(() => { if (!selectedVideoTrack) { return; } return { type: selectedVideoTrack?.type, value: selectedVideoTrack?.value, }; }, [selectedVideoTrack]); const seek = useCallback(async (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'); return; } Platform.select({ ios: () => { nativeRef.current?.setNativeProps({ seek: { time, tolerance: tolerance || 0, }, }); }, default: () => { nativeRef.current?.setNativeProps({ seek: time, }); }, })(); }, []); const presentFullscreenPlayer = useCallback(() => { setIsFullscreen(true); }, [setIsFullscreen]); const dismissFullscreenPlayer = useCallback(() => { setIsFullscreen(false); }, [setIsFullscreen]); const save = useCallback((options: object) => { // VideoManager.save can be null on android & windows return VideoManager.save?.(options, getReactTag(nativeRef)); }, []); const pause = useCallback(() => { return VideoManager.setPlayerPauseState(true, getReactTag(nativeRef)); }, []); const resume = useCallback(() => { return VideoManager.setPlayerPauseState(false, getReactTag(nativeRef)); }, []); const restoreUserInterfaceForPictureInPictureStopCompleted = useCallback( (restored: boolean) => { setRestoreUserInterfaceForPIPStopCompletionHandler(restored); }, [setRestoreUserInterfaceForPIPStopCompletionHandler], ); const onVideoLoadStart = useCallback( (e: NativeSyntheticEvent) => { onLoadStart?.(e.nativeEvent); }, [onLoadStart], ); const onVideoLoad = useCallback( (e: NativeSyntheticEvent) => { if (Platform.OS === 'windows') { setShowPoster(false); } onLoad?.(e.nativeEvent); }, [onLoad, setShowPoster], ); const onVideoError = useCallback( (e: NativeSyntheticEvent) => { onError?.(e.nativeEvent); }, [onError], ); const onVideoProgress = useCallback( (e: NativeSyntheticEvent) => { onProgress?.(e.nativeEvent); }, [onProgress], ); const onVideoSeek = useCallback( (e: NativeSyntheticEvent) => { onSeek?.(e.nativeEvent); }, [onSeek], ); const onVideoPlaybackStateChanged = useCallback( (e: NativeSyntheticEvent) => { onPlaybackStateChanged?.(e.nativeEvent); }, [onPlaybackStateChanged], ); // android only const onVideoIdle = useCallback(() => { onIdle?.(); }, [onIdle]); const _onTimedMetadata = useCallback( (e: NativeSyntheticEvent) => { onTimedMetadata?.(e.nativeEvent); }, [onTimedMetadata], ); const _onAudioTracks = useCallback( (e: NativeSyntheticEvent) => { onAudioTracks?.(e.nativeEvent); }, [onAudioTracks], ); const _onTextTracks = useCallback( (e: NativeSyntheticEvent) => { onTextTracks?.(e.nativeEvent); }, [onTextTracks], ); const _onTextTrackDataChanged = useCallback( ( e: NativeSyntheticEvent, ) => { const {...eventData} = e.nativeEvent; delete eventData.target; onTextTrackDataChanged?.(eventData as OnTextTrackDataChangedData); }, [onTextTrackDataChanged], ); const _onVideoTracks = useCallback( (e: NativeSyntheticEvent) => { onVideoTracks?.(e.nativeEvent); }, [onVideoTracks], ); const _onPlaybackRateChange = useCallback( (e: NativeSyntheticEvent>) => { onPlaybackRateChange?.(e.nativeEvent); }, [onPlaybackRateChange], ); const _onVolumeChange = useCallback( (e: NativeSyntheticEvent>) => { onVolumeChange?.(e.nativeEvent); }, [onVolumeChange], ); const _onReadyForDisplay = useCallback(() => { setShowPoster(false); onReadyForDisplay?.(); }, [setShowPoster, onReadyForDisplay]); const _onPictureInPictureStatusChanged = useCallback( (e: NativeSyntheticEvent) => { onPictureInPictureStatusChanged?.(e.nativeEvent); }, [onPictureInPictureStatusChanged], ); const _onAudioFocusChanged = useCallback( (e: NativeSyntheticEvent) => { onAudioFocusChanged?.(e.nativeEvent); }, [onAudioFocusChanged], ); const onVideoBuffer = useCallback( (e: NativeSyntheticEvent) => { onBuffer?.(e.nativeEvent); }, [onBuffer], ); const onVideoExternalPlaybackChange = useCallback( (e: NativeSyntheticEvent) => { onExternalPlaybackChange?.(e.nativeEvent); }, [onExternalPlaybackChange], ); const _onBandwidthUpdate = useCallback( (e: NativeSyntheticEvent) => { onBandwidthUpdate?.(e.nativeEvent); }, [onBandwidthUpdate], ); const _onReceiveAdEvent = useCallback( (e: NativeSyntheticEvent) => { onReceiveAdEvent?.(e.nativeEvent); }, [onReceiveAdEvent], ); const _onVideoAspectRatio = useCallback( (e: NativeSyntheticEvent) => { onAspectRatio?.(e.nativeEvent); }, [onAspectRatio], ); const onGetLicense = useCallback( (event: NativeSyntheticEvent) => { if (drm && drm.getLicense instanceof Function) { const data = event.nativeEvent; if (data && data.spcBase64) { const getLicenseOverride = drm.getLicense( data.spcBase64, data.contentId, data.licenseUrl, ); const getLicensePromise = Promise.resolve(getLicenseOverride); // Handles both scenarios, getLicenseOverride being a promise and not. getLicensePromise .then((result) => { if (result !== undefined) { nativeRef.current && VideoManager.setLicenseResult( result, data.licenseUrl, getReactTag(nativeRef), ); } else { nativeRef.current && VideoManager.setLicenseResultError( 'Empty license result', data.licenseUrl, getReactTag(nativeRef), ); } }) .catch(() => { nativeRef.current && VideoManager.setLicenseResultError( 'fetch error', data.licenseUrl, getReactTag(nativeRef), ); }); } else { VideoManager.setLicenseResultError( 'No spc received', data.licenseUrl, getReactTag(nativeRef), ); } } }, [drm], ); useImperativeHandle( ref, () => ({ seek, presentFullscreenPlayer, dismissFullscreenPlayer, save, pause, resume, restoreUserInterfaceForPictureInPictureStopCompleted, }), [ seek, presentFullscreenPlayer, dismissFullscreenPlayer, save, pause, resume, restoreUserInterfaceForPictureInPictureStopCompleted, ], ); return ( {showPoster ? ( ) : null} ); }, ); Video.displayName = 'Video'; export default Video;