UNPKG

unified-video-framework

Version:

Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more

999 lines (998 loc) 78.9 kB
import React, { useEffect, useRef, useState, useCallback } from 'react'; import { WebPlayer } from '../WebPlayer.js'; import { GoogleAdsManager } from '../ads/GoogleAdsManager.js'; import PlaylistPanel from './components/playlist/PlaylistPanel.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.js'); EPGOverlay = epgModule.default; } catch (error) { console.warn('Failed to load EPG components:', error); } } return EPGOverlay; }; export const WebPlayerView = (props) => { const containerRef = useRef(null); const fullscreenHostRef = 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 [playerWrapperElement, setPlayerWrapperElement] = useState(null); 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 playlistItems = props.playlist?.items ?? []; const [playlistIndex, setPlaylistIndex] = useState(props.playlist?.startIndex ?? 0); const [playlistLoop, setPlaylistLoop] = useState(props.playlist?.loop ?? false); const [playlistShuffle, setPlaylistShuffle] = useState(props.playlist?.shuffle ?? false); const [shuffleOrder, setShuffleOrder] = useState([]); const [playlistPanelVisible, setPlaylistPanelVisible] = useState(() => props.playlist?.defaultVisible ?? (playlistItems.length > 0)); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { const onFsChange = () => setIsFullscreen(!!document.fullscreenElement); document.addEventListener('fullscreenchange', onFsChange); document.addEventListener('webkitfullscreenchange', onFsChange); return () => { document.removeEventListener('fullscreenchange', onFsChange); document.removeEventListener('webkitfullscreenchange', onFsChange); }; }, []); const playlistIndexRef = useRef(playlistIndex); const playlistLoopRef = useRef(playlistLoop); const playlistShuffleRef = useRef(playlistShuffle); const shuffleOrderRef = useRef(shuffleOrder); useEffect(() => { playlistIndexRef.current = playlistIndex; }, [playlistIndex]); useEffect(() => { playlistLoopRef.current = playlistLoop; }, [playlistLoop]); useEffect(() => { playlistShuffleRef.current = playlistShuffle; }, [playlistShuffle]); useEffect(() => { shuffleOrderRef.current = shuffleOrder; }, [shuffleOrder]); const buildShuffleOrder = (length, currentIdx) => { const indices = Array.from({ length }, (_, i) => i).filter(i => i !== currentIdx); for (let i = indices.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [indices[i], indices[j]] = [indices[j], indices[i]]; } return [currentIdx, ...indices]; }; const getNextIndex = (current, length, loop, shuffle, order) => { if (length <= 1) return null; if (shuffle && order.length === length) { const pos = order.indexOf(current); if (pos < length - 1) return order[pos + 1]; return loop ? order[0] : null; } if (current < length - 1) return current + 1; return loop ? 0 : null; }; const getPrevIndex = (current, length, loop) => { if (length <= 1) return null; if (current > 0) return current - 1; return loop ? length - 1 : null; }; const loadPlaylistItem = useCallback((index) => { const item = playlistItems[index]; if (!playerRef.current || !item) return; const p = playerRef.current; p.load({ url: item.url, type: item.type ?? 'auto', metadata: { title: item.title, posterUrl: item.thumbnail }, subtitles: item.subtitles, }).catch(() => { }); p.play().catch(() => { }); setPlaylistIndex(index); playlistIndexRef.current = index; props.playlist?.onItemChange?.(item, index); }, [props.playlist, playlistItems]); const loadPlaylistItemRef = useRef(loadPlaylistItem); useEffect(() => { loadPlaylistItemRef.current = loadPlaylistItem; }, [loadPlaylistItem]); 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 clampedExistingSegments = existingSegments.map((segment) => { if (segment.endTime > videoDuration) { console.warn(`[AdMarkers] Clamping segment "${segment.title || segment.id}" endTime from ${segment.endTime} to ${videoDuration}`); return { ...segment, endTime: Math.min(segment.endTime, videoDuration) }; } return segment; }); const allSegments = [...clampedExistingSegments, ...adMarkers] .sort((a, b) => a.startTime - b.startTime); console.log('[AdMarkers] Merged chapters:', { existingSegmentCount: clampedExistingSegments.length, adMarkerCount: adMarkers.length, totalSegmentCount: allSegments.length, videoDuration, finalDuration: videoDuration, }); return { ...chapters, enabled: true, showChapterMarkers: true, customStyles, data: { ...chapters.data, duration: videoDuration, 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]); useEffect(() => { if (playerRef.current && props.liveCountdown) { try { playerRef.current.updateLiveCountdown(props.liveCountdown); } catch (error) { console.warn('Failed to update live countdown:', error); } } return () => { if (playerRef.current) { try { const player = playerRef.current; if (player.liveCountdownTimer) { clearInterval(player.liveCountdownTimer); player.liveCountdownTimer = null; } } catch (error) { console.warn('Failed to cleanup countdown timer:', error); } } }; }, [props.liveCountdown]); useEffect(() => { return () => { if (playerRef.current && playerRef.current.isShowingLiveCountdown) { try { playerRef.current.stopLiveCountdown(); } catch (error) { console.warn('Failed to stop live countdown on unmount:', error); } } }; }, []); 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: '100%', height: '100%', maxWidth: '100%', maxHeight: '100%', boxSizing: 'border-box', position: 'absolute', 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: '100%', height: '100%', maxWidth: '100%', maxHeight: '100%', position: 'absolute', top: 0, left: 0, }; } else if (isMobile && isLandscape) { calculatedStyle = { ...calculatedStyle, width: '100%', height: '100%', maxWidth: '100%', maxHeight: '100%', position: 'absolute', top: 0, left: 0, }; } else if (isTablet) { calculatedStyle = { ...calculatedStyle, width: '100%', height: '100%', maxWidth: '100%', maxHeight: '100%', position: 'absolute', top: 0, left: 0, }; } else { calculatedStyle = { ...calculatedStyle, width: '100%', height: '100%', maxWidth: '100%', maxHeight: '100%', position: 'absolute', 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 (_) { } }, []); const hasBootedRef = useRef(false); useEffect(() => { console.log('[WebPlayerView] 🎬 Boot useEffect triggered'); let cancelled = false; async function boot() { console.log('[WebPlayerView] 🚀 boot() called, hasBooted:', hasBootedRef.current); if (!containerRef.current) { console.log('[WebPlayerView] ❌ boot() aborted: no container'); return; } if (hasBootedRef.current) { console.log('[WebPlayerView] ⛔ Boot already executed, skipping duplicate initialization'); return; } hasBootedRef.current = true; console.log('[WebPlayerView] ✅ Proceeding with player initialization'); 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, adaptiveBitrate: props.adaptiveBitrate, debug: props.debug ?? false, startTime: props.startTime, freeDuration: props.freeDuration, isLive: props.isLive, liveWaitingMessages: props.liveWaitingMessages, liveMessageRotationInterval: props.liveMessageRotationInterval, liveRetryInterval: props.liveRetryInterval, liveMaxRetryAttempts: props.liveMaxRetryAttempts, liveBufferingTimeout: props.liveBufferingTimeout, liveCountdown: props.liveCountdown, paywall: paywallCfg, youtubeNativeControls: props.youtubeNativeControls ?? false, controlsVisibility: props.controlsVisibility, 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 }, thumbnailPreview: props.thumbnailPreview }; if (playlistItems.length > 0) { config.playlist = { showPrevNext: false, replaceSkipWithPrevNext: true, onPreviousTrack: () => { const prev = getPrevIndex(playlistIndexRef.current, playlistItems.length, playlistLoopRef.current); if (prev !== null) loadPlaylistItemRef.current?.(prev); }, onNextTrack: () => { const next = getNextIndex(playlistIndexRef.current, playlistItems.length, playlistLoopRef.current, playlistShuffleRef.current, shuffleOrderRef.current); if (next !== null) loadPlaylistItemRef.current?.(next); }, onPlaylistToggle: () => setPlaylistPanelVisible(v => !v), }; } if (playlistItems.length > 0 || props.epg) { config.fullscreenElement = fullscreenHostRef.current ?? undefined; } try { await player.initialize(containerRef.current, config); const wrapper = containerRef.current?.querySelector('.uvf-player-wrapper'); if (wrapper) { setPlayerWrapperElement(wrapper); } 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, }; const shouldDelayVideoForAds = !!props.googleAds?.adTagUrl; if (!shouldDelayVideoForAds) { await player.load(source); } else { console.log('🎬 Ads configured - delaying video load until ads initialize'); player.__pendingSource = 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) { try { const adMarkers = generateAdMarkers(adTimes, duration); const mergedChapters = mergeAdMarkersWithChapters(props.chapters, adMarkers, duration); if (typeof player.loadChapters === 'function' && mergedChapters.data) { const invalidSegments = mergedChapters.data.segments.filter((s) => s.endTime > mergedChapters.data.duration); if (invalidSegments.length > 0) { console.warn('[AdMarkers] Found invalid segments that extend beyond duration:', invalidSegments); } player.loadChapters(mergedChapters.data); console.log('✅ Ad markers injected:', adMarkers.length, 'markers at times:', adTimes); } } catch (error) { console.error('[AdMarkers] Failed to inject ad markers:', error); props.googleAds?.onAdError?.(error); } } else { console.log('⏳ Duration not available yet, waiting for metadata to inject ad markers...'); const videoElement = player.video || player.getVideoElement?.(); if (videoElement) { const retryInject = () => { try { const retryDuration = player.getDuration?.() || 0; if (retryDuration > 0) { const adMarkers = generateAdMarkers(adTimes, retryDuration); const mergedChapters = mergeAdMarkersWithChapters(props.chapters, adMarkers, retryDuration); if (typeof player.loadChapters === 'function' && mergedChapters.data) { player.loadChapters(mergedChapters.data); console.log('✅ Ad markers injected (after metadata):', adMarkers.length, 'markers at times:', adTimes); } } else { console.warn('⚠️ Duration still not available after metadata loaded'); } } catch (error) { console.error('[AdMarkers] Failed to inject ad markers (retry):', error); props.googleAds?.onAdError?.(error); } }; videoElement.addEventListener('loadedmetadata', retryInject, { once: true }); } } }; 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 (playlistItems.length > 1 && typeof player.on === 'function') { player.on('onEnded', () => { if (props.playlist?.autoAdvance === false) return; const next = getNextIndex(playlistIndexRef.current, playlistItems.length, playlistLoopRef.current, playlistShuffleRef.current, shuffleOrderRef.current); if (next !== null) loadPlaylistItemRef.current?.(next); }); } 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}`); setIsFullscreen(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.onLiveStreamWaiting && typeof player.on === 'function') { player.on('onLiveStreamWaiting', props.onLiveStreamWaiting); } if (props.onLiveStreamReady && typeof player.on === 'function') { player.on('onLiveStreamReady', props.onLiveStreamReady); } if (props.onLiveStreamUnavailable && typeof player.on === 'function') { player.on('onLiveStreamUnavailable', props.onLiveStreamUnavailable); } if (props.onBandwidthDetected && typeof player.on === 'function') { player.on('onBandwidthDetected', props.onBandwidthDetected); } if (props.onBandwidthTierChanged && typeof player.on === 'function') { player.on('onBandwidthTierChanged', props.onBandwidthTierChanged); } 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; let waitingForAdResponse = true; const blockVideoUntilPreroll = (e) => { if (waitingForAdResponse || (isPrerollPlaying && hasPrerollAd)) { console.log('⛔ Blocking video playback - waiting for ads'); e.preventDefault(); videoElement.pause(); return false; } return true; }; console.log('🔒 Installing early video blocker - video cannot play until ads load'); videoElement.addEventListener('play', blockVideoUntilPreroll); if (!videoElement.paused) { console.log('⏸️ Pausing video - waiting for ad system to initialize'); videoElement.pause(); } const adsManager = new GoogleAdsManager(videoElement, adContainer, { adTagUrl: props.googleAds.adTagUrl, midrollTimes: props.googleAds.midrollTimes, companionAdSlots: props.googleAds.companionAdSlots, liveAdBreakMode: props.googleAds.liveAdBreakMode, periodicAdInterval: props.googleAds.periodicAdInterval, syncToLiveEdge: props.googleAds.syncToLiveEdge, pauseStreamDuringAd: props.googleAds.pauseStreamDuringAd, liveEdgeOffset: props.googleAds.liveEdgeOffset, onAdStart: () => { setIsAdPlaying(true); waitingForAdResponse = false; if (videoElement.currentTime < 1) { console.log('🎬 Pre-roll ad starting - blocking video playback'); isPrerollPlaying = true; hasPrerollAd = true; } else { videoElement.removeEventListener('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);