react-native-youtube-bridge
Version:
🎥 Easy-to-use YouTube player for React Native with cross-platform support
313 lines (312 loc) • 8.94 kB
JavaScript
"use strict";
import { ERROR_CODES, PlayerState } from "../types/youtube.js";
import { validateVideoId } from "../utils/validate.js";
class YouTubePlayerCore {
player = null;
progressInterval = null;
callbacks = {};
progressIntervalMs = 1000;
seekTimeout = null;
constructor(callbacks = {}) {
this.callbacks = callbacks;
}
static async loadAPI() {
if (typeof window === 'undefined' || window.YT?.Player) {
return Promise.resolve();
}
if (window._ytApiPromise) {
return window._ytApiPromise;
}
window._ytApiPromise = new Promise(resolve => {
if (document.querySelector('script[src*="youtube.com/iframe_api"]')) {
let attempts = 0;
const maxAttempts = 100;
const checkAPI = () => {
if (window.YT?.Player) {
resolve();
return;
}
if (attempts >= maxAttempts) {
console.error('YouTube API failed to load after timeout');
resolve();
return;
}
attempts++;
setTimeout(checkAPI, 100);
};
checkAPI();
return;
}
window.onYouTubeIframeAPIReady = () => {
resolve();
};
const script = document.createElement('script');
script.src = 'https://www.youtube.com/iframe_api';
script.async = true;
document.head.appendChild(script);
});
return window._ytApiPromise;
}
createPlayer(containerId, config) {
if (typeof window === 'undefined' || !window.YT?.Player) {
return;
}
const container = document.getElementById(containerId);
if (!container) {
return;
}
if (!validateVideoId(config.videoId)) {
this.callbacks.onError?.({
code: 1002,
message: ERROR_CODES[1002]
});
return;
}
if (this.player) {
try {
this.player.destroy();
} catch (error) {
console.warn('Error destroying YouTube player:', error);
}
}
this.player = new window.YT.Player(containerId, {
width: '100%',
height: '100%',
videoId: config.videoId,
playerVars: {
autoplay: config.playerVars?.autoplay ? 1 : 0,
controls: config.playerVars?.controls ? 1 : 0,
loop: config.playerVars?.loop ? 1 : 0,
start: config.playerVars?.startTime,
end: config.playerVars?.endTime,
playsinline: config.playerVars?.playsinline ? 1 : 0,
rel: config.playerVars?.rel ? 1 : 0,
enablejsapi: 1
},
events: {
onReady: event => {
const {
playerInfo
} = event.target;
this.callbacks.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
});
this.startProgressTracking();
},
onStateChange: event => {
const state = event.data;
this.callbacks.onStateChange?.(state);
this.handleStateChange(state);
},
onError: event => {
console.error('YouTube player error:', event.data);
const errorCode = event.data;
if (ERROR_CODES[errorCode]) {
this.callbacks.onError?.({
code: errorCode,
message: ERROR_CODES[errorCode]
});
return;
}
this.callbacks.onError?.({
code: 1004,
message: 'UNKNOWN_ERROR'
});
},
onPlaybackQualityChange: event => {
this.callbacks.onPlaybackQualityChange?.(event.data);
},
onPlaybackRateChange: event => {
this.callbacks.onPlaybackRateChange?.(event.data);
},
onAutoplayBlocked: this.callbacks.onAutoplayBlocked
}
});
}
handleStateChange(state) {
if (state === PlayerState.ENDED) {
this.stopProgressTracking();
this.sendProgress();
return;
}
if (state === PlayerState.PLAYING) {
this.startProgressTracking();
return;
}
if (state === PlayerState.PAUSED) {
this.stopProgressTracking();
this.sendProgress();
return;
}
if (state === PlayerState.BUFFERING) {
this.startProgressTracking();
return;
}
if (state === PlayerState.CUED) {
this.stopProgressTracking();
this.sendProgress();
return;
}
this.stopProgressTracking();
}
startProgressTracking() {
if (!this.progressIntervalMs || this.progressInterval) {
return;
}
this.progressInterval = setInterval(async () => {
if (!this.player || !this.player.getCurrentTime) {
this.stopProgressTracking();
return;
}
try {
await this.sendProgress();
} catch (error) {
console.error('Progress tracking error:', error);
this.stopProgressTracking();
}
}, this.progressIntervalMs);
}
stopProgressTracking() {
if (this.progressInterval) {
clearInterval(this.progressInterval);
this.progressInterval = null;
}
}
async sendProgress() {
if (!this.player || !this.player.getCurrentTime) {
return;
}
const currentTime = await this.player.getCurrentTime();
const duration = await this.player.getDuration();
const percentage = duration > 0 ? currentTime / duration * 100 : 0;
const loadedFraction = await this.player.getVideoLoadedFraction();
this.callbacks.onProgress?.({
currentTime,
duration,
percentage,
loadedFraction
});
}
play() {
this.player?.playVideo();
}
pause() {
this.player?.pauseVideo();
}
stop() {
this.player?.stopVideo();
}
async seekTo(seconds, allowSeekAhead = true) {
this.player?.seekTo(seconds, allowSeekAhead);
if (this.seekTimeout) {
clearTimeout(this.seekTimeout);
}
this.seekTimeout = setTimeout(() => {
this.sendProgress();
}, 200);
}
setVolume(volume) {
this.player?.setVolume(volume);
}
async getVolume() {
const volume = await this.player?.getVolume();
return volume ?? 0;
}
mute() {
this.player?.mute();
}
unMute() {
this.player?.unMute();
}
async isMuted() {
const isMuted = await this.player?.isMuted();
return isMuted ?? false;
}
async getCurrentTime() {
const currentTime = await this.player?.getCurrentTime();
return currentTime ?? 0;
}
async getDuration() {
const duration = await this.player?.getDuration();
return duration ?? 0;
}
async getVideoUrl() {
const videoUrl = await this.player?.getVideoUrl();
return videoUrl ?? '';
}
async getVideoEmbedCode() {
const videoEmbedCode = await this.player?.getVideoEmbedCode();
return videoEmbedCode ?? '';
}
async getPlaybackRate() {
const playbackRate = await this.player?.getPlaybackRate();
return playbackRate ?? 1;
}
async getAvailablePlaybackRates() {
const availablePlaybackRates = await this.player?.getAvailablePlaybackRates();
return availablePlaybackRates ?? [1];
}
async getPlayerState() {
const playerState = await this.player?.getPlayerState();
return playerState ?? PlayerState.UNSTARTED;
}
setPlaybackRate(suggestedRate) {
this.player?.setPlaybackRate(suggestedRate);
}
async getVideoLoadedFraction() {
const videoLoadedFraction = await this.player?.getVideoLoadedFraction();
return videoLoadedFraction ?? 0;
}
loadVideoById(videoId, startSeconds, endSeconds) {
this.player?.loadVideoById(videoId, startSeconds, endSeconds);
}
cueVideoById(videoId, startSeconds, endSeconds) {
this.player?.cueVideoById(videoId, startSeconds, endSeconds);
}
setSize(width, height) {
this.player?.setSize(width, height);
}
setProgressInterval(intervalMs) {
this.progressIntervalMs = intervalMs;
if (this.progressInterval) {
this.stopProgressTracking();
}
if (intervalMs) {
this.startProgressTracking();
return;
}
this.stopProgressTracking();
}
updateCallbacks(newCallbacks) {
this.callbacks = {
...this.callbacks,
...newCallbacks
};
}
destroy() {
this.stopProgressTracking();
if (this.seekTimeout) {
clearTimeout(this.seekTimeout);
this.seekTimeout = null;
}
if (this.player) {
try {
this.player.destroy();
} catch (error) {
console.warn('Error destroying YouTube player:', error);
}
this.player = null;
}
}
}
export default YouTubePlayerCore;
//# sourceMappingURL=YouTubePlayerCore.js.map