1import * as React from 'react'; 2import { findNodeHandle, Image, StyleSheet, View } from 'react-native'; 3import { assertStatusValuesInBounds, getNativeSourceAndFullInitialStatusForLoadAsync, getNativeSourceFromSource, getUnloadedStatus, PlaybackMixin, } from './AV'; 4import ExpoVideoManager from './ExpoVideoManager'; 5import ExponentAV from './ExponentAV'; 6import ExponentVideo from './ExponentVideo'; 7import { ResizeMode, } from './Video.types'; 8const _STYLES = StyleSheet.create({ 9 base: { 10 overflow: 'hidden', 11 }, 12 poster: { 13 position: 'absolute', 14 left: 0, 15 top: 0, 16 right: 0, 17 bottom: 0, 18 resizeMode: 'contain', 19 }, 20 video: { 21 position: 'absolute', 22 left: 0, 23 top: 0, 24 right: 0, 25 bottom: 0, 26 }, 27}); 28// On a real device UIManager should be present, however when running offline tests with jest-expo 29// we have to use the provided native module mock to access constants 30const ExpoVideoManagerConstants = ExpoVideoManager; 31const ExpoVideoViewManager = ExpoVideoManager; 32class Video extends React.Component { 33 _nativeRef = React.createRef(); 34 _onPlaybackStatusUpdate = null; 35 constructor(props) { 36 super(props); 37 this.state = { 38 showPoster: !!props.usePoster, 39 }; 40 } 41 /** 42 * @hidden 43 */ 44 setNativeProps(nativeProps) { 45 const nativeVideo = this._nativeRef.current; 46 if (!nativeVideo) 47 throw new Error(`native video reference is not defined.`); 48 nativeVideo.setNativeProps(nativeProps); 49 } 50 // Internal methods 51 _handleNewStatus = (status) => { 52 if (this.state.showPoster && 53 status.isLoaded && 54 (status.isPlaying || status.positionMillis !== 0)) { 55 this.setState({ showPoster: false }); 56 } 57 if (this.props.onPlaybackStatusUpdate) { 58 this.props.onPlaybackStatusUpdate(status); 59 } 60 if (this._onPlaybackStatusUpdate) { 61 this._onPlaybackStatusUpdate(status); 62 } 63 }; 64 _performOperationAndHandleStatusAsync = async (operation) => { 65 const video = this._nativeRef.current; 66 if (!video) { 67 throw new Error(`Cannot complete operation because the Video component has not yet loaded`); 68 } 69 const handle = findNodeHandle(this._nativeRef.current); 70 const status = await operation(handle); 71 this._handleNewStatus(status); 72 return status; 73 }; 74 // Fullscreening API 75 _setFullscreen = async (value) => { 76 return this._performOperationAndHandleStatusAsync((tag) => ExpoVideoViewManager.setFullscreen(tag, value)); 77 }; 78 /** 79 * 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`, 80 * native controls will be visible in fullscreen mode. 81 * @return A `Promise` that is fulfilled with the `AVPlaybackStatus` of the video once the fullscreen player has finished presenting, 82 * or rejects if there was an error, or if this was called on an Android device. 83 */ 84 presentFullscreenPlayer = async () => { 85 return this._setFullscreen(true); 86 }; 87 /** 88 * This dismisses the fullscreen video view. 89 * @return A `Promise` that is fulfilled with the `AVPlaybackStatus` of the video once the fullscreen player has finished dismissing, 90 * or rejects if there was an error, or if this was called on an Android device. 91 */ 92 dismissFullscreenPlayer = async () => { 93 return this._setFullscreen(false); 94 }; 95 // ### Unified playback API ### (consistent with Audio.js) 96 // All calls automatically call onPlaybackStatusUpdate as a side effect. 97 /** 98 * @hidden 99 */ 100 getStatusAsync = async () => { 101 return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.getStatusForVideo(tag)); 102 }; 103 /** 104 * @hidden 105 */ 106 loadAsync = async (source, initialStatus = {}, downloadFirst = true) => { 107 const { nativeSource, fullInitialStatus } = await getNativeSourceAndFullInitialStatusForLoadAsync(source, initialStatus, downloadFirst); 108 return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.loadForVideo(tag, nativeSource, fullInitialStatus)); 109 }; 110 /** 111 * Equivalent to setting URI to `null`. 112 * @hidden 113 */ 114 unloadAsync = async () => { 115 return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.unloadForVideo(tag)); 116 }; 117 componentWillUnmount() { 118 // Auto unload video to perform necessary cleanup safely 119 this.unloadAsync().catch(() => { 120 // Ignored rejection. Sometimes the unloadAsync code is executed when video is already unloaded. 121 // In such cases, it throws: 122 // "[Unhandled promise rejection: Error: Invalid view returned from registry, 123 // expecting EXVideo, got: (null)]" 124 }); 125 } 126 /** 127 * Set status API, only available while `isLoaded = true`. 128 * @hidden 129 */ 130 setStatusAsync = async (status) => { 131 assertStatusValuesInBounds(status); 132 return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.setStatusForVideo(tag, status)); 133 }; 134 /** 135 * @hidden 136 */ 137 replayAsync = async (status = {}) => { 138 if (status.positionMillis && status.positionMillis !== 0) { 139 throw new Error('Requested position after replay has to be 0.'); 140 } 141 return this._performOperationAndHandleStatusAsync((tag) => ExponentAV.replayVideo(tag, { 142 ...status, 143 positionMillis: 0, 144 shouldPlay: true, 145 })); 146 }; 147 /** 148 * Sets a function to be called regularly with the `AVPlaybackStatus` of the playback object. 149 * 150 * `onPlaybackStatusUpdate` will be called whenever a call to the API for this playback object completes 151 * (such as `setStatusAsync()`, `getStatusAsync()`, or `unloadAsync()`), nd will also be called at regular intervals 152 * while the media is in the loaded state. 153 * 154 * Set `progressUpdateIntervalMillis` via `setStatusAsync()` or `setProgressUpdateIntervalAsync()` to modify 155 * the interval with which `onPlaybackStatusUpdate` is called while loaded. 156 * 157 * @param onPlaybackStatusUpdate A function taking a single parameter `AVPlaybackStatus`. 158 */ 159 setOnPlaybackStatusUpdate(onPlaybackStatusUpdate) { 160 this._onPlaybackStatusUpdate = onPlaybackStatusUpdate; 161 this.getStatusAsync(); 162 } 163 // Methods of the Playback interface that are set via PlaybackMixin 164 playAsync; 165 playFromPositionAsync; 166 pauseAsync; 167 stopAsync; 168 setPositionAsync; 169 setRateAsync; 170 setVolumeAsync; 171 setIsMutedAsync; 172 setIsLoopingAsync; 173 setProgressUpdateIntervalAsync; 174 // Callback wrappers 175 _nativeOnPlaybackStatusUpdate = (event) => { 176 this._handleNewStatus(event.nativeEvent); 177 }; 178 // TODO make sure we are passing the right stuff 179 _nativeOnLoadStart = () => { 180 if (this.props.onLoadStart) { 181 this.props.onLoadStart(); 182 } 183 }; 184 _nativeOnLoad = (event) => { 185 if (this.props.onLoad) { 186 this.props.onLoad(event.nativeEvent); 187 } 188 this._handleNewStatus(event.nativeEvent); 189 }; 190 _nativeOnError = (event) => { 191 const error = event.nativeEvent.error; 192 if (this.props.onError) { 193 this.props.onError(error); 194 } 195 this._handleNewStatus(getUnloadedStatus(error)); 196 }; 197 _nativeOnReadyForDisplay = (event) => { 198 if (this.props.onReadyForDisplay) { 199 this.props.onReadyForDisplay(event.nativeEvent); 200 } 201 }; 202 _nativeOnFullscreenUpdate = (event) => { 203 if (this.props.onFullscreenUpdate) { 204 this.props.onFullscreenUpdate(event.nativeEvent); 205 } 206 }; 207 _renderPoster = () => { 208 const PosterComponent = this.props.PosterComponent ?? Image; 209 return this.props.usePoster && this.state.showPoster ? (React.createElement(PosterComponent, { style: [_STYLES.poster, this.props.posterStyle], source: this.props.posterSource })) : null; 210 }; 211 render() { 212 const source = getNativeSourceFromSource(this.props.source) || undefined; 213 let nativeResizeMode = ExpoVideoManagerConstants.ScaleNone; 214 if (this.props.resizeMode) { 215 const resizeMode = this.props.resizeMode; 216 if (resizeMode === ResizeMode.STRETCH) { 217 nativeResizeMode = ExpoVideoManagerConstants.ScaleToFill; 218 } 219 else if (resizeMode === ResizeMode.CONTAIN) { 220 nativeResizeMode = ExpoVideoManagerConstants.ScaleAspectFit; 221 } 222 else if (resizeMode === ResizeMode.COVER) { 223 nativeResizeMode = ExpoVideoManagerConstants.ScaleAspectFill; 224 } 225 } 226 // Set status via individual props 227 const status = { ...this.props.status }; 228 [ 229 'progressUpdateIntervalMillis', 230 'positionMillis', 231 'shouldPlay', 232 'rate', 233 'shouldCorrectPitch', 234 'volume', 235 'isMuted', 236 'isLooping', 237 ].forEach((prop) => { 238 if (prop in this.props) { 239 status[prop] = this.props[prop]; 240 } 241 }); 242 // Replace selected native props 243 const nativeProps = { 244 ...omit(this.props, [ 245 'source', 246 'onPlaybackStatusUpdate', 247 'usePoster', 248 'posterSource', 249 'posterStyle', 250 ...Object.keys(status), 251 ]), 252 style: [_STYLES.base, this.props.style], 253 videoStyle: [_STYLES.video, this.props.videoStyle], 254 source, 255 resizeMode: nativeResizeMode, 256 status, 257 onStatusUpdate: this._nativeOnPlaybackStatusUpdate, 258 onLoadStart: this._nativeOnLoadStart, 259 onLoad: this._nativeOnLoad, 260 onError: this._nativeOnError, 261 onReadyForDisplay: this._nativeOnReadyForDisplay, 262 onFullscreenUpdate: this._nativeOnFullscreenUpdate, 263 }; 264 return (React.createElement(View, { style: nativeProps.style, pointerEvents: "box-none" }, 265 React.createElement(ExponentVideo, { ref: this._nativeRef, ...nativeProps, style: nativeProps.videoStyle }), 266 this._renderPoster())); 267 } 268} 269function omit(props, propNames) { 270 const copied = { ...props }; 271 for (const propName of propNames) { 272 delete copied[propName]; 273 } 274 return copied; 275} 276Object.assign(Video.prototype, PlaybackMixin); 277// note(simek): TypeDoc cannot resolve correctly name of inline and default exported class 278export default Video; 279//# sourceMappingURL=Video.js.map