unified-video-framework
Version:
Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more
982 lines • 60.2 kB
JavaScript
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { WebPlayer } from "../WebPlayer.js";
import { GoogleAdsManager } from "../ads/GoogleAdsManager.js";
import { useCommerceSync } from "./hooks/useCommerceSync.js";
import ProductBadge from "./components/commerce/ProductBadge.js";
import ProductPanel from "./components/commerce/ProductPanel.js";
let EPGOverlay = null;
const loadEPGComponents = async () => {
if (!EPGOverlay) {
try {
const epgModule = await import('./components/EPGOverlay');
EPGOverlay = epgModule.default;
}
catch (error) {
console.warn('Failed to load EPG components:', error);
}
}
return EPGOverlay;
};
export const WebPlayerView = (props) => {
const containerRef = useRef(null);
const internalPlayerRef = useRef(null);
const playerRef = props.playerRef || internalPlayerRef;
const [dimensions, setDimensions] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 1920,
height: typeof window !== 'undefined' ? window.innerHeight : 1080,
});
const [currentTime, setCurrentTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [manifest, setManifest] = useState(props.commerce?.manifest);
const [panelOpen, setPanelOpen] = useState(false);
const [userClosedPanel, setUserClosedPanel] = useState(false);
const prevIsPausedRef = useRef(false);
const { activeOverlays, activeProducts } = useCommerceSync(manifest, currentTime, isPaused);
useEffect(() => {
if (!props.commerce?.manifestUrl)
return;
let cancelled = false;
(async () => {
try {
const r = await fetch(props.commerce.manifestUrl);
if (!r.ok)
return;
const json = await r.json();
if (!cancelled)
setManifest(json);
}
catch { }
})();
return () => { cancelled = true; };
}, [props.commerce?.manifestUrl]);
useEffect(() => {
if (!props.commerce?.autoOpenPanel)
return;
if (isPaused && activeProducts.length > 0 && !panelOpen && !userClosedPanel) {
console.log('[COMMERCE] Auto-opening panel on pause with active products');
setPanelOpen(true);
}
if (!isPaused && panelOpen && !userClosedPanel) {
console.log('[COMMERCE] Auto-closing panel when playing');
setPanelOpen(false);
}
if (prevIsPausedRef.current && !isPaused && userClosedPanel) {
console.log('[COMMERCE] Resetting userClosedPanel on play transition');
setUserClosedPanel(false);
}
prevIsPausedRef.current = isPaused;
}, [activeProducts.length, panelOpen, props.commerce?.autoOpenPanel, userClosedPanel, isPaused]);
const [epgVisible, setEPGVisible] = useState(props.showEPG || false);
const [playerReady, setPlayerReady] = useState(false);
const [epgComponentLoaded, setEPGComponentLoaded] = useState(false);
const [hasUsedEPG, setHasUsedEPG] = useState(() => {
try {
return localStorage.getItem('uvf-epg-used') === 'true';
}
catch {
return false;
}
});
useEffect(() => {
const p = playerRef.current;
if (!p || typeof p.on !== 'function')
return;
if (!props.commerce)
return;
const handleTime = (ct) => setCurrentTime(ct);
const handlePause = () => setIsPaused(true);
const handlePlay = () => setIsPaused(false);
p.on('onTimeUpdate', handleTime);
p.on('onPause', handlePause);
p.on('onPlay', handlePlay);
return () => {
if (typeof p.off === 'function') {
p.off('onTimeUpdate', handleTime);
p.off('onPause', handlePause);
p.off('onPlay', handlePlay);
}
};
}, [playerReady, props.commerce]);
const adsManagerRef = useRef(null);
const adContainerRef = useRef(null);
const [isAdPlaying, setIsAdPlaying] = useState(false);
const generateAdMarkers = useCallback((cuePoints, videoDuration) => {
if (!cuePoints || cuePoints.length === 0)
return [];
return cuePoints.map((time, index) => {
let actualTime = time;
let title = 'Ad Break';
if (time === 0) {
actualTime = 0;
title = 'Pre-roll Ad';
}
else if (time === -1) {
actualTime = videoDuration > 0 ? videoDuration - 0.1 : 0;
title = 'Post-roll Ad';
}
else {
title = `Mid-roll Ad ${index + 1}`;
}
return {
id: `ad-${index}-${time}`,
type: 'ad',
startTime: actualTime,
endTime: actualTime + 0.1,
title,
skipLabel: 'Skip Ad',
description: `Advertisement at ${formatAdTime(actualTime)}`,
autoSkip: false,
showSkipButton: false,
metadata: {
adBreakIndex: index,
originalTime: time,
isPreroll: time === 0,
isPostroll: time === -1,
source: 'google-ads',
},
};
});
}, []);
const formatAdTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const mergeAdMarkersWithChapters = useCallback((chapters, adMarkers, videoDuration) => {
if (adMarkers.length === 0)
return chapters;
const customStyles = chapters?.customStyles || {};
if (!chapters || !chapters.data) {
return {
enabled: true,
showChapterMarkers: true,
customStyles,
data: {
videoId: 'auto-generated',
duration: videoDuration,
segments: adMarkers,
},
};
}
const existingSegments = chapters.data?.segments || [];
const allSegments = [...existingSegments, ...adMarkers]
.sort((a, b) => a.startTime - b.startTime);
return {
...chapters,
enabled: true,
showChapterMarkers: true,
customStyles,
data: {
...chapters.data,
segments: allSegments,
},
};
}, []);
useEffect(() => {
if (typeof window === 'undefined')
return;
const responsiveEnabled = props.responsive?.enabled !== false;
if (!responsiveEnabled)
return;
const handleResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, [props.responsive?.enabled]);
const handleToggleEPG = useCallback((visible) => {
setEPGVisible(visible);
if (visible && !hasUsedEPG) {
try {
localStorage.setItem('uvf-epg-used', 'true');
setHasUsedEPG(true);
}
catch (e) {
}
}
if (props.onToggleEPG) {
props.onToggleEPG(visible);
}
}, [props.onToggleEPG, hasUsedEPG]);
useEffect(() => {
if (props.epg && !epgComponentLoaded) {
console.log('🔄 Loading EPG components...');
loadEPGComponents().then((component) => {
if (component) {
console.log('✅ Setting epgComponentLoaded to true');
setEPGComponentLoaded(true);
}
}).catch((error) => {
console.error('Failed to load EPG components:', error);
});
}
}, [props.epg, epgComponentLoaded]);
useEffect(() => {
if (props.showEPG !== undefined) {
setEPGVisible(props.showEPG);
}
}, [props.showEPG]);
useEffect(() => {
if (!props.epg)
return;
const handleKeyPress = (e) => {
if ((e.key === 'g' || e.key === 'G') && e.ctrlKey) {
e.preventDefault();
handleToggleEPG(!epgVisible);
}
};
document.addEventListener('keydown', handleKeyPress);
return () => document.removeEventListener('keydown', handleKeyPress);
}, [props.epg, epgVisible, handleToggleEPG]);
const getResponsiveDimensions = () => {
const responsiveEnabled = props.responsive?.enabled !== false;
if (!responsiveEnabled)
return props.style || {};
const { width, height } = dimensions;
const responsive = props.responsive || {};
const defaults = {
aspectRatio: responsive.aspectRatio || 16 / 9,
maxWidth: responsive.maxWidth || '100vw',
maxHeight: responsive.maxHeight || '100vh',
breakpoints: {
mobile: responsive.breakpoints?.mobile || 768,
tablet: responsive.breakpoints?.tablet || 1024,
},
mobilePortrait: {
maxHeight: responsive.mobilePortrait?.maxHeight || '100vh',
aspectRatio: responsive.mobilePortrait?.aspectRatio,
},
mobileLandscape: {
maxHeight: responsive.mobileLandscape?.maxHeight || '100vh',
aspectRatio: responsive.mobileLandscape?.aspectRatio,
},
tablet: {
maxWidth: responsive.tablet?.maxWidth || '100vw',
maxHeight: responsive.tablet?.maxHeight || '100vh',
},
};
const isMobile = width < defaults.breakpoints.mobile;
const isTablet = width >= defaults.breakpoints.mobile && width < defaults.breakpoints.tablet;
const isPortrait = height > width;
const isLandscape = width > height;
const playerHeight = '100vh';
const playerMaxHeight = '100vh';
const playerZIndex = props.epg ? 50 : 1000;
let calculatedStyle = {
width: '100vw',
height: playerHeight,
maxWidth: '100vw',
maxHeight: playerMaxHeight,
boxSizing: 'border-box',
position: 'fixed',
top: 0,
left: 0,
zIndex: playerZIndex,
backgroundColor: '#000000',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: 0,
padding: 0,
transition: 'none',
...props.style,
};
if (isMobile && isPortrait) {
calculatedStyle = {
...calculatedStyle,
width: '100vw',
height: playerHeight,
maxWidth: '100vw',
maxHeight: playerMaxHeight,
position: 'fixed',
top: 0,
left: 0,
};
}
else if (isMobile && isLandscape) {
calculatedStyle = {
...calculatedStyle,
width: '100vw',
height: playerHeight,
maxWidth: '100vw',
maxHeight: playerMaxHeight,
position: 'fixed',
top: 0,
left: 0,
};
}
else if (isTablet) {
calculatedStyle = {
...calculatedStyle,
width: '100vw',
height: playerHeight,
maxWidth: '100vw',
maxHeight: playerMaxHeight,
position: 'fixed',
top: 0,
left: 0,
};
}
else {
calculatedStyle = {
...calculatedStyle,
width: '100vw',
height: playerHeight,
maxWidth: '100vw',
maxHeight: playerMaxHeight,
position: 'fixed',
top: 0,
left: 0,
};
}
return calculatedStyle;
};
useEffect(() => {
try {
const params = new URLSearchParams(window.location.search);
const popup = (params.get('popup') || '').toLowerCase() === '1';
const status = (params.get('rental') || '').toLowerCase();
const orderId = params.get('order_id') || '';
const sessionId = params.get('session_id') || '';
if (popup && (status === 'success' || status === 'cancel')) {
try {
window.opener?.postMessage({ type: 'uvfCheckout', status, orderId, sessionId }, '*');
}
catch (_) { }
try {
window.close();
}
catch (_) { }
}
}
catch (_) { }
}, []);
useEffect(() => {
let cancelled = false;
async function boot() {
if (!containerRef.current)
return;
const player = new WebPlayer();
playerRef.current = player;
exposeAPIsToParent(player);
if (props.cast) {
try {
const existing = document.querySelector('script[data-cast-sdk="1"]');
if (!existing) {
const s = document.createElement('script');
s.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
s.async = true;
s.setAttribute('data-cast-sdk', '1');
document.head.appendChild(s);
}
}
catch (_) {
}
}
let paywallCfg = props.paywall;
if (!paywallCfg && props.paywallConfigUrl) {
try {
const resp = await fetch(props.paywallConfigUrl);
if (resp.ok)
paywallCfg = await resp.json();
}
catch (_) { }
}
if (props.emailAuth?.enabled) {
if (!paywallCfg) {
paywallCfg = {
enabled: true,
apiBase: 'http://localhost:3000',
userId: 'user-' + Math.random().toString(36).substr(2, 9),
videoId: 'video-' + Math.random().toString(36).substr(2, 9),
gateways: ['stripe'],
};
}
paywallCfg = {
...paywallCfg,
emailAuth: {
enabled: props.emailAuth.enabled,
skipIfAuthenticated: props.emailAuth.skipIfAuthenticated ?? true,
sessionStorage: {
tokenKey: props.emailAuth.sessionStorage?.tokenKey || 'uvf_session_token',
refreshTokenKey: props.emailAuth.sessionStorage?.refreshTokenKey || 'uvf_refresh_token',
userIdKey: props.emailAuth.sessionStorage?.userIdKey || 'uvf_user_id',
},
api: {
requestOtp: props.emailAuth.apiEndpoints?.requestOtp || '/auth/request-otp',
verifyOtp: props.emailAuth.apiEndpoints?.verifyOtp || '/auth/verify-otp',
refreshToken: props.emailAuth.apiEndpoints?.refreshToken || '/auth/refresh-token',
logout: props.emailAuth.apiEndpoints?.logout || '/auth/logout',
},
ui: {
title: props.emailAuth.ui?.title || 'Sign in to continue',
description: props.emailAuth.ui?.description || 'Enter your email to receive a verification code',
emailPlaceholder: props.emailAuth.ui?.emailPlaceholder || 'Enter your email',
otpPlaceholder: props.emailAuth.ui?.otpPlaceholder || 'Enter 6-digit code',
submitButtonText: props.emailAuth.ui?.submitButtonText || 'Send Code',
resendButtonText: props.emailAuth.ui?.resendButtonText || 'Resend Code',
resendCooldown: props.emailAuth.ui?.resendCooldown || 30,
},
validation: {
otpLength: props.emailAuth.validation?.otpLength || 6,
otpTimeout: props.emailAuth.validation?.otpTimeout || 300,
rateLimiting: {
maxAttempts: props.emailAuth.validation?.rateLimiting?.maxAttempts || 5,
windowMinutes: props.emailAuth.validation?.rateLimiting?.windowMinutes || 60,
},
},
},
};
}
let watermarkConfig;
if (typeof props.watermark === 'boolean') {
watermarkConfig = { enabled: props.watermark };
}
else {
watermarkConfig = props.watermark;
}
let chaptersConfig = props.chapters;
if (props.googleAds && !chaptersConfig) {
chaptersConfig = {
enabled: true,
showChapterMarkers: true,
};
}
const config = {
autoPlay: props.autoPlay ?? false,
muted: props.muted ?? false,
volume: props.volume ?? 1.0,
controls: props.controls ?? true,
loop: props.loop ?? false,
preload: props.preload ?? 'metadata',
crossOrigin: props.crossOrigin,
playsInline: props.playsInline ?? true,
defaultQuality: props.defaultQuality,
enableAdaptiveBitrate: props.enableAdaptiveBitrate ?? true,
debug: props.debug ?? false,
startTime: props.startTime,
freeDuration: props.freeDuration,
paywall: paywallCfg,
customControls: props.customControls,
youtubeNativeControls: props.youtubeNativeControls ?? true,
settings: props.settings,
showFrameworkBranding: props.showFrameworkBranding,
watermark: watermarkConfig,
share: props.share,
qualityFilter: props.qualityFilter,
premiumQualities: props.premiumQualities,
navigation: props.navigation,
chapters: chaptersConfig ? {
enabled: chaptersConfig.enabled ?? (props.googleAds ? true : false),
data: chaptersConfig.data,
dataUrl: chaptersConfig.dataUrl,
autoHide: chaptersConfig.autoHide ?? true,
autoHideDelay: chaptersConfig.autoHideDelay ?? 5000,
showChapterMarkers: chaptersConfig.showChapterMarkers ?? true,
skipButtonPosition: chaptersConfig.skipButtonPosition ?? 'bottom-right',
customStyles: chaptersConfig.customStyles,
userPreferences: {
autoSkipIntro: chaptersConfig.userPreferences?.autoSkipIntro ?? false,
autoSkipRecap: chaptersConfig.userPreferences?.autoSkipRecap ?? false,
autoSkipCredits: chaptersConfig.userPreferences?.autoSkipCredits ?? false,
showSkipButtons: chaptersConfig.userPreferences?.showSkipButtons ?? true,
skipButtonTimeout: chaptersConfig.userPreferences?.skipButtonTimeout ?? 5000,
rememberChoices: chaptersConfig.userPreferences?.rememberChoices ?? true,
resumePlaybackAfterSkip: chaptersConfig.userPreferences?.resumePlaybackAfterSkip ?? true,
}
} : { enabled: false }
};
try {
await player.initialize(containerRef.current, config);
try {
if (props.playerTheme && player.setTheme) {
player.setTheme(props.playerTheme);
}
}
catch (_) { }
const source = {
url: props.url,
type: props.type ?? 'auto',
subtitles: props.subtitles,
metadata: props.metadata,
fallbackSources: props.fallbackSources,
fallbackPoster: props.fallbackPoster,
fallbackShowErrorMessage: props.fallbackShowErrorMessage,
fallbackRetryDelay: props.fallbackRetryDelay,
fallbackRetryAttempts: props.fallbackRetryAttempts,
onAllSourcesFailed: props.onAllSourcesFailed,
};
await player.load(source);
if (!cancelled) {
setPlayerReady(true);
if (props.qualityFilter && typeof player.setQualityFilter === 'function') {
player.setQualityFilter(props.qualityFilter);
}
if (props.settingsScrollbar) {
applySettingsScrollbar(player, props.settingsScrollbar);
}
if (props.autoFocusPlayer && typeof player.focusPlayer === 'function') {
player.focusPlayer();
}
if (props.showFullscreenTipOnMount && typeof player.showFullscreenTip === 'function') {
player.showFullscreenTip();
}
const injectAdMarkersFromTimes = (adTimes) => {
if (!adTimes || adTimes.length === 0)
return;
const duration = player.getDuration?.() || 0;
if (duration > 0) {
const adMarkers = generateAdMarkers(adTimes, duration);
const mergedChapters = mergeAdMarkersWithChapters(props.chapters, adMarkers, duration);
if (typeof player.loadChapters === 'function' && mergedChapters.data) {
player.loadChapters(mergedChapters.data);
console.log('✅ Ad markers injected:', adMarkers.length, 'markers at times:', adTimes);
}
}
};
if (props.googleAds) {
const videoDuration = player.getDuration?.() || 0;
if (props.googleAds.midrollTimes && props.googleAds.midrollTimes.length > 0) {
const injectAdMarkers = () => {
injectAdMarkersFromTimes(props.googleAds.midrollTimes);
};
if (videoDuration > 0) {
injectAdMarkers();
}
else {
const videoElement = player.video || player.getVideoElement?.();
if (videoElement) {
videoElement.addEventListener('loadedmetadata', injectAdMarkers, { once: true });
}
}
}
}
if (props.epg && typeof player.setEPGData === 'function') {
player.setEPGData(props.epg);
}
if (typeof player.on === 'function') {
player.on('epgToggle', () => {
handleToggleEPG(!epgVisible);
});
}
if (props.onChapterChange && typeof player.on === 'function') {
player.on('chapterchange', props.onChapterChange);
}
if (props.onSegmentEntered && typeof player.on === 'function') {
player.on('segmententered', props.onSegmentEntered);
}
if (props.onSegmentExited && typeof player.on === 'function') {
player.on('segmentexited', props.onSegmentExited);
}
if (props.onSegmentSkipped && typeof player.on === 'function') {
player.on('segmentskipped', props.onSegmentSkipped);
}
if (props.onChapterSegmentEntered && typeof player.on === 'function') {
player.on('chapterSegmentEntered', props.onChapterSegmentEntered);
}
if (props.onChapterSegmentSkipped && typeof player.on === 'function') {
player.on('chapterSegmentSkipped', props.onChapterSegmentSkipped);
}
if (props.onChapterSkipButtonShown && typeof player.on === 'function') {
player.on('chapterSkipButtonShown', props.onChapterSkipButtonShown);
}
if (props.onChapterSkipButtonHidden && typeof player.on === 'function') {
player.on('chapterSkipButtonHidden', props.onChapterSkipButtonHidden);
}
if (props.onChaptersLoaded && typeof player.on === 'function') {
player.on('chaptersLoaded', props.onChaptersLoaded);
}
if (props.onChaptersLoadError && typeof player.on === 'function') {
player.on('chaptersLoadError', props.onChaptersLoadError);
}
if (props.onNavigationBackClicked && typeof player.on === 'function') {
player.on('navigationBackClicked', props.onNavigationBackClicked);
}
if (props.onNavigationCloseClicked && typeof player.on === 'function') {
player.on('navigationCloseClicked', props.onNavigationCloseClicked);
}
if (props.onPlay && typeof player.on === 'function') {
player.on('onPlay', props.onPlay);
}
if (props.onPause && typeof player.on === 'function') {
player.on('onPause', props.onPause);
}
if (props.onEnded && typeof player.on === 'function') {
player.on('onEnded', props.onEnded);
}
if (props.onTimeUpdate && typeof player.on === 'function') {
player.on('onTimeUpdate', (currentTime) => {
props.onTimeUpdate?.({
currentTime,
duration: player.getDuration ? player.getDuration() : 0
});
});
}
if (props.onProgress && typeof player.on === 'function') {
player.on('onProgress', (buffered) => {
props.onProgress?.({ buffered });
});
}
if (props.onVolumeChange && typeof player.on === 'function') {
player.on('onVolumeChanged', (volume) => {
const state = player.getState ? player.getState() : {};
props.onVolumeChange?.({ volume, muted: state.isMuted || false });
});
}
if (props.onQualityChange && typeof player.on === 'function') {
player.on('onQualityChanged', props.onQualityChange);
}
if (props.onBuffering && typeof player.on === 'function') {
player.on('onBuffering', props.onBuffering);
}
if (typeof player.on === 'function') {
player.on('onFullscreenChanged', (isFullscreen) => {
console.log(`🔄 Fullscreen changed: ${isFullscreen}`);
if (adContainerRef.current) {
const adContainer = adContainerRef.current;
if (isFullscreen) {
adContainer.style.position = 'fixed';
adContainer.style.top = '0';
adContainer.style.left = '0';
adContainer.style.width = '100vw';
adContainer.style.height = '100vh';
adContainer.style.zIndex = '2147483647';
console.log('✅ Ad container: fullscreen positioning applied');
}
else {
adContainer.style.position = 'absolute';
adContainer.style.top = '0';
adContainer.style.left = '0';
adContainer.style.right = '0';
adContainer.style.bottom = '0';
adContainer.style.width = '';
adContainer.style.height = '';
adContainer.style.zIndex = '999999999';
console.log('✅ Ad container: normal positioning restored');
}
}
if (adsManagerRef.current) {
const google = window.google;
if (google && google.ima) {
const width = isFullscreen
? window.screen.width
: (containerRef.current?.clientWidth || window.innerWidth);
const height = isFullscreen
? window.screen.height
: (containerRef.current?.clientHeight || window.innerHeight);
const viewMode = isFullscreen
? google.ima.ViewMode.FULLSCREEN
: google.ima.ViewMode.NORMAL;
adsManagerRef.current.resize(width, height, viewMode);
console.log(`✅ Ads resized: ${width}x${height}, ViewMode: ${viewMode === google.ima.ViewMode.FULLSCREEN ? 'FULLSCREEN' : 'NORMAL'}`);
}
}
props.onFullscreenChange?.(isFullscreen);
});
}
if (props.onPictureInPictureChange && typeof player.on === 'function') {
player.on('onPictureInPicturechange', props.onPictureInPictureChange);
}
if (props.googleAds) {
setTimeout(async () => {
try {
if (!adContainerRef.current) {
console.error('❌ Ad container ref is null - cannot initialize ads');
return;
}
const adContainer = adContainerRef.current;
const videoElement = player.video || player.getVideoElement?.();
if (!adContainer) {
console.error('❌ Ad container element not found');
return;
}
if (!videoElement) {
console.error('❌ Video element not found');
return;
}
console.log('✅ Initializing Google Ads...', {
adContainer: adContainer.className,
videoElement: videoElement.tagName,
adContainerInDOM: document.body.contains(adContainer)
});
let isPrerollPlaying = false;
let hasPrerollAd = false;
const blockVideoUntilPreroll = (e) => {
if (isPrerollPlaying && hasPrerollAd) {
console.log('⛔ Blocking video playback - waiting for pre-roll ad');
e.preventDefault();
videoElement.pause();
return false;
}
return true;
};
const adsManager = new GoogleAdsManager(videoElement, adContainer, {
adTagUrl: props.googleAds.adTagUrl,
midrollTimes: props.googleAds.midrollTimes,
companionAdSlots: props.googleAds.companionAdSlots,
onAdStart: () => {
setIsAdPlaying(true);
if (videoElement.currentTime < 1) {
console.log('🎬 Pre-roll ad starting - blocking video playback');
isPrerollPlaying = true;
hasPrerollAd = true;
videoElement.addEventListener('play', blockVideoUntilPreroll);
}
if (typeof player.setAdPlaying === 'function') {
player.setAdPlaying(true);
}
props.googleAds?.onAdStart?.();
},
onAdEnd: () => {
setIsAdPlaying(false);
if (isPrerollPlaying) {
console.log('🎬 Pre-roll ad completed');
isPrerollPlaying = false;
videoElement.removeEventListener('play', blockVideoUntilPreroll);
if (videoElement.currentTime > 0 && videoElement.currentTime < 10) {
console.log(`⏮️ Resetting video to 0:00 (was at ${videoElement.currentTime.toFixed(2)}s)`);
videoElement.currentTime = 0;
}
}
if (typeof player.setAdPlaying === 'function') {
player.setAdPlaying(false);
}
props.googleAds?.onAdEnd?.();
},
onAdError: (error) => {
setIsAdPlaying(false);
isPrerollPlaying = false;
videoElement.removeEventListener('play', blockVideoUntilPreroll);
if (typeof player.setAdPlaying === 'function') {
player.setAdPlaying(false);
}
props.googleAds?.onAdError?.(error);
},
onAllAdsComplete: () => {
setIsAdPlaying(false);
isPrerollPlaying = false;
videoElement.removeEventListener('play', blockVideoUntilPreroll);
if (typeof player.setAdPlaying === 'function') {
player.setAdPlaying(false);
}
props.googleAds?.onAllAdsComplete?.();
},
onAdCuePoints: (cuePoints) => {
if (cuePoints.includes(0)) {
console.log('✅ Pre-roll ad detected in cue points');
hasPrerollAd = true;
}
if (!props.googleAds?.midrollTimes || props.googleAds.midrollTimes.length === 0) {
console.log('🔵 Using VMAP cue points for ad markers:', cuePoints);
injectAdMarkersFromTimes(cuePoints);
}
},
});
await adsManager.initialize();
adsManagerRef.current = adsManager;
console.log('✅ Google Ads initialized successfully');
const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
if (playerWrapper && adContainerRef.current && adContainerRef.current.parentElement) {
playerWrapper.appendChild(adContainerRef.current);
console.log('✅ Ad container moved into player wrapper for fullscreen support');
}
let adContainerInitialized = false;
let adsRequested = false;
const initAndRequestAds = () => {
if (!adContainerInitialized && adsManagerRef.current) {
console.log('🎬 Initializing ad container and requesting ads on user gesture');
try {
adsManagerRef.current.initAdDisplayContainer();
adContainerInitialized = true;
console.log('✅ Ad container initialized');
}
catch (err) {
console.warn('Ad container init error:', err);
}
}
if (adContainerInitialized && !adsRequested && adsManagerRef.current) {
console.log('📡 Requesting ads (before video plays to prevent leak)');
adsRequested = true;
adsManagerRef.current.requestAds();
}
};
videoElement.addEventListener('click', initAndRequestAds, { once: true });
adContainerRef.current?.addEventListener('click', initAndRequestAds, { once: true });
if (props.autoPlay) {
setTimeout(() => {
if (!adsRequested) {
console.log('🎬 Autoplay detected - requesting ads immediately');
initAndRequestAds();
}
}, 100);
}
}
catch (adsError) {
console.error('Failed to initialize Google Ads:', adsError);
props.googleAds?.onAdError?.(adsError);
}
}, 100);
}
props.onReady?.(player);
}
}
catch (err) {
if (!cancelled)
props.onError?.(err);
}
}
void boot();
return () => {
cancelled = true;
if (adsManagerRef.current) {
adsManagerRef.current.destroy();
adsManagerRef.current = null;
}
if (playerRef.current) {
playerRef.current.destroy().catch(() => { });
playerRef.current = null;
}
};
}, [
props.autoPlay,
props.muted,
props.volume,
props.controls,
props.loop,
props.preload,
props.crossOrigin,
props.playsInline,
props.defaultQuality,
props.enableAdaptiveBitrate,
props.debug,
props.url,
props.type,
JSON.stringify(props.subtitles),
JSON.stringify(props.metadata),
props.cast,
props.freeDuration,
JSON.stringify(props.responsive),
JSON.stringify(props.paywall),
JSON.stringify(props.emailAuth),
props.paywallConfigUrl,
props.customControls,
JSON.stringify(props.settings),
props.showFrameworkBranding,
JSON.stringify(props.watermark),
JSON.stringify(props.share),
JSON.stringify(props.navigation),
JSON.stringify(props.googleAds),
JSON.stringify(props.qualityFilter),
JSON.stringify(props.premiumQualities),
JSON.stringify(props.fallbackSources),
props.fallbackPoster,
props.fallbackShowErrorMessage,
props.fallbackRetryDelay,
props.fallbackRetryAttempts,
]);
const filterQualities = useCallback((qualities) => {
if (!props.qualityFilter || qualities.length === 0) {
return qualities;
}
const filter = props.qualityFilter;
let filtered = [...qualities];
if (filter.allowedHeights && filter.allowedHeights.length > 0) {
filtered = filtered.filter(q => filter.allowedHeights.includes(q.height));
}
if (filter.allowedLabels && filter.allowedLabels.length > 0) {
filtered = filtered.filter(q => filter.allowedLabels.includes(q.label));
}
if (filter.minHeight !== undefined) {
filtered = filtered.filter(q => q.height >= filter.minHeight);
}
if (filter.maxHeight !== undefined) {
filtered = filtered.filter(q => q.height <= filter.maxHeight);
}
return filtered;
}, [props.qualityFilter]);
const exposeAPIsToParent = useCallback((player) => {
const p = player;
if (props.onChapterAPI) {
const chapterAPI = {
loadChapters: (chapters) => p.loadChapters ? p.loadChapters(chapters) : Promise.resolve(),
loadChaptersFromUrl: (url) => p.loadChaptersFromUrl ? p.loadChaptersFromUrl(url) : Promise.resolve(),
getCurrentSegment: () => p.getCurrentSegment ? p.getCurrentSegment() : null,
skipToSegment: (segmentId) => p.skipToSegment && p.skipToSegment(segmentId),
getSegments: () => p.getSegments ? p.getSegments() : [],
updateChapterConfig: (config) => p.updateChapterConfig && p.updateChapterConfig(config),
hasChapters: () => p.hasChapters ? p.hasChapters() : false,
getChapters: () => p.getChapters ? p.getChapters() : null,
getCoreChapters: () => p.getCoreChapters ? p.getCoreChapters() : [],
getCoreSegments: () => p.getCoreSegments ? p.getCoreSegments() : [],
getCurrentChapterInfo: () => p.getCurrentChapterInfo ? p.getCurrentChapterInfo() : null,
seekToChapter: (chapterId) => p.seekToChapter && p.seekToChapter(chapterId),
getNextChapter: () => p.getNextChapter ? p.getNextChapter() : null,
getPreviousChapter: () => p.getPreviousChapter ? p.getPreviousChapter() : null,
};
props.onChapterAPI(chapterAPI);
}
if (props.onQualityAPI) {
const allQualities = p.getQualities ? p.getQualities() : [];
const filteredQualities = filterQualities(allQualities);
const indexMap = new Map();
filteredQualities.forEach((quality, filteredIndex) => {
const originalIndex = allQualities.findIndex(q => q.id === quality.id);
indexMap.set(filteredIndex, originalIndex);
});
const qualityAPI = {
getQualities: () => filteredQualities,
getCurrentQuality: () => {
const current = p.getCurrentQuality ? p.getCurrentQuality() : null;
if (!current)
return null;
return filteredQualities.find(q => q.id === current.id) || null;
},
setQuality: (filteredIndex) => {
const originalIndex = indexMap.get(filteredIndex);
if (originalIndex !== undefined && p.setQuality) {
p.setQuality(originalIndex);
}
},
setAutoQuality: (enabled) => p.setAutoQuality && p.setAutoQuality(enabled),
};
props.onQualityAPI(qualityAPI);
}
if (props.onEPGAPI) {
const epgAPI = {
setEPGData: (data) => p.setEPGData && p.setEPGData(data),
showEPGButton: () => p.showEPGButton && p.showEPGButton(),
hideEPGButton: () => p.hideEPGButton && p.hideEPGButton(),
isEPGButtonVisible: () => p.isEPGButtonVisible ? p.isEPGButtonVisible() : false,
};
props.onEPGAPI(epgAPI);
}
if (props.onUIHelperAPI) {
const uiAPI = {
focusPlayer: () => p.focusPlayer && p.focusPlayer(),
showFullscreenTip: () => p.showFullscreenTip && p.showFullscreenTip(),
triggerFullscreenButton: () => p.triggerFullscreenButton && p.triggerFullscreenButton(),
showTemporaryMessage: (message) => p.showTemporaryMessage && p.showTemporaryMessage(message),
showFullscreenInstructions: () => p.showFullscreenInstructions && p.showFullscreenInstructions(),
enterFullscreenSynchronously: () => p.enterFullscreenSynchronously && p.enterFullscreenSynchronously(),
};
props.onUIHelperAPI(uiAPI);
}
if (props.onFullscreenAPI) {
const fullscreenAPI = {
enterFullscreen: () => p.enterFullscreen ? p.enterFullscreen() : Promise.resolve(),
exitFullscreen: () => p.exitFullscreen ? p.exitFullscreen() : Promise.resolve(),
toggleFullscreen: () => p.toggleFullscreen ? p.toggleFullscreen() : Promise.resolve(),
enterPictureInPicture: () => p.enterPictureInPicture ? p.enterPictureInPicture() : Promise.resolve(),
exitPictureInPicture: () => p.exitPictureInPicture ? p.exitPictureInPicture() : Promise.resolve(),
};
props.onFullscreenAPI(fullscreenAPI);
}
if (props.onPlaybackAPI) {
const playbackAPI = {
play: () => p.play ? p.play() : Promise.resolve(),
pause: () => p.pause && p.pause(),
requestPause: () => p.requestPause && p.requestPa