expo-av
Version:
Expo universal module for Audio and Video playback
290 lines • 11.3 kB
JavaScript
import * as React from 'react';
import { findNodeHandle, Image, StyleSheet, View } from 'react-native';
import { assertStatusValuesInBounds, getNativeSourceAndFullInitialStatusForLoadAsync, getNativeSourceFromSource, getUnloadedStatus, PlaybackMixin, } from './AV';
import ExpoVideoManager from './ExpoVideoManager';
import ExponentAV from './ExponentAV';
import ExponentVideo from './ExponentVideo';
import { ResizeMode, } from './Video.types';
const _STYLES = StyleSheet.create({
base: {
overflow: 'hidden',
pointerEvents: 'box-none',
},
poster: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
resizeMode: 'contain',
},
video: {
position: 'absolute',
left: 0,
top: 0,
right: 0,
bottom: 0,
},
});
let didWarnAboutVideoDeprecation = false;
// On a real device UIManager should be present, however when running offline tests with jest-expo
// we have to use the provided native module mock to access constants
const ExpoVideoManagerConstants = ExpoVideoManager;
const ExpoVideoViewManager = ExpoVideoManager;
class Video extends React.Component {
_nativeRef = React.createRef();
_onPlaybackStatusUpdate = null;
constructor(props) {
super(props);
this.state = {
showPoster: !!props.usePoster,
};
}
/**
* @hidden
*/
setNativeProps(nativeProps) {
const nativeVideo = this._nativeRef.current;
if (!nativeVideo)
throw new Error(`native video reference is not defined.`);
nativeVideo.setNativeProps(nativeProps);
}
// Internal methods
_handleNewStatus = (status) => {
if (this.state.showPoster &&
status.isLoaded &&
(status.isPlaying || status.positionMillis !== 0)) {
this.setState({ showPoster: false });
}
if (this.props.onPlaybackStatusUpdate) {
this.props.onPlaybackStatusUpdate(status);
}
if (this._onPlaybackStatusUpdate) {
this._onPlaybackStatusUpdate(status);
}
};
_performOperationAndHandleStatusAsync = async (operation) => {
const video = this._nativeRef.current;
if (!video) {
throw new Error(`Cannot complete operation because the Video component has not yet loaded`);
}
const handle = findNodeHandle(this._nativeRef.current);
const status = await operation(handle);
this._handleNewStatus(status);
return status;
};
// Fullscreening API
_setFullscreen = async (value) => {
return this._performOperationAndHandleStatusAsync((tag) => ExpoVideoViewManager.setFullscreen(tag, value));
};
/**
* This presents a fullscreen view of your video component on top of your app's UI. Note that even if `useNativeControls` is set to `false`,
* native controls will be visible in fullscreen mode.
* @return A `Promise` that is fulfilled with the `AVPlaybackStatus` of the video once the fullscreen player has finished presenting,
* or rejects if there was an error, or if this was called on an Android device.
*/
presentFullscreenPlayer = async () => {
return this._setFullscreen(true);
};
/**
* This dismisses the fullscreen video view.
* @return A `Promise` that is fulfilled with the `AVPlaybackStatus` of the video once the fullscreen player has finished dismissing,
* or rejects if there was an error, or if this was called on an Android device.
*/
dismissFullscreenPlayer = async () => {
return this._setFullscreen(false);
};
// ### Unified playback API ### (consistent with Audio.js)
// All calls automatically call onPlaybackStatusUpdate as a side effect.
/**
* @hidden
*/
getStatusAsync = async () => {
return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.getStatusForVideo(tag));
};
/**
* @hidden
*/
loadAsync = async (source, initialStatus = {}, downloadFirst = true) => {
const { nativeSource, fullInitialStatus } = await getNativeSourceAndFullInitialStatusForLoadAsync(source, initialStatus, downloadFirst);
return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.loadForVideo(tag, nativeSource, fullInitialStatus));
};
/**
* Equivalent to setting URI to `null`.
* @hidden
*/
unloadAsync = async () => {
return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.unloadForVideo(tag));
};
componentWillUnmount() {
// Auto unload video to perform necessary cleanup safely
this.unloadAsync().catch(() => {
// Ignored rejection. Sometimes the unloadAsync code is executed when video is already unloaded.
// In such cases, it throws:
// "[Unhandled promise rejection: Error: Invalid view returned from registry,
// expecting EXVideo, got: (null)]"
});
}
/**
* Set status API, only available while `isLoaded = true`.
* @hidden
*/
setStatusAsync = async (status) => {
assertStatusValuesInBounds(status);
return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.setStatusForVideo(tag, status));
};
/**
* @hidden
*/
replayAsync = async (status = {}) => {
if (status.positionMillis && status.positionMillis !== 0) {
throw new Error('Requested position after replay has to be 0.');
}
return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.replayVideo(tag, {
...status,
positionMillis: 0,
shouldPlay: true,
}));
};
/**
* Sets a function to be called regularly with the `AVPlaybackStatus` of the playback object.
*
* `onPlaybackStatusUpdate` will be called whenever a call to the API for this playback object completes
* (such as `setStatusAsync()`, `getStatusAsync()`, or `unloadAsync()`), nd will also be called at regular intervals
* while the media is in the loaded state.
*
* Set `progressUpdateIntervalMillis` via `setStatusAsync()` or `setProgressUpdateIntervalAsync()` to modify
* the interval with which `onPlaybackStatusUpdate` is called while loaded.
*
* @param onPlaybackStatusUpdate A function taking a single parameter `AVPlaybackStatus`.
*/
setOnPlaybackStatusUpdate(onPlaybackStatusUpdate) {
this._onPlaybackStatusUpdate = onPlaybackStatusUpdate;
this.getStatusAsync();
}
// Methods of the Playback interface that are set via PlaybackMixin
playAsync;
playFromPositionAsync;
pauseAsync;
stopAsync;
setPositionAsync;
setRateAsync;
setVolumeAsync;
setIsMutedAsync;
setIsLoopingAsync;
setProgressUpdateIntervalAsync;
// Callback wrappers
_nativeOnPlaybackStatusUpdate = (event) => {
this._handleNewStatus(event.nativeEvent);
};
// TODO make sure we are passing the right stuff
_nativeOnLoadStart = () => {
if (this.props.onLoadStart) {
this.props.onLoadStart();
}
};
_nativeOnLoad = (event) => {
if (this.props.onLoad) {
this.props.onLoad(event.nativeEvent);
}
this._handleNewStatus(event.nativeEvent);
};
_nativeOnError = (event) => {
const error = event.nativeEvent.error;
if (this.props.onError) {
this.props.onError(error);
}
this._handleNewStatus(getUnloadedStatus(error));
};
_nativeOnReadyForDisplay = (event) => {
if (this.props.onReadyForDisplay) {
this.props.onReadyForDisplay(event.nativeEvent);
}
};
_nativeOnFullscreenUpdate = (event) => {
if (this.props.onFullscreenUpdate) {
this.props.onFullscreenUpdate(event.nativeEvent);
}
};
_renderPoster = () => {
const PosterComponent = this.props.PosterComponent ?? Image;
return this.props.usePoster && this.state.showPoster ? (<PosterComponent style={[_STYLES.poster, this.props.posterStyle]} source={this.props.posterSource}/>) : null;
};
render() {
maybeWarnAboutVideoDeprecation();
const source = getNativeSourceFromSource(this.props.source) || undefined;
let nativeResizeMode = ExpoVideoManagerConstants.ScaleNone;
if (this.props.resizeMode) {
const resizeMode = this.props.resizeMode;
if (resizeMode === ResizeMode.STRETCH) {
nativeResizeMode = ExpoVideoManagerConstants.ScaleToFill;
}
else if (resizeMode === ResizeMode.CONTAIN) {
nativeResizeMode = ExpoVideoManagerConstants.ScaleAspectFit;
}
else if (resizeMode === ResizeMode.COVER) {
nativeResizeMode = ExpoVideoManagerConstants.ScaleAspectFill;
}
}
// Set status via individual props
const status = { ...this.props.status };
[
'progressUpdateIntervalMillis',
'positionMillis',
'shouldPlay',
'rate',
'shouldCorrectPitch',
'volume',
'isMuted',
'isLooping',
].forEach((prop) => {
if (prop in this.props) {
status[prop] = this.props[prop];
}
});
// Replace selected native props
const nativeProps = {
...omit(this.props, [
'source',
'onPlaybackStatusUpdate',
'usePoster',
'posterSource',
'posterStyle',
...Object.keys(status),
]),
style: [_STYLES.base, this.props.style],
videoStyle: [_STYLES.video, this.props.videoStyle],
source,
resizeMode: nativeResizeMode,
status,
onStatusUpdate: this._nativeOnPlaybackStatusUpdate,
onLoadStart: this._nativeOnLoadStart,
onLoad: this._nativeOnLoad,
onError: this._nativeOnError,
onReadyForDisplay: this._nativeOnReadyForDisplay,
onFullscreenUpdate: this._nativeOnFullscreenUpdate,
};
return (<View style={nativeProps.style}>
<ExponentVideo ref={this._nativeRef} {...nativeProps} style={nativeProps.videoStyle}/>
{this._renderPoster()}
</View>);
}
}
function omit(props, propNames) {
const copied = { ...props };
for (const propName of propNames) {
delete copied[propName];
}
return copied;
}
function maybeWarnAboutVideoDeprecation() {
if (__DEV__ && !didWarnAboutVideoDeprecation) {
didWarnAboutVideoDeprecation = true;
console.log('⚠️ \x1b[33m[expo-av]: Video component from `expo-av` is deprecated in favor of `expo-video`. ' +
'See the documentation at https://docs.expo.dev/versions/latest/sdk/video/ for the new API reference.');
}
}
Object.assign(Video.prototype, PlaybackMixin);
// note(simek): TypeDoc cannot resolve correctly name of inline and default exported class
export default Video;
//# sourceMappingURL=Video.js.map