UNPKG

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
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