@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
JavaScript
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);