UNPKG

@dooboo/react-native-youtube-iframe

Version:

A simple wrapper around the youtube iframe js API for react native

299 lines (272 loc) 8.36 kB
import { CUSTOM_USER_AGENT, DEFAULT_BASE_URL, PLAYER_ERROR, PLAYER_STATES, } from './constants'; import { MAIN_SCRIPT, PLAYER_FUNCTIONS, playMode, soundMode, } from './PlayerScripts'; import {Platform, StyleSheet, View} from 'react-native'; import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import {EventEmitter} from 'events'; import {WebView} from './WebView'; import {deepComparePlayList} from './utils'; const YoutubeIframe = (props, ref) => { const { height, width, videoId, playList, play = false, mute = false, volume = 100, webViewStyle, webViewProps, useLocalHTML, baseUrlOverride, playbackRate = 1, contentScale = 1.0, onError = _err => {}, onReady = _event => {}, playListStartIndex = 0, initialPlayerParams, allowWebViewZoom = false, forceAndroidAutoplay = false, onChangeState = _event => {}, onFullScreenChange = _status => {}, onPlaybackQualityChange = _quality => {}, onPlaybackRateChange = _playbackRate => {}, } = props; const [playerReady, setPlayerReady] = useState(false); const lastVideoIdRef = useRef(videoId); const lastPlayListRef = useRef(playList); const initialPlayerParamsRef = useRef(initialPlayerParams || {}); const webViewRef = useRef(null); const eventEmitter = useRef(new EventEmitter()); useImperativeHandle( ref, () => ({ getVideoUrl: () => { webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.getVideoUrlScript); return new Promise(resolve => { eventEmitter.current.once('getVideoUrl', resolve); }); }, getDuration: () => { webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.durationScript); return new Promise(resolve => { eventEmitter.current.once('getDuration', resolve); }); }, getCurrentTime: () => { webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.currentTimeScript); return new Promise(resolve => { eventEmitter.current.once('getCurrentTime', resolve); }); }, isMuted: () => { webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.isMutedScript); return new Promise(resolve => { eventEmitter.current.once('isMuted', resolve); }); }, getVolume: () => { webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.getVolumeScript); return new Promise(resolve => { eventEmitter.current.once('getVolume', resolve); }); }, getPlaybackRate: () => { webViewRef.current.injectJavaScript( PLAYER_FUNCTIONS.getPlaybackRateScript, ); return new Promise(resolve => { eventEmitter.current.once('getPlaybackRate', resolve); }); }, getAvailablePlaybackRates: () => { webViewRef.current.injectJavaScript( PLAYER_FUNCTIONS.getAvailablePlaybackRatesScript, ); return new Promise(resolve => { eventEmitter.current.once('getAvailablePlaybackRates', resolve); }); }, seekTo: (seconds, allowSeekAhead) => { webViewRef.current.injectJavaScript( PLAYER_FUNCTIONS.seekToScript(seconds, allowSeekAhead), ); }, playVideo: () => { webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.playVideoScript()); }, pauseVideo: () => { webViewRef.current.injectJavaScript( PLAYER_FUNCTIONS.pauseVideoScript(), ); }, stopVideo: () => { webViewRef.current.injectJavaScript(PLAYER_FUNCTIONS.stopVideoScript()); }, }), [], ); useEffect(() => { if (!playerReady) { // no instance of player is ready return; } [ playMode[play], soundMode[mute], PLAYER_FUNCTIONS.setVolume(volume), PLAYER_FUNCTIONS.setPlaybackRate(playbackRate), ].forEach(webViewRef.current.injectJavaScript); }, [play, mute, volume, playbackRate, playerReady]); useEffect(() => { if (!playerReady || lastVideoIdRef.current === videoId) { // no instance of player is ready // or videoId has not changed return; } lastVideoIdRef.current = videoId; webViewRef.current.injectJavaScript( PLAYER_FUNCTIONS.loadVideoById(videoId, play), ); }, [videoId, play, playerReady]); useEffect(() => { if (!playerReady) { // no instance of player is ready return; } // Also, right now, we are helping users by doing "deep" comparisons of playList prop, // but in the next major we should leave the responsibility to user (either via useMemo or moving the array outside) if (!playList || deepComparePlayList(lastPlayListRef.current, playList)) { return; } lastPlayListRef.current = playList; webViewRef.current.injectJavaScript( PLAYER_FUNCTIONS.loadPlaylist(playList, playListStartIndex, play), ); }, [playList, play, playListStartIndex, playerReady]); const onWebMessage = useCallback( event => { try { const message = JSON.parse(event.nativeEvent.data); switch (message.eventType) { case 'fullScreenChange': onFullScreenChange(message.data); break; case 'playerStateChange': onChangeState(PLAYER_STATES[message.data]); break; case 'playerReady': onReady(); setPlayerReady(true); break; case 'playerQualityChange': onPlaybackQualityChange(message.data); break; case 'playerError': onError(PLAYER_ERROR[message.data]); break; case 'playbackRateChange': onPlaybackRateChange(message.data); break; default: eventEmitter.current.emit(message.eventType, message.data); break; } } catch (error) { console.warn('[rn-youtube-iframe]', error); } }, [ onReady, onError, onChangeState, onFullScreenChange, onPlaybackRateChange, onPlaybackQualityChange, ], ); const onShouldStartLoadWithRequest = useCallback( request => { try { const url = request.mainDocumentURL || request.url; const iosFirstLoad = Platform.OS === 'ios' && url === 'about:blank'; const shouldLoad = iosFirstLoad || url.startsWith(baseUrlOverride || DEFAULT_BASE_URL); return shouldLoad; } catch (error) { // defaults to true in case of error // returning false stops the video from loading return true; } }, [baseUrlOverride], ); const source = useMemo(() => { const ytScript = MAIN_SCRIPT( lastVideoIdRef.current, lastPlayListRef.current, initialPlayerParamsRef.current, allowWebViewZoom, contentScale, ); if (useLocalHTML) { const res = {html: ytScript.htmlString}; if (baseUrlOverride) { res.baseUrl = baseUrlOverride; } return res; } const base = baseUrlOverride || DEFAULT_BASE_URL; const data = ytScript.urlEncodedJSON; return {uri: base + '?data=' + data}; }, [useLocalHTML, contentScale, baseUrlOverride, allowWebViewZoom]); return ( <View style={{height, width}}> <WebView bounces={false} originWhitelist={['*']} allowsInlineMediaPlayback style={[styles.webView, webViewStyle]} mediaPlaybackRequiresUserAction={false} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} allowsFullscreenVideo={ !initialPlayerParamsRef.current.preventFullScreen } userAgent={ forceAndroidAutoplay ? Platform.select({android: CUSTOM_USER_AGENT, ios: ''}) : '' } // props above this are override-able // -- {...webViewProps} // -- // add props that should not be allowed to be overridden below source={source} ref={webViewRef} onMessage={onWebMessage} /> </View> ); }; const styles = StyleSheet.create({ webView: {backgroundColor: 'transparent'}, }); export default forwardRef(YoutubeIframe);