xref: /expo/packages/expo-av/build/Video.js (revision 8a5984c1)
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