From 9b8f7a9cd1cf963e89bb36511569fb6920dfd6ba Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sat, 11 Sep 2021 03:17:40 +0700 Subject: [PATCH 1/2] Use YouTube API for videoId and playlist updates --- src/PlayerScripts.js | 6 +++++ src/YoutubeIframe.js | 59 ++++++++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/PlayerScripts.js b/src/PlayerScripts.js index 5447eec..9c71cae 100644 --- a/src/PlayerScripts.js +++ b/src/PlayerScripts.js @@ -53,6 +53,12 @@ true; return `player.${func}({playlist: ${playlistJson}, index: ${index}); true;`; }, + + loadVideoById: (videoId, play) => { + const func = play ? 'loadVideoById' : 'cueVideoById'; + + return `player.${func}({videoId: ${JSON.stringify(videoId)}}); true;`; + }, }; export const playMode = { diff --git a/src/YoutubeIframe.js b/src/YoutubeIframe.js index c403f32..4962e1c 100644 --- a/src/YoutubeIframe.js +++ b/src/YoutubeIframe.js @@ -126,6 +126,40 @@ const YoutubeIframe = (props, ref) => { ].forEach(webViewRef.current.injectJavaScript); }, [play, playerReady, mute, volume, playbackRate]); + useEffect(() => { + if (playerReady < 1) { + // no instance of player is ready + return; + } + + webViewRef.current.injectJavaScript( + PLAYER_FUNCTIONS.loadVideoById(videoId, play), + ); + // We do not need `play` prop because we should not + // recall the load function when the prop changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [videoId, playerReady]); + + useEffect(() => { + if (playerReady < 1) { + // no instance of player is ready + return; + } + + if (!playList) { + return; + } + + webViewRef.current.injectJavaScript( + PLAYER_FUNCTIONS.loadPlaylist(playList, playListStartIndex, play), + ); + // We do not need `play` and `playListStartIndex` props because we should not + // recall the load function when the props changes + // 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) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [Array.isArray(playList) ? playList.join('') : playList, playerReady]); + const onWebMessage = useCallback( event => { try { @@ -141,15 +175,6 @@ const YoutubeIframe = (props, ref) => { case 'playerReady': onReady(); setPlayerReady(prev => prev + 1); - if (Array.isArray(playList)) { - webViewRef.current.injectJavaScript( - PLAYER_FUNCTIONS.loadPlaylist( - playList, - playListStartIndex, - play, - ), - ); - } break; case 'playerQualityChange': onPlaybackQualityChange(message.data); @@ -169,13 +194,10 @@ const YoutubeIframe = (props, ref) => { } }, [ - play, onReady, onError, - playList, onChangeState, onFullScreenChange, - playListStartIndex, onPlaybackRateChange, onPlaybackQualityChange, ], @@ -219,15 +241,10 @@ const YoutubeIframe = (props, ref) => { const data = ytScript.urlEncodedJSON; return {uri: base + '?data=' + data}; - }, [ - videoId, - playList, - useLocalHTML, - contentScale, - baseUrlOverride, - allowWebViewZoom, - initialPlayerParams, - ]); + // videoId, playlist, initialPlayerParams are only used once when initializing YT.Player instance + // further changes are handled by injectJavaScript + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [useLocalHTML, contentScale, baseUrlOverride, allowWebViewZoom]); return ( From ba645ebf2f5a4ce3d90b27c49866d23ccdc2e482 Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Tue, 14 Sep 2021 22:59:15 +0700 Subject: [PATCH 2/2] Use ref for playList and videoId --- src/YoutubeIframe.js | 73 ++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/src/YoutubeIframe.js b/src/YoutubeIframe.js index 4962e1c..9123ef6 100644 --- a/src/YoutubeIframe.js +++ b/src/YoutubeIframe.js @@ -1,27 +1,38 @@ +import {EventEmitter} from 'events'; import React, { - useRef, - useState, - useEffect, forwardRef, useCallback, + useEffect, useImperativeHandle, useMemo, + useRef, + useState, } from 'react'; -import {View, StyleSheet, Platform} from 'react-native'; -import {WebView} from './WebView'; +import {Platform, StyleSheet, View} from 'react-native'; import { + CUSTOM_USER_AGENT, + DEFAULT_BASE_URL, PLAYER_ERROR, PLAYER_STATES, - DEFAULT_BASE_URL, - CUSTOM_USER_AGENT, } from './constants'; -import {EventEmitter} from 'events'; import { - playMode, - soundMode, MAIN_SCRIPT, PLAYER_FUNCTIONS, + playMode, + soundMode, } from './PlayerScripts'; +import {WebView} from './WebView'; + +const deepComparePlayList = (lastPlayList, playList) => { + return ( + typeof lastPlayList === typeof playList && + (Array.isArray(lastPlayList) + ? lastPlayList.join('') + : lastPlayList === Array.isArray(playList) + ? playList.join('') + : playList) + ); +}; const YoutubeIframe = (props, ref) => { const { @@ -41,7 +52,7 @@ const YoutubeIframe = (props, ref) => { onError = _err => {}, onReady = _event => {}, playListStartIndex = 0, - initialPlayerParams = {}, + initialPlayerParams, allowWebViewZoom = false, forceAndroidAutoplay = false, onChangeState = _event => {}, @@ -50,6 +61,10 @@ const YoutubeIframe = (props, ref) => { onPlaybackRateChange = _playbackRate => {}, } = props; + const lastVideoIdRef = useRef(videoId); + const lastPlayListRef = useRef(playList); + const initialPlayerParamsRef = useRef(initialPlayerParams || {}); + const webViewRef = useRef(null); const eventEmitter = useRef(new EventEmitter()); const [playerReady, setPlayerReady] = useState(0); @@ -127,18 +142,18 @@ const YoutubeIframe = (props, ref) => { }, [play, playerReady, mute, volume, playbackRate]); useEffect(() => { - if (playerReady < 1) { + if (playerReady < 1 || 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), ); - // We do not need `play` prop because we should not - // recall the load function when the prop changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [videoId, playerReady]); + }, [videoId, play, playerReady]); useEffect(() => { if (playerReady < 1) { @@ -146,19 +161,18 @@ const YoutubeIframe = (props, ref) => { return; } - if (!playList) { + // 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), ); - // We do not need `play` and `playListStartIndex` props because we should not - // recall the load function when the props changes - // 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) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [Array.isArray(playList) ? playList.join('') : playList, playerReady]); + }, [playList, play, playListStartIndex, playerReady]); const onWebMessage = useCallback( event => { @@ -222,9 +236,9 @@ const YoutubeIframe = (props, ref) => { const source = useMemo(() => { const ytScript = MAIN_SCRIPT( - videoId, - playList, - initialPlayerParams, + lastVideoIdRef.current, + lastPlayListRef.current, + initialPlayerParamsRef.current, allowWebViewZoom, contentScale, ); @@ -241,9 +255,6 @@ const YoutubeIframe = (props, ref) => { const data = ytScript.urlEncodedJSON; return {uri: base + '?data=' + data}; - // videoId, playlist, initialPlayerParams are only used once when initializing YT.Player instance - // further changes are handled by injectJavaScript - // eslint-disable-next-line react-hooks/exhaustive-deps }, [useLocalHTML, contentScale, baseUrlOverride, allowWebViewZoom]); return ( @@ -255,7 +266,9 @@ const YoutubeIframe = (props, ref) => { style={[styles.webView, webViewStyle]} mediaPlaybackRequiresUserAction={false} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} - allowsFullscreenVideo={!initialPlayerParams?.preventFullScreen} + allowsFullscreenVideo={ + !initialPlayerParamsRef.current.preventFullScreen + } userAgent={ forceAndroidAutoplay ? Platform.select({android: CUSTOM_USER_AGENT, ios: ''})