import React, { useState, useCallback, useMemo, useRef, forwardRef, useImperativeHandle, type ComponentRef, } from 'react'; import {View, StyleSheet, Image, Platform} from 'react-native'; import NativeVideoComponent, { type OnAudioFocusChangedData, type OnAudioTracksData, type OnBandwidthUpdateData, type OnBufferData, type OnExternalPlaybackChangeData, type OnGetLicenseData, type OnLoadData, type OnLoadStartData, type OnPictureInPictureStatusChangedData, type OnPlaybackStateChangedData, type OnProgressData, type OnReceiveAdEventData, type OnSeekData, type OnTextTrackDataChangedData, type OnTextTracksData, type OnTimedMetadataData, type OnVideoAspectRatioData, type OnVideoErrorData, type OnVideoTracksData, type VideoComponentType, type VideoSrc, } from './specs/VideoNativeComponent'; import type {StyleProp, ImageStyle, NativeSyntheticEvent} from 'react-native'; import { generateHeaderForNative, getReactTag, resolveAssetSourceForVideo, } from './utils'; import {VideoManager} from './specs/VideoNativeComponent'; import type {ReactVideoProps} from './types/video'; 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: generateHeaderForNative(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: generateHeaderForNative(drm.headers), contentId: drm.contentId, certificateUrl: drm.certificateUrl, base64Certificate: drm.base64Certificate, useExternalGetLicense: !!drm.getLicense, }; }, [drm]); const _selectedTextTrack = useMemo(() => { if (!selectedTextTrack) { return; } const value = selectedTextTrack.value ? `${selectedTextTrack.value}` : undefined; return { type: selectedTextTrack?.type, value, }; }, [selectedTextTrack]); const _selectedAudioTrack = useMemo(() => { if (!selectedAudioTrack) { return; } const value = selectedAudioTrack.value ? `${selectedAudioTrack.value}` : undefined; return { type: selectedAudioTrack?.type, 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;