885 lines
25 KiB
TypeScript
885 lines
25 KiB
TypeScript
import React, {
|
|
useState,
|
|
useCallback,
|
|
useMemo,
|
|
useRef,
|
|
forwardRef,
|
|
useImperativeHandle,
|
|
} from 'react';
|
|
import type {ElementRef} from 'react';
|
|
import {View, StyleSheet, Image, Platform, processColor} from 'react-native';
|
|
import type {
|
|
StyleProp,
|
|
ImageStyle,
|
|
NativeSyntheticEvent,
|
|
ViewStyle,
|
|
ImageResizeMode,
|
|
} from 'react-native';
|
|
|
|
import NativeVideoComponent, {
|
|
NativeCmcdConfiguration,
|
|
} from './specs/VideoNativeComponent';
|
|
import type {
|
|
OnAudioFocusChangedData,
|
|
OnAudioTracksData,
|
|
OnBandwidthUpdateData,
|
|
OnBufferData,
|
|
OnControlsVisibilityChange,
|
|
OnExternalPlaybackChangeData,
|
|
OnGetLicenseData,
|
|
OnLoadStartData,
|
|
OnPictureInPictureStatusChangedData,
|
|
OnPlaybackStateChangedData,
|
|
OnProgressData,
|
|
OnSeekData,
|
|
OnSeekCompleteData,
|
|
OnTextTrackDataChangedData,
|
|
OnTimedMetadataData,
|
|
OnVideoAspectRatioData,
|
|
OnVideoErrorData,
|
|
OnVideoTracksData,
|
|
VideoSrc,
|
|
} from './specs/VideoNativeComponent';
|
|
import {
|
|
generateHeaderForNative,
|
|
getReactTag,
|
|
resolveAssetSourceForVideo,
|
|
} from './utils';
|
|
import NativeVideoManager from './specs/NativeVideoManager';
|
|
import {type VideoSaveData, CmcdMode, ViewType} from './types';
|
|
import type {
|
|
OnLoadData,
|
|
OnTextTracksData,
|
|
OnReceiveAdEventData,
|
|
ReactVideoProps,
|
|
CmcdData,
|
|
ReactVideoSource,
|
|
} from './types';
|
|
|
|
export interface VideoRef {
|
|
seek: (time: number, tolerance?: number) => void;
|
|
resume: () => void;
|
|
pause: () => void;
|
|
presentFullscreenPlayer: () => void;
|
|
dismissFullscreenPlayer: () => void;
|
|
restoreUserInterfaceForPictureInPictureStopCompleted: (
|
|
restore: boolean,
|
|
) => void;
|
|
setVolume: (volume: number) => void;
|
|
setFullScreen: (fullScreen: boolean) => void;
|
|
setSource: (source?: ReactVideoSource) => void;
|
|
save: (options: object) => Promise<VideoSaveData> | void;
|
|
getCurrentPosition: () => Promise<number>;
|
|
}
|
|
|
|
const Video = forwardRef<VideoRef, ReactVideoProps>(
|
|
(
|
|
{
|
|
source,
|
|
style,
|
|
resizeMode,
|
|
poster,
|
|
posterResizeMode,
|
|
renderLoader,
|
|
contentStartTime,
|
|
drm,
|
|
textTracks,
|
|
selectedVideoTrack,
|
|
selectedAudioTrack,
|
|
selectedTextTrack,
|
|
useTextureView,
|
|
useSecureView,
|
|
viewType,
|
|
shutterColor,
|
|
adTagUrl,
|
|
adLanguage,
|
|
onLoadStart,
|
|
onLoad,
|
|
onError,
|
|
onProgress,
|
|
onSeek,
|
|
onSeekComplete,
|
|
onEnd,
|
|
onBuffer,
|
|
onBandwidthUpdate,
|
|
onControlsVisibilityChange,
|
|
onExternalPlaybackChange,
|
|
onFullscreenPlayerWillPresent,
|
|
onFullscreenPlayerDidPresent,
|
|
onFullscreenPlayerWillDismiss,
|
|
onFullscreenPlayerDidDismiss,
|
|
onReadyForDisplay,
|
|
onPlaybackRateChange,
|
|
onVolumeChange,
|
|
onAudioBecomingNoisy,
|
|
onPictureInPictureStatusChanged,
|
|
onRestoreUserInterfaceForPictureInPictureStop,
|
|
onReceiveAdEvent,
|
|
onPlaybackStateChanged,
|
|
onAudioFocusChanged,
|
|
onIdle,
|
|
onTimedMetadata,
|
|
onAudioTracks,
|
|
onTextTracks,
|
|
onTextTrackDataChanged,
|
|
onVideoTracks,
|
|
onAspectRatio,
|
|
localSourceEncryptionKeyScheme,
|
|
...rest
|
|
},
|
|
ref,
|
|
) => {
|
|
const nativeRef = useRef<ElementRef<typeof NativeVideoComponent>>(null);
|
|
|
|
const isPosterDeprecated = typeof poster === 'string';
|
|
|
|
const _renderLoader = useMemo(
|
|
() =>
|
|
!renderLoader
|
|
? undefined
|
|
: renderLoader instanceof Function
|
|
? renderLoader
|
|
: () => renderLoader,
|
|
[renderLoader],
|
|
);
|
|
|
|
const hasPoster = useMemo(() => {
|
|
if (_renderLoader) {
|
|
return true;
|
|
}
|
|
|
|
if (isPosterDeprecated) {
|
|
return !!poster;
|
|
}
|
|
|
|
return !!poster?.source;
|
|
}, [isPosterDeprecated, poster, _renderLoader]);
|
|
|
|
const [showPoster, setShowPoster] = useState(hasPoster);
|
|
|
|
const [
|
|
_restoreUserInterfaceForPIPStopCompletionHandler,
|
|
setRestoreUserInterfaceForPIPStopCompletionHandler,
|
|
] = useState<boolean | undefined>();
|
|
|
|
const sourceToUnternalSource = useCallback(
|
|
(_source?: ReactVideoSource) => {
|
|
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(/^(rtp|rtsp|http|https):/));
|
|
const isAsset = !!(
|
|
uri &&
|
|
uri.match(
|
|
/^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/,
|
|
)
|
|
);
|
|
|
|
const selectedDrm = _source.drm || drm;
|
|
const _textTracks = _source.textTracks || textTracks;
|
|
const _drm = !selectedDrm
|
|
? undefined
|
|
: {
|
|
type: selectedDrm.type,
|
|
licenseServer: selectedDrm.licenseServer,
|
|
headers: generateHeaderForNative(selectedDrm.headers),
|
|
contentId: selectedDrm.contentId,
|
|
certificateUrl: selectedDrm.certificateUrl,
|
|
base64Certificate: selectedDrm.base64Certificate,
|
|
useExternalGetLicense: !!selectedDrm.getLicense,
|
|
multiDrm: selectedDrm.multiDrm,
|
|
localSourceEncryptionKeyScheme:
|
|
selectedDrm.localSourceEncryptionKeyScheme ||
|
|
localSourceEncryptionKeyScheme,
|
|
};
|
|
|
|
let _cmcd: NativeCmcdConfiguration | undefined;
|
|
if (Platform.OS === 'android' && source?.cmcd) {
|
|
const cmcd = source.cmcd;
|
|
|
|
if (typeof cmcd === 'boolean') {
|
|
_cmcd = cmcd ? {mode: CmcdMode.MODE_QUERY_PARAMETER} : undefined;
|
|
} else if (typeof cmcd === 'object' && !Array.isArray(cmcd)) {
|
|
const createCmcdHeader = (property?: CmcdData) =>
|
|
property ? generateHeaderForNative(property) : undefined;
|
|
|
|
_cmcd = {
|
|
mode: cmcd.mode ?? CmcdMode.MODE_QUERY_PARAMETER,
|
|
request: createCmcdHeader(cmcd.request),
|
|
session: createCmcdHeader(cmcd.session),
|
|
object: createCmcdHeader(cmcd.object),
|
|
status: createCmcdHeader(cmcd.status),
|
|
};
|
|
} else {
|
|
throw new Error(
|
|
'Invalid CMCD configuration: Expected a boolean or an object.',
|
|
);
|
|
}
|
|
}
|
|
|
|
const selectedContentStartTime =
|
|
_source.contentStartTime || contentStartTime;
|
|
|
|
const _ad =
|
|
_source.ad ||
|
|
(adTagUrl || adLanguage
|
|
? {adTagUrl: adTagUrl, adLanguage: adLanguage}
|
|
: undefined);
|
|
|
|
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,
|
|
contentStartTime: selectedContentStartTime,
|
|
metadata: resolvedSource.metadata,
|
|
drm: _drm,
|
|
ad: _ad,
|
|
cmcd: _cmcd,
|
|
textTracks: _textTracks,
|
|
textTracksAllowChunklessPreparation:
|
|
resolvedSource.textTracksAllowChunklessPreparation,
|
|
};
|
|
},
|
|
[
|
|
adLanguage,
|
|
adTagUrl,
|
|
contentStartTime,
|
|
drm,
|
|
localSourceEncryptionKeyScheme,
|
|
source?.cmcd,
|
|
textTracks,
|
|
],
|
|
);
|
|
|
|
const src = useMemo<VideoSrc | undefined>(() => {
|
|
return sourceToUnternalSource(source);
|
|
}, [sourceToUnternalSource, source]);
|
|
|
|
const _selectedTextTrack = useMemo(() => {
|
|
if (!selectedTextTrack) {
|
|
return;
|
|
}
|
|
const typeOfValueProp = typeof selectedTextTrack.value;
|
|
if (
|
|
typeOfValueProp !== 'number' &&
|
|
typeOfValueProp !== 'string' &&
|
|
typeOfValueProp !== 'undefined'
|
|
) {
|
|
console.warn(
|
|
'invalid type provided to selectedTextTrack.value: ',
|
|
typeOfValueProp,
|
|
);
|
|
return;
|
|
}
|
|
return {
|
|
type: selectedTextTrack?.type,
|
|
value: `${selectedTextTrack.value}`,
|
|
};
|
|
}, [selectedTextTrack]);
|
|
|
|
const _selectedAudioTrack = useMemo(() => {
|
|
if (!selectedAudioTrack) {
|
|
return;
|
|
}
|
|
const typeOfValueProp = typeof selectedAudioTrack.value;
|
|
if (
|
|
typeOfValueProp !== 'number' &&
|
|
typeOfValueProp !== 'string' &&
|
|
typeOfValueProp !== 'undefined'
|
|
) {
|
|
console.warn(
|
|
'invalid type provided to selectedAudioTrack.value: ',
|
|
typeOfValueProp,
|
|
);
|
|
return;
|
|
}
|
|
|
|
return {
|
|
type: selectedAudioTrack?.type,
|
|
value: `${selectedAudioTrack.value}`,
|
|
};
|
|
}, [selectedAudioTrack]);
|
|
|
|
const _selectedVideoTrack = useMemo(() => {
|
|
if (!selectedVideoTrack) {
|
|
return;
|
|
}
|
|
const typeOfValueProp = typeof selectedVideoTrack.value;
|
|
if (
|
|
typeOfValueProp !== 'number' &&
|
|
typeOfValueProp !== 'string' &&
|
|
typeOfValueProp !== 'undefined'
|
|
) {
|
|
console.warn(
|
|
'invalid type provided to selectedVideoTrack.value: ',
|
|
typeOfValueProp,
|
|
);
|
|
return;
|
|
}
|
|
return {
|
|
type: selectedVideoTrack?.type,
|
|
value: `${selectedVideoTrack.value}`,
|
|
};
|
|
}, [selectedVideoTrack]);
|
|
|
|
const seek = useCallback(async (time: number, tolerance?: number) => {
|
|
if (isNaN(time) || time === null) {
|
|
throw new Error("Specified time is not a number: '" + time + "'");
|
|
}
|
|
|
|
if (!nativeRef.current) {
|
|
console.warn('Video Component is not mounted');
|
|
return;
|
|
}
|
|
|
|
const callSeekFunction = () => {
|
|
NativeVideoManager.seekCmd(
|
|
getReactTag(nativeRef),
|
|
time,
|
|
tolerance || 0,
|
|
);
|
|
};
|
|
|
|
Platform.select({
|
|
ios: callSeekFunction,
|
|
android: callSeekFunction,
|
|
default: () => {
|
|
// TODO: Implement VideoManager.seekCmd for windows
|
|
nativeRef.current?.setNativeProps({seek: time});
|
|
},
|
|
})();
|
|
}, []);
|
|
|
|
const pause = useCallback(() => {
|
|
return NativeVideoManager.setPlayerPauseStateCmd(
|
|
getReactTag(nativeRef),
|
|
true,
|
|
);
|
|
}, []);
|
|
|
|
const resume = useCallback(() => {
|
|
return NativeVideoManager.setPlayerPauseStateCmd(
|
|
getReactTag(nativeRef),
|
|
false,
|
|
);
|
|
}, []);
|
|
|
|
const setVolume = useCallback((volume: number) => {
|
|
return NativeVideoManager.setVolumeCmd(getReactTag(nativeRef), volume);
|
|
}, []);
|
|
|
|
const setFullScreen = useCallback((fullScreen: boolean) => {
|
|
return NativeVideoManager.setFullScreenCmd(
|
|
getReactTag(nativeRef),
|
|
fullScreen,
|
|
);
|
|
}, []);
|
|
|
|
const setSource = useCallback(
|
|
(_source?: ReactVideoSource) => {
|
|
return NativeVideoManager.setSourceCmd(
|
|
getReactTag(nativeRef),
|
|
sourceToUnternalSource(_source),
|
|
);
|
|
},
|
|
[sourceToUnternalSource],
|
|
);
|
|
|
|
const presentFullscreenPlayer = useCallback(
|
|
() => setFullScreen(true),
|
|
[setFullScreen],
|
|
);
|
|
|
|
const dismissFullscreenPlayer = useCallback(
|
|
() => setFullScreen(false),
|
|
[setFullScreen],
|
|
);
|
|
|
|
const save = useCallback((options: object) => {
|
|
// VideoManager.save can be null on android & windows
|
|
if (Platform.OS !== 'ios') {
|
|
return;
|
|
}
|
|
// @todo Must implement it in a different way.
|
|
return NativeVideoManager.save?.(getReactTag(nativeRef), options);
|
|
}, []);
|
|
|
|
const getCurrentPosition = useCallback(() => {
|
|
// @todo Must implement it in a different way.
|
|
return NativeVideoManager.getCurrentPosition(getReactTag(nativeRef));
|
|
}, []);
|
|
|
|
const restoreUserInterfaceForPictureInPictureStopCompleted = useCallback(
|
|
(restored: boolean) => {
|
|
setRestoreUserInterfaceForPIPStopCompletionHandler(restored);
|
|
},
|
|
[setRestoreUserInterfaceForPIPStopCompletionHandler],
|
|
);
|
|
|
|
const onVideoLoadStart = useCallback(
|
|
(e: NativeSyntheticEvent<OnLoadStartData>) => {
|
|
hasPoster && setShowPoster(true);
|
|
onLoadStart?.(e.nativeEvent);
|
|
},
|
|
[hasPoster, onLoadStart],
|
|
);
|
|
|
|
const onVideoLoad = useCallback(
|
|
(e: NativeSyntheticEvent<OnLoadData>) => {
|
|
if (Platform.OS === 'windows') {
|
|
hasPoster && setShowPoster(false);
|
|
}
|
|
onLoad?.(e.nativeEvent);
|
|
},
|
|
[onLoad, hasPoster, setShowPoster],
|
|
);
|
|
|
|
const onVideoError = useCallback(
|
|
(e: NativeSyntheticEvent<OnVideoErrorData>) => {
|
|
onError?.(e.nativeEvent);
|
|
},
|
|
[onError],
|
|
);
|
|
|
|
const onVideoProgress = useCallback(
|
|
(e: NativeSyntheticEvent<OnProgressData>) => {
|
|
onProgress?.(e.nativeEvent);
|
|
},
|
|
[onProgress],
|
|
);
|
|
|
|
const onVideoSeek = useCallback(
|
|
(e: NativeSyntheticEvent<OnSeekData>) => {
|
|
onSeek?.(e.nativeEvent);
|
|
},
|
|
[onSeek],
|
|
);
|
|
|
|
const onVideoSeekComplete = useCallback(
|
|
(e: NativeSyntheticEvent<OnSeekCompleteData>) => {
|
|
onSeekComplete?.(e.nativeEvent);
|
|
},
|
|
[onSeekComplete]
|
|
);
|
|
|
|
const onVideoPlaybackStateChanged = useCallback(
|
|
(e: NativeSyntheticEvent<OnPlaybackStateChangedData>) => {
|
|
onPlaybackStateChanged?.(e.nativeEvent);
|
|
},
|
|
[onPlaybackStateChanged],
|
|
);
|
|
|
|
const _shutterColor = useMemo(() => {
|
|
const color = processColor(shutterColor);
|
|
return typeof color === 'number' ? color : undefined;
|
|
}, [shutterColor]);
|
|
|
|
// android only
|
|
const _onTimedMetadata = useCallback(
|
|
(e: NativeSyntheticEvent<OnTimedMetadataData>) => {
|
|
onTimedMetadata?.(e.nativeEvent);
|
|
},
|
|
[onTimedMetadata],
|
|
);
|
|
|
|
const _onAudioTracks = useCallback(
|
|
(e: NativeSyntheticEvent<OnAudioTracksData>) => {
|
|
onAudioTracks?.(e.nativeEvent);
|
|
},
|
|
[onAudioTracks],
|
|
);
|
|
|
|
const _onTextTracks = useCallback(
|
|
(e: NativeSyntheticEvent<OnTextTracksData>) => {
|
|
onTextTracks?.(e.nativeEvent);
|
|
},
|
|
[onTextTracks],
|
|
);
|
|
|
|
const _onTextTrackDataChanged = useCallback(
|
|
(
|
|
e: NativeSyntheticEvent<OnTextTrackDataChangedData & {target?: number}>,
|
|
) => {
|
|
const {...eventData} = e.nativeEvent;
|
|
delete eventData.target;
|
|
onTextTrackDataChanged?.(eventData as OnTextTrackDataChangedData);
|
|
},
|
|
[onTextTrackDataChanged],
|
|
);
|
|
|
|
const _onVideoTracks = useCallback(
|
|
(e: NativeSyntheticEvent<OnVideoTracksData>) => {
|
|
onVideoTracks?.(e.nativeEvent);
|
|
},
|
|
[onVideoTracks],
|
|
);
|
|
|
|
const _onPlaybackRateChange = useCallback(
|
|
(e: NativeSyntheticEvent<Readonly<{playbackRate: number}>>) => {
|
|
onPlaybackRateChange?.(e.nativeEvent);
|
|
},
|
|
[onPlaybackRateChange],
|
|
);
|
|
|
|
const _onVolumeChange = useCallback(
|
|
(e: NativeSyntheticEvent<Readonly<{volume: number}>>) => {
|
|
onVolumeChange?.(e.nativeEvent);
|
|
},
|
|
[onVolumeChange],
|
|
);
|
|
|
|
const _onReadyForDisplay = useCallback(() => {
|
|
hasPoster && setShowPoster(false);
|
|
onReadyForDisplay?.();
|
|
}, [setShowPoster, hasPoster, onReadyForDisplay]);
|
|
|
|
const _onPictureInPictureStatusChanged = useCallback(
|
|
(e: NativeSyntheticEvent<OnPictureInPictureStatusChangedData>) => {
|
|
onPictureInPictureStatusChanged?.(e.nativeEvent);
|
|
},
|
|
[onPictureInPictureStatusChanged],
|
|
);
|
|
|
|
const _onAudioFocusChanged = useCallback(
|
|
(e: NativeSyntheticEvent<OnAudioFocusChangedData>) => {
|
|
onAudioFocusChanged?.(e.nativeEvent);
|
|
},
|
|
[onAudioFocusChanged],
|
|
);
|
|
|
|
const onVideoBuffer = useCallback(
|
|
(e: NativeSyntheticEvent<OnBufferData>) => {
|
|
onBuffer?.(e.nativeEvent);
|
|
},
|
|
[onBuffer],
|
|
);
|
|
|
|
const onVideoExternalPlaybackChange = useCallback(
|
|
(e: NativeSyntheticEvent<OnExternalPlaybackChangeData>) => {
|
|
onExternalPlaybackChange?.(e.nativeEvent);
|
|
},
|
|
[onExternalPlaybackChange],
|
|
);
|
|
|
|
const _onBandwidthUpdate = useCallback(
|
|
(e: NativeSyntheticEvent<OnBandwidthUpdateData>) => {
|
|
onBandwidthUpdate?.(e.nativeEvent);
|
|
},
|
|
[onBandwidthUpdate],
|
|
);
|
|
|
|
const _onReceiveAdEvent = useCallback(
|
|
(e: NativeSyntheticEvent<OnReceiveAdEventData>) => {
|
|
onReceiveAdEvent?.(e.nativeEvent);
|
|
},
|
|
[onReceiveAdEvent],
|
|
);
|
|
|
|
const _onVideoAspectRatio = useCallback(
|
|
(e: NativeSyntheticEvent<OnVideoAspectRatioData>) => {
|
|
onAspectRatio?.(e.nativeEvent);
|
|
},
|
|
[onAspectRatio],
|
|
);
|
|
|
|
const _onControlsVisibilityChange = useCallback(
|
|
(e: NativeSyntheticEvent<OnControlsVisibilityChange>) => {
|
|
onControlsVisibilityChange?.(e.nativeEvent);
|
|
},
|
|
[onControlsVisibilityChange],
|
|
);
|
|
|
|
const selectedDrm = source?.drm || drm;
|
|
const usingExternalGetLicense = selectedDrm?.getLicense instanceof Function;
|
|
|
|
const onGetLicense = useCallback(
|
|
async (event: NativeSyntheticEvent<OnGetLicenseData>) => {
|
|
if (!usingExternalGetLicense) {
|
|
return;
|
|
}
|
|
const data = event.nativeEvent;
|
|
try {
|
|
if (!data?.spcBase64) {
|
|
throw new Error('No spc received');
|
|
}
|
|
// Handles both scenarios, getLicenseOverride being a promise and not.
|
|
const license = await Promise.resolve(
|
|
selectedDrm.getLicense(
|
|
data.spcBase64,
|
|
data.contentId,
|
|
data.licenseUrl,
|
|
data.loadedLicenseUrl,
|
|
),
|
|
).catch(() => {
|
|
throw new Error('fetch error');
|
|
});
|
|
if (typeof license !== 'string') {
|
|
throw Error('Empty license result');
|
|
}
|
|
if (nativeRef.current) {
|
|
NativeVideoManager.setLicenseResultCmd(
|
|
getReactTag(nativeRef),
|
|
license,
|
|
data.loadedLicenseUrl,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : 'fetch error';
|
|
if (nativeRef.current) {
|
|
NativeVideoManager.setLicenseResultErrorCmd(
|
|
getReactTag(nativeRef),
|
|
msg,
|
|
data.loadedLicenseUrl,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
[selectedDrm, usingExternalGetLicense],
|
|
);
|
|
|
|
useImperativeHandle(
|
|
ref,
|
|
() => ({
|
|
seek,
|
|
presentFullscreenPlayer,
|
|
dismissFullscreenPlayer,
|
|
save,
|
|
pause,
|
|
resume,
|
|
restoreUserInterfaceForPictureInPictureStopCompleted,
|
|
setVolume,
|
|
getCurrentPosition,
|
|
setFullScreen,
|
|
setSource,
|
|
}),
|
|
[
|
|
seek,
|
|
presentFullscreenPlayer,
|
|
dismissFullscreenPlayer,
|
|
save,
|
|
pause,
|
|
resume,
|
|
restoreUserInterfaceForPictureInPictureStopCompleted,
|
|
setVolume,
|
|
getCurrentPosition,
|
|
setFullScreen,
|
|
setSource,
|
|
],
|
|
);
|
|
|
|
const _viewType = useMemo(() => {
|
|
const hasValidDrmProp =
|
|
drm !== undefined && Object.keys(drm).length !== 0;
|
|
|
|
const shallForceViewType =
|
|
hasValidDrmProp && (viewType === ViewType.TEXTURE || useTextureView);
|
|
|
|
if (useSecureView && useTextureView) {
|
|
console.warn(
|
|
'cannot use SecureView on texture view. please set useTextureView={false}',
|
|
);
|
|
}
|
|
|
|
if (shallForceViewType) {
|
|
console.warn(
|
|
'cannot use DRM on texture view. please set useTextureView={false}',
|
|
);
|
|
return useSecureView ? ViewType.SURFACE_SECURE : ViewType.SURFACE;
|
|
}
|
|
|
|
if (viewType !== undefined && viewType !== null) {
|
|
return viewType;
|
|
}
|
|
|
|
if (useSecureView) {
|
|
return ViewType.SURFACE_SECURE;
|
|
}
|
|
|
|
if (useTextureView) {
|
|
return ViewType.TEXTURE;
|
|
}
|
|
|
|
return ViewType.SURFACE;
|
|
}, [drm, useSecureView, useTextureView, viewType]);
|
|
|
|
const _renderPoster = useCallback(() => {
|
|
if (!hasPoster || !showPoster) {
|
|
return null;
|
|
}
|
|
|
|
// poster resize mode
|
|
let _posterResizeMode: ImageResizeMode = 'contain';
|
|
|
|
if (!isPosterDeprecated && poster?.resizeMode) {
|
|
_posterResizeMode = poster.resizeMode;
|
|
} else if (posterResizeMode && posterResizeMode !== 'none') {
|
|
_posterResizeMode = posterResizeMode;
|
|
}
|
|
|
|
// poster style
|
|
const baseStyle: StyleProp<ImageStyle> = {
|
|
...StyleSheet.absoluteFillObject,
|
|
resizeMode: _posterResizeMode,
|
|
};
|
|
|
|
let posterStyle: StyleProp<ImageStyle> = baseStyle;
|
|
|
|
if (!isPosterDeprecated && poster?.style) {
|
|
const styles = Array.isArray(poster.style)
|
|
? poster.style
|
|
: [poster.style];
|
|
posterStyle = [baseStyle, ...styles];
|
|
}
|
|
|
|
// render poster
|
|
if (_renderLoader && (poster || posterResizeMode)) {
|
|
console.warn(
|
|
'You provided both `renderLoader` and `poster` or `posterResizeMode` props. `renderLoader` will be used.',
|
|
);
|
|
}
|
|
|
|
// render loader
|
|
if (_renderLoader) {
|
|
return (
|
|
<View style={StyleSheet.absoluteFill}>
|
|
{_renderLoader({
|
|
source: source,
|
|
style: posterStyle,
|
|
resizeMode: resizeMode,
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Image
|
|
{...(isPosterDeprecated ? {} : poster)}
|
|
source={isPosterDeprecated ? {uri: poster} : poster?.source}
|
|
style={posterStyle}
|
|
/>
|
|
);
|
|
}, [
|
|
hasPoster,
|
|
isPosterDeprecated,
|
|
poster,
|
|
posterResizeMode,
|
|
_renderLoader,
|
|
showPoster,
|
|
source,
|
|
resizeMode,
|
|
]);
|
|
|
|
const _style: StyleProp<ViewStyle> = useMemo(
|
|
() => ({
|
|
...StyleSheet.absoluteFillObject,
|
|
...(showPoster ? {display: 'none'} : {}),
|
|
}),
|
|
[showPoster],
|
|
);
|
|
|
|
return (
|
|
<View style={style}>
|
|
<NativeVideoComponent
|
|
ref={nativeRef}
|
|
{...rest}
|
|
src={src}
|
|
style={_style}
|
|
resizeMode={resizeMode}
|
|
restoreUserInterfaceForPIPStopCompletionHandler={
|
|
_restoreUserInterfaceForPIPStopCompletionHandler
|
|
}
|
|
selectedTextTrack={_selectedTextTrack}
|
|
selectedAudioTrack={_selectedAudioTrack}
|
|
selectedVideoTrack={_selectedVideoTrack}
|
|
shutterColor={_shutterColor}
|
|
onGetLicense={usingExternalGetLicense ? onGetLicense : undefined}
|
|
onVideoLoad={
|
|
onLoad || hasPoster
|
|
? (onVideoLoad as (e: NativeSyntheticEvent<object>) => void)
|
|
: undefined
|
|
}
|
|
onVideoLoadStart={
|
|
onLoadStart || hasPoster ? onVideoLoadStart : undefined
|
|
}
|
|
onVideoError={onError ? onVideoError : undefined}
|
|
onVideoProgress={onProgress ? onVideoProgress : undefined}
|
|
onVideoSeek={onSeek ? onVideoSeek : undefined}
|
|
onVideoSeekComplete={onSeekComplete ? onVideoSeekComplete : undefined}
|
|
onVideoEnd={onEnd}
|
|
onVideoBuffer={onBuffer ? onVideoBuffer : undefined}
|
|
onVideoPlaybackStateChanged={
|
|
onPlaybackStateChanged ? onVideoPlaybackStateChanged : undefined
|
|
}
|
|
onVideoBandwidthUpdate={
|
|
onBandwidthUpdate ? _onBandwidthUpdate : undefined
|
|
}
|
|
onTimedMetadata={onTimedMetadata ? _onTimedMetadata : undefined}
|
|
onAudioTracks={onAudioTracks ? _onAudioTracks : undefined}
|
|
onTextTracks={onTextTracks ? _onTextTracks : undefined}
|
|
onTextTrackDataChanged={
|
|
onTextTrackDataChanged ? _onTextTrackDataChanged : undefined
|
|
}
|
|
onVideoTracks={onVideoTracks ? _onVideoTracks : undefined}
|
|
onVideoFullscreenPlayerDidDismiss={onFullscreenPlayerDidDismiss}
|
|
onVideoFullscreenPlayerDidPresent={onFullscreenPlayerDidPresent}
|
|
onVideoFullscreenPlayerWillDismiss={onFullscreenPlayerWillDismiss}
|
|
onVideoFullscreenPlayerWillPresent={onFullscreenPlayerWillPresent}
|
|
onVideoExternalPlaybackChange={
|
|
onExternalPlaybackChange ? onVideoExternalPlaybackChange : undefined
|
|
}
|
|
onVideoIdle={onIdle}
|
|
onAudioFocusChanged={
|
|
onAudioFocusChanged ? _onAudioFocusChanged : undefined
|
|
}
|
|
onReadyForDisplay={
|
|
onReadyForDisplay || hasPoster ? _onReadyForDisplay : undefined
|
|
}
|
|
onPlaybackRateChange={
|
|
onPlaybackRateChange ? _onPlaybackRateChange : undefined
|
|
}
|
|
onVolumeChange={onVolumeChange ? _onVolumeChange : undefined}
|
|
onVideoAudioBecomingNoisy={onAudioBecomingNoisy}
|
|
onPictureInPictureStatusChanged={
|
|
onPictureInPictureStatusChanged
|
|
? _onPictureInPictureStatusChanged
|
|
: undefined
|
|
}
|
|
onRestoreUserInterfaceForPictureInPictureStop={
|
|
onRestoreUserInterfaceForPictureInPictureStop
|
|
}
|
|
onVideoAspectRatio={onAspectRatio ? _onVideoAspectRatio : undefined}
|
|
onReceiveAdEvent={
|
|
onReceiveAdEvent
|
|
? (_onReceiveAdEvent as (e: NativeSyntheticEvent<object>) => void)
|
|
: undefined
|
|
}
|
|
onControlsVisibilityChange={
|
|
onControlsVisibilityChange ? _onControlsVisibilityChange : undefined
|
|
}
|
|
viewType={_viewType}
|
|
/>
|
|
{_renderPoster()}
|
|
</View>
|
|
);
|
|
},
|
|
);
|
|
|
|
Video.displayName = 'Video';
|
|
export default Video;
|