1 | import omit from 'lodash/omit';
|
2 | import nullthrows from 'nullthrows';
|
3 | import PropTypes from 'prop-types';
|
4 | import * as React from 'react';
|
5 | import {
|
6 | findNodeHandle,
|
7 | Image,
|
8 | NativeMethods,
|
9 | StyleSheet,
|
10 | View,
|
11 | ViewPropTypes,
|
12 | } from 'react-native';
|
13 |
|
14 | import {
|
15 | assertStatusValuesInBounds,
|
16 | getNativeSourceAndFullInitialStatusForLoadAsync,
|
17 | getNativeSourceFromSource,
|
18 | getUnloadedStatus,
|
19 | Playback,
|
20 | PlaybackMixin,
|
21 | AVPlaybackSource,
|
22 | AVPlaybackStatus,
|
23 | AVPlaybackStatusToSet,
|
24 | AVPlaybackNativeSource,
|
25 | } from './AV';
|
26 | import ExpoVideoManager from './ExpoVideoManager';
|
27 | import ExponentAV from './ExponentAV';
|
28 | import ExponentVideo from './ExponentVideo';
|
29 | import {
|
30 | ExponentVideoComponent,
|
31 | VideoFullscreenUpdateEvent,
|
32 | VideoNativeProps,
|
33 | VideoNaturalSize,
|
34 | VideoProps,
|
35 | VideoReadyForDisplayEvent,
|
36 | ResizeMode,
|
37 | VideoState,
|
38 | } from './Video.types';
|
39 |
|
40 | export {
|
41 | ExponentVideoComponent,
|
42 | VideoFullscreenUpdateEvent,
|
43 | VideoNativeProps,
|
44 | VideoNaturalSize,
|
45 | VideoProps,
|
46 | VideoReadyForDisplayEvent,
|
47 | ResizeMode,
|
48 | VideoState,
|
49 | AVPlaybackStatus,
|
50 | AVPlaybackStatusToSet,
|
51 | AVPlaybackNativeSource,
|
52 | };
|
53 |
|
54 | export const FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT = 0;
|
55 | export const FULLSCREEN_UPDATE_PLAYER_DID_PRESENT = 1;
|
56 | export const FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS = 2;
|
57 | export const FULLSCREEN_UPDATE_PLAYER_DID_DISMISS = 3;
|
58 |
|
59 | export const IOS_FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT = FULLSCREEN_UPDATE_PLAYER_WILL_PRESENT;
|
60 | export const IOS_FULLSCREEN_UPDATE_PLAYER_DID_PRESENT = FULLSCREEN_UPDATE_PLAYER_DID_PRESENT;
|
61 | export const IOS_FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS = FULLSCREEN_UPDATE_PLAYER_WILL_DISMISS;
|
62 | export const IOS_FULLSCREEN_UPDATE_PLAYER_DID_DISMISS = FULLSCREEN_UPDATE_PLAYER_DID_DISMISS;
|
63 |
|
64 | const _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 |
|
86 |
|
87 | const ExpoVideoManagerConstants = ExpoVideoManager;
|
88 | const ExpoVideoViewManager = ExpoVideoManager;
|
89 |
|
90 | export 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 |
|
107 | source: PropTypes.oneOfType([
|
108 | PropTypes.shape({
|
109 | uri: PropTypes.string,
|
110 | overrideFileExtensionAndroid: PropTypes.string,
|
111 | }),
|
112 | PropTypes.number,
|
113 | ]),
|
114 | posterSource: PropTypes.oneOfType([
|
115 | PropTypes.shape({
|
116 | uri: PropTypes.string,
|
117 | }),
|
118 | PropTypes.number,
|
119 | ]),
|
120 | posterStyle: ViewPropTypes.style,
|
121 |
|
122 |
|
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 |
|
132 | useNativeControls: PropTypes.bool,
|
133 | resizeMode: PropTypes.string,
|
134 | usePoster: PropTypes.bool,
|
135 |
|
136 |
|
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 |
|
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 |
|
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 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 | getStatusAsync = async (): Promise<AVPlaybackStatus> => {
|
255 | return this._performOperationAndHandleStatusAsync((tag: number) =>
|
256 | ExponentAV.getStatusForVideo(tag)
|
257 | );
|
258 | };
|
259 |
|
260 |
|
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 |
|
277 | unloadAsync = async (): Promise<AVPlaybackStatus> => {
|
278 | return this._performOperationAndHandleStatusAsync((tag: number) =>
|
279 | ExponentAV.unloadForVideo(tag)
|
280 | );
|
281 | };
|
282 |
|
283 |
|
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 |
|
332 |
|
333 | _nativeOnPlaybackStatusUpdate = (event: { nativeEvent: AVPlaybackStatus }) => {
|
334 | this._handleNewStatus(event.nativeEvent);
|
335 | };
|
336 |
|
337 |
|
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 |
|
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 |
|
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 |
|
424 |
|
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 |
|
455 | Object.assign(Video.prototype, PlaybackMixin);
|