UNPKG

13.2 kBJavaScriptView Raw
1// @flow
2
3import React, { Component } from 'react';
4import PropTypes from 'prop-types';
5import {
6 Image,
7 findNodeHandle,
8 NativeModules,
9 Platform,
10 requireNativeComponent,
11 StyleSheet,
12 View,
13 ViewPropTypes,
14} from 'react-native';
15
16import {
17 _COMMON_AV_PLAYBACK_METHODS,
18 _getURIFromSource,
19 _getURIAndFullInitialStatusForLoadAsync,
20 _throwErrorIfValuesOutOfBoundsInStatus,
21 _getUnloadedStatus,
22 type PlaybackSource,
23 type PlaybackStatus,
24 type PlaybackStatusToSet,
25} from './AV';
26
27export type NaturalSize = {
28 width: number,
29 height: number,
30 orientation: 'portrait' | 'landscape',
31};
32
33type ResizeMode = 'contain' | 'cover' | 'stretch';
34
35type ReadyForDisplayEvent = {
36 naturalSize: NaturalSize,
37 status: PlaybackStatus,
38};
39
40type FullscreenUpdateEvent = {
41 fullscreenUpdate: 0 | 1 | 2 | 3,
42 status: PlaybackStatus,
43};
44
45// TODO extend the Props type from Component
46type Props = {
47 // Source stuff
48 source?: PlaybackSource, // { uri: 'http://foo/bar.mp4' }, Asset, or require('./foo/bar.mp4')
49 posterSource?: { uri: string } | number, // { uri: 'http://foo/bar.mp4' } or require('./foo/bar.mp4')
50
51 // Callbacks
52 callback?: (status: PlaybackStatus) => void, // DEPRECATED -- WILL BE REMOVED IN SDK21
53 onPlaybackStatusUpdate?: (status: PlaybackStatus) => void,
54 onLoadStart?: () => void,
55 onLoad?: (status: PlaybackStatus) => void,
56 onError?: (error: string) => void,
57 onReadyForDisplay?: (event: ReadyForDisplayEvent) => void,
58 onIOSFullscreenUpdate?: (event: FullscreenUpdateEvent) => void,
59
60 // UI stuff
61 useNativeControls?: boolean,
62 resizeMode?: ResizeMode,
63 usePoster?: boolean,
64
65 // Playback API
66 status?: PlaybackStatusToSet,
67 progressUpdateIntervalMillis?: number,
68 positionMillis?: number,
69 shouldPlay?: boolean,
70 rate?: number,
71 shouldCorrectPitch?: boolean,
72 volume?: number,
73 isMuted?: boolean,
74 isLooping?: boolean,
75
76 // Required by react-native
77 scaleX?: number,
78 scaleY?: number,
79 translateX?: number,
80 translateY?: number,
81 rotation?: number,
82
83 // plus View props
84};
85
86type NativeProps = {
87 uri?: ?string,
88 nativeResizeMode?: Object,
89 status?: PlaybackStatusToSet,
90 onStatusUpdateNative?: (event: Object) => void,
91 onReadyForDisplayNative?: (event: Object) => void,
92 onFullscreenUpdateNative?: (event: Object) => void,
93 useNativeControls?: boolean,
94
95 // plus View props
96 style?: Object,
97 // etc...
98};
99
100type State = {
101 showPoster: boolean,
102};
103
104export const IOS_FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT = 0;
105export const IOS_FULLSCREEN_UPDATE_PLAYER_DID_PRESENT = 1;
106export const IOS_FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS = 2;
107export const IOS_FULLSCREEN_UPDATE_PLAYER_DID_DISMISS = 3;
108
109const _STYLES = StyleSheet.create({
110 base: {
111 overflow: 'hidden',
112 },
113 poster: {
114 position: 'absolute',
115 left: 0,
116 top: 0,
117 right: 0,
118 bottom: 0,
119 resizeMode: 'contain',
120 },
121});
122
123export default class Video extends Component<Props, State> {
124 static RESIZE_MODE_CONTAIN = 'contain';
125 static RESIZE_MODE_COVER = 'cover';
126 static RESIZE_MODE_STRETCH = 'stretch';
127
128 _root: ExponentVideo;
129
130 constructor(props: Props) {
131 super(props);
132 this.state = {
133 showPoster: props.usePoster != null && props.usePoster,
134 };
135 }
136
137 setNativeProps(nativeProps: NativeProps) {
138 this._root.setNativeProps(nativeProps);
139 }
140
141 // Internal methods
142
143 _assignRoot = (component: ExponentVideo) => {
144 this._root = component;
145 };
146
147 _handleNewStatus = (status: PlaybackStatus) => {
148 if (
149 this.state.showPoster &&
150 status.isLoaded &&
151 (status.isPlaying || status.positionMillis !== 0)
152 ) {
153 this.setState({ showPoster: false });
154 }
155
156 if (this.props.onPlaybackStatusUpdate) {
157 this.props.onPlaybackStatusUpdate(status);
158 } else if (this.props.callback) {
159 // DEPRECATED -- WILL BE REMOVED IN SDK21
160 this.props.callback(status);
161 }
162 };
163
164 _performOperationAndHandleStatusAsync = async (
165 operation: (tag: number) => Promise<PlaybackStatus>
166 ): Promise<PlaybackStatus> => {
167 if (this._root) {
168 const status: PlaybackStatus = await operation(
169 findNodeHandle(this._root)
170 );
171 this._handleNewStatus(status);
172 return status;
173 } else {
174 throw new Error(
175 'Cannot complete operation because the Video component has not yet loaded.'
176 );
177 }
178 };
179
180 // ### iOS Fullscreening API ###
181
182 _setIOSFullscreen = async (value: boolean) => {
183 if (Platform.OS !== 'ios') {
184 throw new Error('Cannot call fullscreen method if the OS is not iOS!');
185 }
186 return this._performOperationAndHandleStatusAsync((tag: number) =>
187 NativeModules.ExponentVideoManager.setFullscreen(tag, value)
188 );
189 };
190
191 presentIOSFullscreenPlayer = async () => {
192 return this._setIOSFullscreen(true);
193 };
194
195 dismissIOSFullscreenPlayer = async () => {
196 return this._setIOSFullscreen(false);
197 };
198
199 // ### Unified playback API ### (consistent with Audio.js)
200 // All calls automatically call onPlaybackStatusUpdate as a side effect.
201
202 // Get status API
203
204 getStatusAsync = async (): Promise<PlaybackStatus> => {
205 return this._performOperationAndHandleStatusAsync((tag: number) =>
206 NativeModules.ExponentAV.getStatusForVideo(tag)
207 );
208 };
209
210 setOnPlaybackStatusUpdate = (
211 onPlaybackStatusUpdate: ?(status: PlaybackStatus) => void
212 ) => {
213 this.setNativeProps({ onPlaybackStatusUpdate });
214 this.getStatusAsync();
215 };
216
217 // DEPRECATED -- WILL BE REMOVED IN SDK21:
218 setCallback = (callback: ?(status: PlaybackStatus) => void) => {
219 console.warn(
220 `'Video.setCallback()' is deprecated and will be removed in SDK21. Use 'Video.setOnPlaybackStatusUpdate()' instead.`
221 );
222 this.setOnPlaybackStatusUpdate(callback);
223 };
224
225 // Loading / unloading API
226
227 loadAsync = async (
228 source: PlaybackSource,
229 initialStatus: PlaybackStatusToSet = {},
230 downloadFirst: boolean = true
231 ): Promise<PlaybackStatus> => {
232 const {
233 uri,
234 fullInitialStatus,
235 } = await _getURIAndFullInitialStatusForLoadAsync(
236 source,
237 initialStatus,
238 downloadFirst
239 );
240 return this._performOperationAndHandleStatusAsync((tag: number) =>
241 NativeModules.ExponentAV.loadForVideo(tag, uri, fullInitialStatus)
242 );
243 };
244
245 // Equivalent to setting URI to null.
246 unloadAsync = async (): Promise<PlaybackStatus> => {
247 return this._performOperationAndHandleStatusAsync((tag: number) =>
248 NativeModules.ExponentAV.unloadForVideo(tag)
249 );
250 };
251
252 // Set status API (only available while isLoaded = true)
253
254 setStatusAsync = async (
255 status: PlaybackStatusToSet
256 ): Promise<PlaybackStatus> => {
257 _throwErrorIfValuesOutOfBoundsInStatus(status);
258 return this._performOperationAndHandleStatusAsync((tag: number) =>
259 NativeModules.ExponentAV.setStatusForVideo(tag, status)
260 );
261 };
262
263 // Additional convenience methods on top of setStatusAsync are defined via _COMMON_AV_PLAYBACK_METHODS:
264 playAsync: () => Promise<PlaybackStatus>;
265 playFromPositionAsync: (positionMillis: number) => Promise<PlaybackStatus>;
266 pauseAsync: () => Promise<PlaybackStatus>;
267 stopAsync: () => Promise<PlaybackStatus>;
268 setPositionAsync: (positionMillis: number) => Promise<PlaybackStatus>;
269 setRateAsync: (
270 rate: number,
271 shouldCorrectPitch: boolean
272 ) => Promise<PlaybackStatus>;
273 setVolumeAsync: (volume: number) => Promise<PlaybackStatus>;
274 setIsMutedAsync: (isMuted: boolean) => Promise<PlaybackStatus>;
275 setIsLoopingAsync: (isLooping: boolean) => Promise<PlaybackStatus>;
276 setProgressUpdateIntervalAsync: (
277 progressUpdateIntervalMillis: number
278 ) => Promise<PlaybackStatus>;
279
280 // ### Callback wrappers ###
281
282 _nativeOnPlaybackStatusUpdate = (event: { nativeEvent: PlaybackStatus }) => {
283 this._handleNewStatus(event.nativeEvent);
284 };
285
286 // TODO make sure we are passing the right stuff
287 _nativeOnLoadStart = (event: SyntheticEvent<*>) => {
288 if (this.props.onLoadStart) {
289 this.props.onLoadStart();
290 }
291 };
292
293 _nativeOnLoad = (event: { nativeEvent: PlaybackStatus }) => {
294 if (this.props.onLoad) {
295 this.props.onLoad(event.nativeEvent);
296 }
297 this._handleNewStatus(event.nativeEvent);
298 };
299
300 _nativeOnError = (event: { nativeEvent: { error: string } }) => {
301 const error: string = event.nativeEvent.error;
302 if (this.props.onError) {
303 this.props.onError(error);
304 }
305 this._handleNewStatus(_getUnloadedStatus(error));
306 };
307
308 _nativeOnReadyForDisplay = (event: { nativeEvent: ReadyForDisplayEvent }) => {
309 if (this.props.onReadyForDisplay) {
310 this.props.onReadyForDisplay(event.nativeEvent);
311 }
312 };
313
314 _nativeOnFullscreenUpdate = (event: {
315 nativeEvent: FullscreenUpdateEvent,
316 }) => {
317 if (this.props.onIOSFullscreenUpdate) {
318 this.props.onIOSFullscreenUpdate(event.nativeEvent);
319 }
320 };
321
322 render() {
323 // DEPRECATED -- WILL BE REMOVED IN SDK21:
324 if (this.props.callback) {
325 console.warn(
326 `The prop 'callback' on Video is deprecated and will be removed in SDK21. Use the new 'onPlaybackStatusUpdate' prop instead.`
327 );
328 }
329
330 const uri: ?string = _getURIFromSource(this.props.source);
331
332 let nativeResizeMode: Object =
333 NativeModules.UIManager.ExponentVideo.Constants.ScaleNone;
334 if (this.props.resizeMode) {
335 let resizeMode: ResizeMode = this.props.resizeMode;
336 if (resizeMode === Video.RESIZE_MODE_STRETCH) {
337 nativeResizeMode =
338 NativeModules.UIManager.ExponentVideo.Constants.ScaleToFill;
339 } else if (resizeMode === Video.RESIZE_MODE_CONTAIN) {
340 nativeResizeMode =
341 NativeModules.UIManager.ExponentVideo.Constants.ScaleAspectFit;
342 } else if (resizeMode === Video.RESIZE_MODE_COVER) {
343 nativeResizeMode =
344 NativeModules.UIManager.ExponentVideo.Constants.ScaleAspectFill;
345 }
346 }
347
348 // Set status via individual props (casting as Object to appease flow)
349 const status: Object = { ...this.props.status };
350 [
351 'progressUpdateIntervalMillis',
352 'positionMillis',
353 'shouldPlay',
354 'rate',
355 'shouldCorrectPitch',
356 'volume',
357 'isMuted',
358 'isLooping',
359 ].forEach(prop => {
360 if (prop in this.props) {
361 status[prop] = this.props[prop];
362 }
363 });
364
365 // Replace selected native props (casting as Object to appease flow)
366 const nativeProps: NativeProps = {
367 style: _STYLES.base,
368 ...this.props,
369 uri,
370 nativeResizeMode,
371 status,
372 onStatusUpdateNative: this._nativeOnPlaybackStatusUpdate,
373 onLoadStartNative: this._nativeOnLoadStart,
374 onLoadNative: this._nativeOnLoad,
375 onErrorNative: this._nativeOnError,
376 onReadyForDisplayNative: this._nativeOnReadyForDisplay,
377 onFullscreenUpdateNative: this._nativeOnFullscreenUpdate,
378 };
379
380 return this.props.usePoster && this.state.showPoster ? (
381 <View style={nativeProps.style}>
382 <ExponentVideo ref={this._assignRoot} {...nativeProps} />
383 <Image style={_STYLES.poster} source={this.props.posterSource} />
384 </View>
385 ) : (
386 <ExponentVideo ref={this._assignRoot} {...nativeProps} />
387 );
388 }
389}
390
391Object.assign(Video.prototype, _COMMON_AV_PLAYBACK_METHODS);
392
393Video.propTypes = {
394 // Source stuff
395 source: PropTypes.oneOfType([
396 PropTypes.shape({
397 uri: PropTypes.string,
398 }), // remote URI like { uri: 'http://foo/bar.mp4' }
399 PropTypes.number, // asset module like require('./foo/bar.mp4')
400 ]),
401 posterSource: PropTypes.oneOfType([
402 PropTypes.shape({
403 uri: PropTypes.string,
404 }), // remote URI like { uri: 'http://foo/bar.mp4' }
405 PropTypes.number, // asset module like require('./foo/bar.mp4')
406 ]),
407
408 // Callbacks
409 callback: PropTypes.func, // DEPRECATED -- WILL BE REMOVED IN SDK21
410 onPlaybackStatusUpdate: PropTypes.func,
411 onLoadStart: PropTypes.func,
412 onLoad: PropTypes.func,
413 onError: PropTypes.func,
414 onIOSFullscreenUpdate: PropTypes.func,
415 onReadyForDisplay: PropTypes.func,
416
417 // UI stuff
418 useNativeControls: PropTypes.bool,
419 resizeMode: PropTypes.string,
420 usePoster: PropTypes.bool,
421
422 // Playback API
423 status: PropTypes.shape({
424 progressUpdateIntervalMillis: PropTypes.number,
425 positionMillis: PropTypes.number,
426 shouldPlay: PropTypes.bool,
427 rate: PropTypes.number,
428 shouldCorrectPitch: PropTypes.bool,
429 volume: PropTypes.number,
430 isMuted: PropTypes.bool,
431 isLooping: PropTypes.bool,
432 }),
433 progressUpdateIntervalMillis: PropTypes.number,
434 positionMillis: PropTypes.number,
435 shouldPlay: PropTypes.bool,
436 rate: PropTypes.number,
437 shouldCorrectPitch: PropTypes.bool,
438 volume: PropTypes.number,
439 isMuted: PropTypes.bool,
440 isLooping: PropTypes.bool,
441
442 // Required by react-native
443 scaleX: PropTypes.number,
444 scaleY: PropTypes.number,
445 translateX: PropTypes.number,
446 translateY: PropTypes.number,
447 rotation: PropTypes.number,
448 ...ViewPropTypes,
449};
450
451const ExponentVideo = requireNativeComponent('ExponentVideo', Video, {
452 nativeOnly: {
453 uri: true,
454 nativeResizeMode: true,
455 onStatusUpdateNative: true,
456 onLoadStartNative: true,
457 onLoadNative: true,
458 onErrorNative: true,
459 onReadyForDisplayNative: true,
460 onFullscreenUpdateNative: true,
461 },
462});