UNPKG

14.5 kBTypeScriptView Raw
1import omit from 'lodash/omit';
2import nullthrows from 'nullthrows';
3import PropTypes from 'prop-types';
4import * as React from 'react';
5import {
6 findNodeHandle,
7 Image,
8 NativeMethods,
9 StyleSheet,
10 View,
11 ViewPropTypes,
12} from 'react-native';
13
14import {
15 assertStatusValuesInBounds,
16 getNativeSourceAndFullInitialStatusForLoadAsync,
17 getNativeSourceFromSource,
18 getUnloadedStatus,
19 Playback,
20 PlaybackMixin,
21 AVPlaybackSource,
22 AVPlaybackStatus,
23 AVPlaybackStatusToSet,
24 AVPlaybackNativeSource,
25} from './AV';
26import ExpoVideoManager from './ExpoVideoManager';
27import ExponentAV from './ExponentAV';
28import ExponentVideo from './ExponentVideo';
29import {
30 ExponentVideoComponent,
31 VideoFullscreenUpdateEvent,
32 VideoNativeProps,
33 VideoNaturalSize,
34 VideoProps,
35 VideoReadyForDisplayEvent,
36 ResizeMode,
37 VideoState,
38} from './Video.types';
39
40export {
41 ExponentVideoComponent,
42 VideoFullscreenUpdateEvent,
43 VideoNativeProps,
44 VideoNaturalSize,
45 VideoProps,
46 VideoReadyForDisplayEvent,
47 ResizeMode,
48 VideoState,
49 AVPlaybackStatus,
50 AVPlaybackStatusToSet,
51 AVPlaybackNativeSource,
52};
53
54export const FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT = 0;
55export const FULLSCREEN_UPDATE_PLAYER_DID_PRESENT = 1;
56export const FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS = 2;
57export const FULLSCREEN_UPDATE_PLAYER_DID_DISMISS = 3;
58
59export const IOS_FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT = FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT;
60export const IOS_FULLSCREEN_UPDATE_PLAYER_DID_PRESENT = FULLSCREEN_UPDATE_PLAYER_DID_PRESENT;
61export const IOS_FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS = FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS;
62export const IOS_FULLSCREEN_UPDATE_PLAYER_DID_DISMISS = FULLSCREEN_UPDATE_PLAYER_DID_DISMISS;
63
64const _STYLES = StyleSheet.create({
65 base: {
66 overflow: 'hidden',
67 },
68 poster: {
69 position: 'absolute',
70 left: 0,
71 top: 0,
72 right: 0,
73 bottom: 0,
74 resizeMode: 'contain',
75 },
76 video: {
77 position: 'absolute',
78 left: 0,
79 top: 0,
80 right: 0,
81 bottom: 0,
82 },
83});
84
85// On a real device UIManager should be present, however when running offline tests with jest-expo
86// we have to use the provided native module mock to access constants
87const ExpoVideoManagerConstants = ExpoVideoManager;
88const ExpoVideoViewManager = ExpoVideoManager;
89
90export default class Video extends React.Component<VideoProps, VideoState> implements Playback {
91 static RESIZE_MODE_CONTAIN = ResizeMode.CONTAIN;
92 static RESIZE_MODE_COVER = ResizeMode.COVER;
93 static RESIZE_MODE_STRETCH = ResizeMode.STRETCH;
94
95 static IOS_FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT = IOS_FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT;
96 static IOS_FULLSCREEN_UPDATE_PLAYER_DID_PRESENT = IOS_FULLSCREEN_UPDATE_PLAYER_DID_PRESENT;
97 static IOS_FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS = IOS_FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS;
98 static IOS_FULLSCREEN_UPDATE_PLAYER_DID_DISMISS = IOS_FULLSCREEN_UPDATE_PLAYER_DID_DISMISS;
99
100 static FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT = FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT;
101 static FULLSCREEN_UPDATE_PLAYER_DID_PRESENT = FULLSCREEN_UPDATE_PLAYER_DID_PRESENT;
102 static FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS = FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS;
103 static FULLSCREEN_UPDATE_PLAYER_DID_DISMISS = FULLSCREEN_UPDATE_PLAYER_DID_DISMISS;
104
105 static propTypes = {
106 // Source stuff
107 source: PropTypes.oneOfType([
108 PropTypes.shape({
109 uri: PropTypes.string,
110 overrideFileExtensionAndroid: PropTypes.string,
111 }), // remote URI like { uri: 'http://foo/bar.mp4' }
112 PropTypes.number, // asset module like require('./foo/bar.mp4')
113 ]),
114 posterSource: PropTypes.oneOfType([
115 PropTypes.shape({
116 uri: PropTypes.string,
117 }), // remote URI like { uri: 'http://foo/bar.mp4' }
118 PropTypes.number, // asset module like require('./foo/bar.mp4')
119 ]),
120 posterStyle: ViewPropTypes.style,
121
122 // Callbacks
123 onPlaybackStatusUpdate: PropTypes.func,
124 onLoadStart: PropTypes.func,
125 onLoad: PropTypes.func,
126 onError: PropTypes.func,
127 onIOSFullscreenUpdate: PropTypes.func,
128 onFullscreenUpdate: PropTypes.func,
129 onReadyForDisplay: PropTypes.func,
130
131 // UI stuff
132 useNativeControls: PropTypes.bool,
133 resizeMode: PropTypes.string,
134 usePoster: PropTypes.bool,
135
136 // Playback API
137 status: PropTypes.shape({
138 progressUpdateIntervalMillis: PropTypes.number,
139 positionMillis: PropTypes.number,
140 shouldPlay: PropTypes.bool,
141 rate: PropTypes.number,
142 shouldCorrectPitch: PropTypes.bool,
143 volume: PropTypes.number,
144 isMuted: PropTypes.bool,
145 isLooping: PropTypes.bool,
146 }),
147 progressUpdateIntervalMillis: PropTypes.number,
148 positionMillis: PropTypes.number,
149 shouldPlay: PropTypes.bool,
150 rate: PropTypes.number,
151 shouldCorrectPitch: PropTypes.bool,
152 volume: PropTypes.number,
153 isMuted: PropTypes.bool,
154 isLooping: PropTypes.bool,
155
156 // Required by react-native
157 scaleX: PropTypes.number,
158 scaleY: PropTypes.number,
159 translateX: PropTypes.number,
160 translateY: PropTypes.number,
161 rotation: PropTypes.number,
162 ...ViewPropTypes,
163 };
164
165 _nativeRef = React.createRef<InstanceType<ExponentVideoComponent> & NativeMethods>();
166 _onPlaybackStatusUpdate: ((status: AVPlaybackStatus) => void) | null = null;
167
168 // componentOrHandle: null | number | React.Component<any, any> | React.ComponentClass<any>
169
170 constructor(props: VideoProps) {
171 super(props);
172 this.state = {
173 showPoster: !!props.usePoster,
174 };
175 }
176
177 setNativeProps(nativeProps: VideoNativeProps) {
178 const nativeVideo = nullthrows(this._nativeRef.current);
179 nativeVideo.setNativeProps(nativeProps);
180 }
181
182 // Internal methods
183
184 _handleNewStatus = (status: AVPlaybackStatus) => {
185 if (
186 this.state.showPoster &&
187 status.isLoaded &&
188 (status.isPlaying || status.positionMillis !== 0)
189 ) {
190 this.setState({ showPoster: false });
191 }
192
193 if (this.props.onPlaybackStatusUpdate) {
194 this.props.onPlaybackStatusUpdate(status);
195 }
196 if (this._onPlaybackStatusUpdate) {
197 this._onPlaybackStatusUpdate(status);
198 }
199 };
200
201 _performOperationAndHandleStatusAsync = async (
202 operation: (tag: number) => Promise<AVPlaybackStatus>
203 ): Promise<AVPlaybackStatus> => {
204 const video = this._nativeRef.current;
205 if (!video) {
206 throw new Error(`Cannot complete operation because the Video component has not yet loaded`);
207 }
208
209 const handle = findNodeHandle(this._nativeRef.current)!;
210 const status: AVPlaybackStatus = await operation(handle);
211 this._handleNewStatus(status);
212 return status;
213 };
214
215 // ### iOS Fullscreening API ###
216
217 _setFullscreen = async (value: boolean) => {
218 return this._performOperationAndHandleStatusAsync((tag: number) =>
219 ExpoVideoViewManager.setFullscreen(tag, value)
220 );
221 };
222
223 presentFullscreenPlayer = async () => {
224 return this._setFullscreen(true);
225 };
226
227 presentIOSFullscreenPlayer = () => {
228 console.warn(
229 "You're using `presentIOSFullscreenPlayer`. Please migrate your code to use `presentFullscreenPlayer` instead."
230 );
231 return this.presentFullscreenPlayer();
232 };
233
234 presentFullscreenPlayerAsync = async () => {
235 return await this.presentFullscreenPlayer();
236 };
237
238 dismissFullscreenPlayer = async () => {
239 return this._setFullscreen(false);
240 };
241
242 dismissIOSFullscreenPlayer = () => {
243 console.warn(
244 "You're using `dismissIOSFullscreenPlayer`. Please migrate your code to use `dismissFullscreenPlayer` instead."
245 );
246 this.dismissFullscreenPlayer();
247 };
248
249 // ### Unified playback API ### (consistent with Audio.js)
250 // All calls automatically call onPlaybackStatusUpdate as a side effect.
251
252 // Get status API
253
254 getStatusAsync = async (): Promise<AVPlaybackStatus> => {
255 return this._performOperationAndHandleStatusAsync((tag: number) =>
256 ExponentAV.getStatusForVideo(tag)
257 );
258 };
259
260 // Loading / unloading API
261
262 loadAsync = async (
263 source: AVPlaybackSource,
264 initialStatus: AVPlaybackStatusToSet = {},
265 downloadFirst: boolean = true
266 ): Promise<AVPlaybackStatus> => {
267 const {
268 nativeSource,
269 fullInitialStatus,
270 } = await getNativeSourceAndFullInitialStatusForLoadAsync(source, initialStatus, downloadFirst);
271 return this._performOperationAndHandleStatusAsync((tag: number) =>
272 ExponentAV.loadForVideo(tag, nativeSource, fullInitialStatus)
273 );
274 };
275
276 // Equivalent to setting URI to null.
277 unloadAsync = async (): Promise<AVPlaybackStatus> => {
278 return this._performOperationAndHandleStatusAsync((tag: number) =>
279 ExponentAV.unloadForVideo(tag)
280 );
281 };
282
283 // Set status API (only available while isLoaded = true)
284
285 setStatusAsync = async (status: AVPlaybackStatusToSet): Promise<AVPlaybackStatus> => {
286 assertStatusValuesInBounds(status);
287 return this._performOperationAndHandleStatusAsync((tag: number) =>
288 ExponentAV.setStatusForVideo(tag, status)
289 );
290 };
291
292 replayAsync = async (status: AVPlaybackStatusToSet = {}): Promise<AVPlaybackStatus> => {
293 if (status.positionMillis && status.positionMillis !== 0) {
294 throw new Error('Requested position after replay has to be 0.');
295 }
296
297 return this._performOperationAndHandleStatusAsync((tag: number) =>
298 ExponentAV.replayVideo(tag, {
299 ...status,
300 positionMillis: 0,
301 shouldPlay: true,
302 })
303 );
304 };
305
306 setOnPlaybackStatusUpdate(onPlaybackStatusUpdate: ((status: AVPlaybackStatus) => void) | null) {
307 this._onPlaybackStatusUpdate = onPlaybackStatusUpdate;
308 this.getStatusAsync();
309 }
310
311 // Methods of the Playback interface that are set via PlaybackMixin
312 playAsync!: () => Promise<AVPlaybackStatus>;
313 playFromPositionAsync!: (
314 positionMillis: number,
315 tolerances?: { toleranceMillisBefore?: number; toleranceMillisAfter?: number }
316 ) => Promise<AVPlaybackStatus>;
317 pauseAsync!: () => Promise<AVPlaybackStatus>;
318 stopAsync!: () => Promise<AVPlaybackStatus>;
319 setPositionAsync!: (
320 positionMillis: number,
321 tolerances?: { toleranceMillisBefore?: number; toleranceMillisAfter?: number }
322 ) => Promise<AVPlaybackStatus>;
323 setRateAsync!: (rate: number, shouldCorrectPitch: boolean) => Promise<AVPlaybackStatus>;
324 setVolumeAsync!: (volume: number) => Promise<AVPlaybackStatus>;
325 setIsMutedAsync!: (isMuted: boolean) => Promise<AVPlaybackStatus>;
326 setIsLoopingAsync!: (isLooping: boolean) => Promise<AVPlaybackStatus>;
327 setProgressUpdateIntervalAsync!: (
328 progressUpdateIntervalMillis: number
329 ) => Promise<AVPlaybackStatus>;
330
331 // ### Callback wrappers ###
332
333 _nativeOnPlaybackStatusUpdate = (event: { nativeEvent: AVPlaybackStatus }) => {
334 this._handleNewStatus(event.nativeEvent);
335 };
336
337 // TODO make sure we are passing the right stuff
338 _nativeOnLoadStart = () => {
339 if (this.props.onLoadStart) {
340 this.props.onLoadStart();
341 }
342 };
343
344 _nativeOnLoad = (event: { nativeEvent: AVPlaybackStatus }) => {
345 if (this.props.onLoad) {
346 this.props.onLoad(event.nativeEvent);
347 }
348 this._handleNewStatus(event.nativeEvent);
349 };
350
351 _nativeOnError = (event: { nativeEvent: { error: string } }) => {
352 const error: string = event.nativeEvent.error;
353 if (this.props.onError) {
354 this.props.onError(error);
355 }
356 this._handleNewStatus(getUnloadedStatus(error));
357 };
358
359 _nativeOnReadyForDisplay = (event: { nativeEvent: VideoReadyForDisplayEvent }) => {
360 if (this.props.onReadyForDisplay) {
361 this.props.onReadyForDisplay(event.nativeEvent);
362 }
363 };
364
365 _nativeOnFullscreenUpdate = (event: { nativeEvent: VideoFullscreenUpdateEvent }) => {
366 if (this.props.onIOSFullscreenUpdate && this.props.onFullscreenUpdate) {
367 console.warn(
368 "You've supplied both `onIOSFullscreenUpdate` and `onFullscreenUpdate`. You're going to receive updates on both the callbacks."
369 );
370 } else if (this.props.onIOSFullscreenUpdate) {
371 console.warn(
372 "You're using `onIOSFullscreenUpdate`. Please migrate your code to use `onFullscreenUpdate` instead."
373 );
374 }
375
376 if (this.props.onIOSFullscreenUpdate) {
377 this.props.onIOSFullscreenUpdate(event.nativeEvent);
378 }
379
380 if (this.props.onFullscreenUpdate) {
381 this.props.onFullscreenUpdate(event.nativeEvent);
382 }
383 };
384
385 _renderPoster = () =>
386 this.props.usePoster && this.state.showPoster ? (
387 // @ts-ignore: the react-native type declarations are overly restrictive
388 <Image style={[_STYLES.poster, this.props.posterStyle]} source={this.props.posterSource!} />
389 ) : null;
390
391 render() {
392 const source = getNativeSourceFromSource(this.props.source) || undefined;
393
394 let nativeResizeMode = ExpoVideoManagerConstants.ScaleNone;
395 if (this.props.resizeMode) {
396 const resizeMode = this.props.resizeMode;
397 if (resizeMode === ResizeMode.STRETCH) {
398 nativeResizeMode = ExpoVideoManagerConstants.ScaleToFill;
399 } else if (resizeMode === ResizeMode.CONTAIN) {
400 nativeResizeMode = ExpoVideoManagerConstants.ScaleAspectFit;
401 } else if (resizeMode === ResizeMode.COVER) {
402 nativeResizeMode = ExpoVideoManagerConstants.ScaleAspectFill;
403 }
404 }
405
406 // Set status via individual props
407 const status: AVPlaybackStatusToSet = { ...this.props.status };
408 [
409 'progressUpdateIntervalMillis',
410 'positionMillis',
411 'shouldPlay',
412 'rate',
413 'shouldCorrectPitch',
414 'volume',
415 'isMuted',
416 'isLooping',
417 ].forEach(prop => {
418 if (prop in this.props) {
419 status[prop] = this.props[prop];
420 }
421 });
422
423 // Replace selected native props
424 // @ts-ignore: TypeScript thinks "children" is not in the list of props
425 const nativeProps: VideoNativeProps = {
426 ...omit(
427 this.props,
428 'source',
429 'onPlaybackStatusUpdate',
430 'usePoster',
431 'posterSource',
432 ...Object.keys(status)
433 ),
434 style: StyleSheet.flatten([_STYLES.base, this.props.style]),
435 source,
436 resizeMode: nativeResizeMode,
437 status,
438 onStatusUpdate: this._nativeOnPlaybackStatusUpdate,
439 onLoadStart: this._nativeOnLoadStart,
440 onLoad: this._nativeOnLoad,
441 onError: this._nativeOnError,
442 onReadyForDisplay: this._nativeOnReadyForDisplay,
443 onFullscreenUpdate: this._nativeOnFullscreenUpdate,
444 };
445
446 return (
447 <View style={nativeProps.style} pointerEvents="box-none">
448 <ExponentVideo ref={this._nativeRef} {...nativeProps} style={_STYLES.video} />
449 {this._renderPoster()}
450 </View>
451 );
452 }
453}
454
455Object.assign(Video.prototype, PlaybackMixin);