react-native-youtube-bridge
Version:
🎥 Easy-to-use YouTube player for React Native with cross-platform support
250 lines (249 loc) • 8.09 kB
JavaScript
;
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import WebView from 'react-native-webview';
import YoutubePlayerWrapper from "./YoutubePlayerWrapper.js";
import useCreateLocalPlayerHtml from "./hooks/useCreateLocalPlayerHtml.js";
import useYouTubeVideoId from "./hooks/useYoutubeVideoId.js";
import { safeNumber, validateVideoId } from "./utils/validate.js";
import { jsx as _jsx } from "react/jsx-runtime";
const {
width: screenWidth
} = Dimensions.get('window');
const YoutubePlayer = /*#__PURE__*/forwardRef(({
source,
width = screenWidth,
height = 200,
progressInterval,
onReady,
onStateChange,
onError,
onProgress,
onPlaybackRateChange,
onPlaybackQualityChange,
onAutoplayBlocked,
style,
playerVars = {
startTime: 0,
autoplay: false,
controls: true,
loop: false,
muted: false,
playsinline: true,
rel: false
},
webViewStyle,
webViewProps
}, ref) => {
const {
startTime = 0,
endTime
} = playerVars;
const videoId = useYouTubeVideoId(source);
const webViewRef = useRef(null);
const [isReady, setIsReady] = useState(false);
const commandIdRef = useRef(0);
const pendingCommandsRef = useRef(new Map());
const dataDetectorTypes = useMemo(() => ['none'], []);
const createPlayerHTML = useCreateLocalPlayerHtml({
videoId,
...playerVars
});
const handleMessage = useCallback(event => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === 'ready') {
const {
playerInfo
} = data;
setIsReady(true);
onReady?.({
availablePlaybackRates: playerInfo.availablePlaybackRates,
availableQualityLevels: playerInfo.availableQualityLevels,
currentTime: playerInfo.currentTime,
duration: playerInfo.duration,
muted: playerInfo.muted,
playbackQuality: playerInfo.playbackQuality,
playbackRate: playerInfo.playbackRate,
playerState: playerInfo.playerState,
size: playerInfo.size,
volume: playerInfo.volume
});
return;
}
if (data.type === 'stateChange') {
const state = data.state;
onStateChange?.(state);
return;
}
if (data.type === 'error') {
onError?.(data.error);
return;
}
if (data.type === 'progress') {
onProgress?.(data);
return;
}
if (data.type === 'playbackRateChange') {
onPlaybackRateChange?.(data.playbackRate);
return;
}
if (data.type === 'playbackQualityChange') {
onPlaybackQualityChange?.(data.quality);
return;
}
if (data.type === 'autoplayBlocked') {
onAutoplayBlocked?.();
return;
}
if (data.type === 'commandResult') {
const resolver = pendingCommandsRef.current.get(data.id);
if (resolver) {
resolver(data.result);
pendingCommandsRef.current.delete(data.id);
}
return;
}
} catch (error) {
console.error('Error parsing WebView message:', error);
onError?.({
code: 1000,
message: 'FAILED_TO_PARSE_WEBVIEW_MESSAGE'
});
}
}, [onReady, onStateChange, onError, onProgress, onPlaybackRateChange, onPlaybackQualityChange, onAutoplayBlocked]);
const sendCommand = useCallback((command, args = [], needsResult = false
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
) => {
return new Promise(resolve => {
if (!webViewRef.current || !isReady) {
resolve(null);
return;
}
const messageId = needsResult ? (++commandIdRef.current).toString() : undefined;
if (needsResult && messageId) {
const timeout = setTimeout(() => {
pendingCommandsRef.current.delete(messageId);
console.warn('Command timeout:', command, messageId);
resolve(null);
}, 5000);
pendingCommandsRef.current.set(messageId, result => {
clearTimeout(timeout);
resolve(result);
});
}
const commandData = {
command,
args,
...(messageId && {
id: messageId
})
};
const injectedJS = /*js*/`
window.__execCommand && window.__execCommand(${JSON.stringify(commandData)}); true;
`;
webViewRef.current?.injectJavaScript(injectedJS);
if (!needsResult) {
resolve(null);
}
});
}, [isReady]);
useEffect(() => {
if (!isReady) {
return;
}
if (!validateVideoId(videoId)) {
onError?.({
code: 1002,
message: 'INVALID_YOUTUBE_VIDEO_ID'
});
return;
}
sendCommand('loadVideoById', [videoId, startTime, endTime]);
}, [videoId, startTime, endTime, isReady, sendCommand, onError]);
useImperativeHandle(ref, () => ({
play: () => sendCommand('play'),
pause: () => sendCommand('pause'),
stop: () => sendCommand('stop'),
seekTo: (seconds, allowSeekAhead = true) => sendCommand('seekTo', [seconds, allowSeekAhead]),
setVolume: volume => sendCommand('setVolume', [volume]),
getVolume: () => sendCommand('getVolume', [], true),
mute: () => sendCommand('mute'),
unMute: () => sendCommand('unMute'),
isMuted: () => sendCommand('isMuted', [], true),
getCurrentTime: () => sendCommand('getCurrentTime', [], true),
getDuration: () => sendCommand('getDuration', [], true),
getVideoUrl: () => sendCommand('getVideoUrl', [], true),
getVideoEmbedCode: () => sendCommand('getVideoEmbedCode', [], true),
getPlaybackRate: () => sendCommand('getPlaybackRate', [], true),
setPlaybackRate: rate => {
sendCommand('setPlaybackRate', [rate]);
},
getAvailablePlaybackRates: () => sendCommand('getAvailablePlaybackRates', [], true),
getPlayerState: () => sendCommand('getPlayerState', [], true),
getVideoLoadedFraction: () => sendCommand('getVideoLoadedFraction', [], true),
loadVideoById: (videoId, startSeconds, endSeconds) => sendCommand('loadVideoById', [videoId, startSeconds, endSeconds]),
cueVideoById: (videoId, startSeconds, endSeconds) => sendCommand('cueVideoById', [videoId, startSeconds, endSeconds]),
setSize: (width, height) => sendCommand('setSize', [width, height])
}), [sendCommand]);
useEffect(() => {
return () => {
pendingCommandsRef.current.clear();
if (webViewRef.current && isReady) {
sendCommand('cleanup');
}
};
}, [isReady, sendCommand]);
useEffect(() => {
if (isReady) {
const safeInterval = safeNumber(progressInterval);
sendCommand('updateProgressInterval', [safeInterval]);
}
}, [progressInterval, isReady, sendCommand]);
return /*#__PURE__*/_jsx(YoutubePlayerWrapper, {
width: width,
height: height,
style: style,
children: /*#__PURE__*/_jsx(WebView, {
ref: webViewRef,
source: {
html: createPlayerHTML()
},
style: [styles.webView, webViewStyle],
onMessage: handleMessage,
...webViewProps,
javaScriptEnabled: true,
domStorageEnabled: true,
allowsFullscreenVideo: true,
allowsInlineMediaPlayback: true,
bounces: false,
scrollEnabled: false,
mediaPlaybackRequiresUserAction: false,
originWhitelist: ['*'],
onError: error => {
console.error('WebView error:', error);
onError?.({
code: 1001,
message: 'WEBVIEW_LOADING_ERROR'
});
}
// iOS specific props
,
allowsLinkPreview: false,
dataDetectorTypes: dataDetectorTypes
// Android specific props
,
mixedContentMode: "compatibility",
thirdPartyCookiesEnabled: false
})
});
});
const styles = StyleSheet.create({
webView: {
flex: 1,
backgroundColor: 'transparent'
}
});
YoutubePlayer.displayName = 'YoutubePlayer';
export default YoutubePlayer;
//# sourceMappingURL=YoutubePlayer.js.map