@react-native-youtube-bridge/core
Version:
Core package for react-native-youtube-bridge
353 lines (348 loc) • 10.2 kB
JavaScript
//#region src/types/index.ts
let PlayerState = /* @__PURE__ */ function(PlayerState$1) {
PlayerState$1[PlayerState$1["UNSTARTED"] = -1] = "UNSTARTED";
PlayerState$1[PlayerState$1["ENDED"] = 0] = "ENDED";
PlayerState$1[PlayerState$1["PLAYING"] = 1] = "PLAYING";
PlayerState$1[PlayerState$1["PAUSED"] = 2] = "PAUSED";
PlayerState$1[PlayerState$1["BUFFERING"] = 3] = "BUFFERING";
PlayerState$1[PlayerState$1["CUED"] = 5] = "CUED";
return PlayerState$1;
}({});
//#endregion
//#region src/constants.ts
const ERROR_CODES = {
2: "INVALID_PARAMETER_VALUE",
5: "HTML5_PLAYER_ERROR",
100: "VIDEO_NOT_FOUND_OR_PRIVATE",
101: "EMBEDDED_PLAYBACK_NOT_ALLOWED",
150: "EMBEDDED_RESTRICTED",
1e3: "FAILED_TO_PARSE_WEBVIEW_MESSAGE",
1001: "WEBVIEW_LOADING_ERROR",
1002: "INVALID_YOUTUBE_VIDEO_ID",
1003: "FAILED_TO_LOAD_YOUTUBE_API",
1004: "UNKNOWN_ERROR"
};
const MATCH_URL_YOUTUBE = /(?:youtu\.be\/|youtube(?:-nocookie|education)?\.com\/(?:embed\/|v\/|watch\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))((\w|-){11})/;
//#endregion
//#region src/utils.ts
const extractVideoIdFromUrl = (url) => {
if (!url) return void 0;
const match = url.match(MATCH_URL_YOUTUBE);
return match ? match[1] : void 0;
};
const validateVideoId = (videoId) => {
if (!videoId) return false;
const videoIdRegex = /^[\w-]{11}$/;
return videoIdRegex.test(videoId);
};
const escapeHtml = (unsafe) => {
if (!unsafe) return "";
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
};
const safeNumber = (value, defaultValue = 0) => {
if (typeof value !== "number" || Number.isNaN(value)) return defaultValue;
return Math.max(0, Math.floor(value));
};
//#endregion
//#region src/WebYoutubePlayerController.ts
var WebYoutubePlayerController = class WebYoutubePlayerController {
player = null;
progressInterval = null;
callbacks = {};
progressIntervalMs = 1e3;
seekTimeout = null;
static createInstance() {
return new WebYoutubePlayerController();
}
static async initialize() {
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,
origin: config.playerVars?.origin,
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
});
}
getPlayer() {
return this.player;
}
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;
}
async setPlaybackRate(suggestedRate) {
await 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);
}
updateProgressInterval(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;
}
}
};
var WebYoutubePlayerController_default = WebYoutubePlayerController;
//#endregion
exports.ERROR_CODES = ERROR_CODES;
exports.MATCH_URL_YOUTUBE = MATCH_URL_YOUTUBE;
exports.PlayerState = PlayerState;
exports.WebYoutubePlayerController = WebYoutubePlayerController_default;
exports.escapeHtml = escapeHtml;
exports.extractVideoIdFromUrl = extractVideoIdFromUrl;
exports.safeNumber = safeNumber;
exports.validateVideoId = validateVideoId;