UNPKG

unified-video-framework

Version:

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

1,137 lines (1,136 loc) β€’ 515 kB
import { BasePlayer } from '../../core/dist/BasePlayer.js'; import { ChapterManager as CoreChapterManager } from '../../core/dist/index.js'; import { ChapterManager } from './chapters/ChapterManager.js'; import YouTubeExtractor from './utils/YouTubeExtractor.js'; import { SrtConverter } from './utils/SrtConverter.js'; import { DRMManager, DRMErrorHandler } from './drm/index.js'; export class WebPlayer extends BasePlayer { constructor() { super(...arguments); this.video = null; this.hls = null; this.dash = null; this.qualities = []; this.currentQualityIndex = -1; this.autoQuality = true; this.currentStreamType = ''; this.useCustomControls = true; this.controlsContainer = null; this.volumeHideTimeout = null; this.hideControlsTimeout = null; this.isVolumeSliding = false; this.availableQualities = []; this.availableSubtitles = []; this.currentQuality = 'auto'; this.currentSubtitle = 'off'; this.currentPlaybackRate = 1; this.isDragging = false; this.globalMouseMoveHandler = null; this.globalTouchMoveHandler = null; this.globalMouseUpHandler = null; this.globalTouchEndHandler = null; this.settingsConfig = { enabled: true, speed: true, quality: true, subtitles: true }; this.watermarkCanvas = null; this.playerWrapper = null; this.instanceId = ''; this.cachedElements = {}; this.flashTickerContainer = null; this.flashTickerTopElement = null; this.flashTickerBottomElement = null; this.tickerCurrentItemIndex = 0; this.tickerCycleTimer = null; this.tickerConfig = null; this.tickerHeadlineElement = null; this.tickerDetailElement = null; this.tickerIntroOverlay = null; this.tickerProgressBar = null; this.tickerProgressFill = null; this.tickerIsPaused = false; this.tickerPauseStartTime = 0; this.tickerRemainingTime = 0; this.previewGateHit = false; this.paymentSuccessTime = 0; this.paymentSuccessful = false; this.isPaywallActive = false; this.authValidationInterval = null; this.overlayRemovalAttempts = 0; this.maxOverlayRemovalAttempts = 3; this.lastSecurityCheck = 0; this.castContext = null; this.remotePlayer = null; this.remoteController = null; this.isCasting = false; this._castTrackIdByKey = {}; this.selectedSubtitleKey = 'off'; this._kiTo = null; this.paywallController = null; this._playPromise = null; this._deferredPause = false; this._lastToggleAt = 0; this._TOGGLE_DEBOUNCE_MS = 120; this.hasTriedButtonFallback = false; this.lastUserInteraction = 0; this.showTimeTooltip = false; this.tapStartTime = 0; this.tapStartX = 0; this.tapStartY = 0; this.lastTapTime = 0; this.lastTapX = 0; this.tapCount = 0; this.longPressTimer = null; this.isLongPressing = false; this.longPressPlaybackRate = 1; this.tapResetTimer = null; this.fastBackwardInterval = null; this.handleSingleTap = () => { }; this.handleDoubleTap = () => { }; this.handleLongPress = () => { }; this.handleLongPressEnd = () => { }; this.autoplayCapabilities = { canAutoplay: false, canAutoplayMuted: false, canAutoplayUnmuted: false, lastCheck: 0 }; this.autoplayRetryPending = false; this.autoplayRetryAttempts = 0; this.maxAutoplayRetries = 3; this.chapterManager = null; this.drmManager = null; this.isDRMProtected = false; this.hasAppliedStartTime = false; this.coreChapterManager = null; this.chapterConfig = { enabled: false }; this.qualityFilter = null; this.premiumQualities = null; this.youtubeNativeControls = true; this.isAdPlaying = false; this.fallbackSourceIndex = -1; this.fallbackErrors = []; this.isLoadingFallback = false; this.currentRetryAttempt = 0; this.lastFailedUrl = ''; this.isFallbackPosterMode = false; this.hlsErrorRetryCount = 0; this.MAX_HLS_ERROR_RETRIES = 3; this.lastDuration = 0; this.isDetectedAsLive = false; this.isWaitingForLiveStream = false; this.liveRetryTimer = null; this.liveRetryAttempts = 0; this.bandwidthDetector = { estimatedBandwidth: null, detectionComplete: false, detectionMethod: 'fallback' }; this.BANDWIDTH_TIERS = { HIGH: 5000000, MEDIUM: 2500000, LOW: 1000000 }; this.liveStreamOriginalSource = null; this.liveMessageRotationTimer = null; this.liveMessageIndex = 0; this.liveBufferingTimeoutTimer = null; this.liveCountdownTimer = null; this.liveCountdownRemainingSeconds = 0; this.isShowingLiveCountdown = false; this.hasCountdownCompleted = false; this.isReloadingAfterCountdown = false; this.countdownCallbackFired = false; this.thumbnailPreviewConfig = null; this.thumbnailEntries = []; this.preloadedThumbnails = new Map(); this.currentThumbnailUrl = null; this.thumbnailPreviewEnabled = false; this.subtitleBlobUrls = []; this.subtitleOverlay = null; this.autoplayAttempted = false; this.youtubePlayer = null; this.youtubePlayerReady = false; this.youtubeIframe = null; this.youtubeTimeTrackingInterval = null; this.clickToUnmuteHandler = null; } debugLog(message, ...args) { if (this.config.debug) { console.log(`[WebPlayer] ${message}`, ...args); } } debugError(message, ...args) { if (this.config.debug) { console.error(`[WebPlayer] ${message}`, ...args); } } debugWarn(message, ...args) { if (this.config.debug) { console.warn(`[WebPlayer] ${message}`, ...args); } } isLiveStreamNotReady(error) { if (!this.config.isLive || !this.video) return false; if (this.video.error) { this.debugLog(`Live stream error detected (code: ${this.video.error.code}), treating as "not ready"`); return true; } if (this.video.readyState >= 1) { if (this.video.duration === 0 || !isFinite(this.video.duration)) { this.debugLog('Live stream has invalid duration, treating as "not ready"'); return true; } } return false; } startLiveStreamWaiting() { if (this.isWaitingForLiveStream) { this.debugLog(`⏭️ Already in waiting mode, retry count: ${this.liveRetryAttempts}`); this.scheduleLiveStreamRetry(); return; } this.debugLog('πŸ”΄ Entering live stream waiting mode'); this.isWaitingForLiveStream = true; this.liveRetryAttempts = 0; if (this.liveBufferingTimeoutTimer) { clearTimeout(this.liveBufferingTimeoutTimer); this.liveBufferingTimeoutTimer = null; } if (this.source) { this.liveStreamOriginalSource = this.source; } this.showLiveWaitingUI(); this.emit('onLiveStreamWaiting'); this.scheduleLiveStreamRetry(); } stopLiveStreamWaiting(success) { if (!this.isWaitingForLiveStream) return; this.debugLog(`🟒 Exiting live stream waiting mode (success: ${success})`); if (this.liveMessageRotationTimer) { clearInterval(this.liveMessageRotationTimer); this.liveMessageRotationTimer = null; } if (this.liveRetryTimer) { clearTimeout(this.liveRetryTimer); this.liveRetryTimer = null; } const loading = this.cachedElements.loading; if (loading) { loading.classList.remove('with-message'); const messageEl = loading.querySelector('.uvf-loading-message'); if (messageEl) { messageEl.textContent = ''; } } this.isWaitingForLiveStream = false; this.liveRetryAttempts = 0; this.liveStreamOriginalSource = null; this.liveMessageIndex = 0; if (success) { this.emit('onLiveStreamReady'); } } scheduleLiveStreamRetry() { const maxRetries = this.config.liveMaxRetryAttempts; this.debugLog(`πŸ” Checking retries: current=${this.liveRetryAttempts}, max=${maxRetries}`); if (maxRetries !== undefined && this.liveRetryAttempts >= maxRetries) { this.debugLog(`❌ Max retry attempts (${maxRetries}) reached, stopping`); this.stopLiveStreamWaiting(false); this.debugLog('πŸ“’ Emitting onLiveStreamUnavailable event'); this.emit('onLiveStreamUnavailable', { reason: 'Max retry attempts exceeded', attempts: this.liveRetryAttempts }); this.handleError({ code: 'LIVE_STREAM_UNAVAILABLE', message: 'Live stream is not currently broadcasting', type: 'network', fatal: true }); return; } const retryInterval = this.config.liveRetryInterval || 5000; this.liveRetryAttempts++; this.debugLog(`Scheduling live retry #${this.liveRetryAttempts} in ${retryInterval}ms`); this.liveRetryTimer = setTimeout(() => { this.retryLiveStreamLoad(); }, retryInterval); } async retryLiveStreamLoad() { if (!this.isWaitingForLiveStream || !this.liveStreamOriginalSource) return; this.debugLog(`πŸ”„ Retry attempt #${this.liveRetryAttempts} for live stream`); try { await this.load(this.liveStreamOriginalSource); if (this.isWaitingForLiveStream) { const loading = this.cachedElements.loading; if (loading && !loading.classList.contains('active')) { loading.classList.add('active', 'with-message'); } } } catch (error) { this.debugLog('Retry failed, scheduling next attempt', error); this.scheduleLiveStreamRetry(); } } showLiveWaitingUI() { const loading = this.cachedElements.loading; if (!loading) return; loading.classList.add('active', 'with-message'); const messages = this.getLiveWaitingMessages(); this.liveMessageIndex = 0; this.updateLiveWaitingMessage(messages[0]); const rotationInterval = this.config.liveMessageRotationInterval || 2500; if (this.liveMessageRotationTimer) { clearInterval(this.liveMessageRotationTimer); } this.liveMessageRotationTimer = setInterval(() => { this.rotateLoadingMessage(); }, rotationInterval); } updateLiveWaitingMessage(text) { const loading = this.cachedElements.loading; if (!loading) return; const messageEl = loading.querySelector('.uvf-loading-message'); if (messageEl) { messageEl.textContent = text; } } getLiveWaitingMessages() { const messages = this.config.liveWaitingMessages || {}; return [ messages.waitingForStream || 'Waiting for Stream', messages.loading || 'Loading', messages.comingBack || 'Coming back', ]; } rotateLoadingMessage() { if (!this.isWaitingForLiveStream) return; const messages = this.getLiveWaitingMessages(); this.liveMessageIndex = (this.liveMessageIndex + 1) % messages.length; this.updateLiveWaitingMessage(messages[this.liveMessageIndex]); } showLiveCountdown() { const countdown = this.config.liveCountdown; if (!countdown?.nextProgramStartTime) { this.debugLog('⚠️ showLiveCountdown called but no nextProgramStartTime configured'); return; } if (this.hasCountdownCompleted) { this.debugLog('⏱️ showLiveCountdown called but countdown already completed - skipping'); return; } const remainingMs = countdown.nextProgramStartTime - Date.now(); this.liveCountdownRemainingSeconds = Math.max(0, Math.floor(remainingMs / 1000)); this.debugLog(`⏱️ Starting countdown with ${this.liveCountdownRemainingSeconds} seconds remaining (hasCountdownCompleted=${this.hasCountdownCompleted})`); if (this.liveCountdownRemainingSeconds <= 0) { this.handleCountdownComplete(); return; } const loading = this.cachedElements.loading; if (!loading) return; this.isShowingLiveCountdown = true; loading.classList.add('active', 'uvf-countdown-mode'); const centerPlayBtn = this.playerWrapper?.querySelector('.uvf-center-play-btn'); if (centerPlayBtn) { centerPlayBtn.style.display = 'none'; } this.updateLiveCountdownDisplay(); const updateInterval = countdown.updateInterval || 1000; if (this.liveCountdownTimer) { clearInterval(this.liveCountdownTimer); } this.liveCountdownTimer = setInterval(() => { this.tickCountdown(); }, updateInterval); } sanitizeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } updateLiveCountdownDisplay() { const loading = this.cachedElements.loading; if (!loading) return; const messageEl = loading.querySelector('.uvf-loading-message'); if (!messageEl) return; const countdown = this.config.liveCountdown; const noProgramMsg = this.sanitizeHtml(countdown?.noProgramMessage || 'There is no active program in this channel.'); const countdownMsg = this.sanitizeHtml(countdown?.countdownMessage || 'Next program will start in:'); const timerColor = this.sanitizeHtml(countdown?.timerColor || 'var(--uvf-accent-color, #00d4ff)'); const formattedTime = this.sanitizeHtml(this.formatCountdownTime(this.liveCountdownRemainingSeconds)); messageEl.innerHTML = ` <div style="font-size: 18px; margin-bottom: 16px; font-weight: 500;">${noProgramMsg}</div> <div style="font-size: 16px; margin-bottom: 8px;">${countdownMsg}</div> <div style="font-size: 20px; font-weight: bold; color: ${timerColor};">${formattedTime}</div> `; } tickCountdown() { const nextProgramTime = this.config.liveCountdown?.nextProgramStartTime; if (!nextProgramTime) { this.handleCountdownComplete(); return; } const remainingMs = nextProgramTime - Date.now(); this.liveCountdownRemainingSeconds = Math.max(0, Math.floor(remainingMs / 1000)); if (this.liveCountdownRemainingSeconds <= 0) { this.handleCountdownComplete(); } else { this.updateLiveCountdownDisplay(); } } handleCountdownComplete() { this.debugLog('⏱️ Countdown complete!'); this.hasCountdownCompleted = true; this.debugLog(`⏱️ Set hasCountdownCompleted = true`); this.stopLiveCountdown(); if (this.config.liveCountdown?.onCountdownComplete && !this.countdownCallbackFired) { this.countdownCallbackFired = true; this.debugLog('⏱️ Firing onCountdownComplete callback'); this.config.liveCountdown.onCountdownComplete(); } const autoReload = this.config.liveCountdown?.autoReloadOnComplete !== false; if (autoReload && this.source && !this.isReloadingAfterCountdown) { this.isReloadingAfterCountdown = true; this.debugLog('πŸ”„ Auto-reloading stream after countdown'); this.autoplayAttempted = false; this.load(this.source) .catch((error) => { this.debugError('Failed to reload stream after countdown:', error); }) .finally(() => { this.isReloadingAfterCountdown = false; this.debugLog('πŸ”„ Stream reload after countdown complete'); }); } } stopLiveCountdown() { if (!this.isShowingLiveCountdown) return; this.debugLog('⏱️ Stopping countdown'); if (this.liveCountdownTimer) { clearInterval(this.liveCountdownTimer); this.liveCountdownTimer = null; } this.isShowingLiveCountdown = false; this.liveCountdownRemainingSeconds = 0; const loading = this.cachedElements.loading; if (loading) { loading.classList.remove('uvf-countdown-mode', 'active'); const messageEl = loading.querySelector('.uvf-loading-message'); if (messageEl) { messageEl.innerHTML = ''; } } if (this.playerWrapper) { const centerPlayBtn = this.playerWrapper.querySelector('.uvf-center-play-btn'); if (centerPlayBtn) { centerPlayBtn.style.display = ''; } } } updateLiveCountdown(config) { if (!config || !config.nextProgramStartTime) { this.debugLog('⏱️ updateLiveCountdown called with no config/timestamp - stopping countdown'); if (this.isShowingLiveCountdown) { this.stopLiveCountdown(); } this.config.liveCountdown = config; return; } const oldTimestamp = this.config.liveCountdown?.nextProgramStartTime; const newTimestamp = config.nextProgramStartTime; const isDifferentTimestamp = oldTimestamp !== newTimestamp; this.debugLog(`⏱️ updateLiveCountdown called: oldTimestamp=${oldTimestamp}, newTimestamp=${newTimestamp}, isDifferent=${isDifferentTimestamp}, hasCountdownCompleted=${this.hasCountdownCompleted}`); this.config.liveCountdown = config; if (isDifferentTimestamp) { this.debugLog(`⏱️ Different timestamp detected - resetting flags`); this.hasCountdownCompleted = false; this.countdownCallbackFired = false; } else { this.debugLog(`⏱️ Same timestamp - keeping hasCountdownCompleted=${this.hasCountdownCompleted}`); } if (this.isShowingLiveCountdown) { this.stopLiveCountdown(); } if (!this.hasCountdownCompleted && !this.isReloadingAfterCountdown) { const remainingMs = newTimestamp - Date.now(); if (remainingMs > 0) { this.showLiveCountdown(); } } } getElementId(baseName) { return `uvf-${this.instanceId}-${baseName}`; } getElement(baseName) { return document.getElementById(this.getElementId(baseName)); } cacheElementReferences() { this.cachedElements = { loading: this.getElement('loading'), progressBar: this.getElement('progress-bar'), progressFilled: this.getElement('progress-filled'), progressHandle: this.getElement('progress-handle'), progressBuffered: this.getElement('progress-buffered'), playIcon: this.getElement('play-icon'), pauseIcon: this.getElement('pause-icon'), centerPlay: this.getElement('center-play'), timeDisplay: this.getElement('time-display'), thumbnailPreview: this.getElement('thumbnail-preview'), thumbnailImage: this.getElement('thumbnail-image'), thumbnailTime: this.getElement('thumbnail-time'), timeTooltip: this.getElement('time-tooltip'), settingsMenu: this.getElement('settings-menu'), fullscreenBtn: this.getElement('fullscreen-btn'), }; } deepMergeControlsConfig(existing, partial) { return { playback: { ...existing.playback, ...partial.playback }, audio: { ...existing.audio, ...partial.audio }, progress: { ...existing.progress, ...partial.progress }, quality: { ...existing.quality, ...partial.quality }, display: { ...existing.display, ...partial.display }, features: { ...existing.features, ...partial.features }, chrome: { ...existing.chrome, ...partial.chrome }, }; } setElementVisibility(elementBaseName, visible) { if (visible === undefined) return; const element = this.getElement(elementBaseName); if (element) { if (visible) { element.classList.remove('uvf-hidden'); element.style.display = ''; } else { element.classList.add('uvf-hidden'); element.style.setProperty('display', 'none', 'important'); } } } applyControlsVisibility() { if (!this.container) return; const cv = this.controlsVisibility; this.setElementVisibility('center-play', cv.playback?.centerPlayButton); this.setElementVisibility('play-pause', cv.playback?.playPauseButton); this.setElementVisibility('skip-back', cv.playback?.skipButtons); this.setElementVisibility('skip-forward', cv.playback?.skipButtons); this.setElementVisibility('btn-prev', cv.playback?.previousButton); this.setElementVisibility('btn-next', cv.playback?.nextButton); const volumeControl = this.container.querySelector('.uvf-volume-control'); if (volumeControl) { volumeControl.style.display = cv.audio?.volumeButton ? '' : 'none'; } this.setElementVisibility('volume-panel', cv.audio?.volumeSlider); this.setElementVisibility('progress-bar', cv.progress?.progressBar); this.setElementVisibility('time-display', cv.progress?.timeDisplay); this.setElementVisibility('quality-badge', cv.quality?.badge); const settingsContainer = this.container.querySelector('.uvf-settings-container'); if (settingsContainer) { settingsContainer.style.display = cv.quality?.settingsButton ? '' : 'none'; } this.setElementVisibility('fullscreen-btn', cv.display?.fullscreenButton); this.setElementVisibility('pip-btn', cv.display?.pipButton); this.setElementVisibility('epg-btn', cv.features?.epgButton); this.setElementVisibility('playlist-btn', cv.features?.playlistButton); this.setElementVisibility('cast-btn', cv.features?.castButton); this.setElementVisibility('share-btn', cv.features?.shareButton); const topBar = this.container.querySelector('.uvf-top-bar'); if (topBar) { topBar.style.display = cv.chrome?.navigationButtons ? '' : 'none'; } const brandingContainer = this.container.querySelector('.uvf-framework-branding'); if (brandingContainer) { brandingContainer.style.display = cv.chrome?.frameworkBranding ? '' : 'none'; } } setControlsEnabled(enabled) { if (!this.container || !this.playerWrapper) { throw new Error('Player not initialized. Call initialize() first.'); } this.useCustomControls = enabled; if (enabled) { this.playerWrapper.classList.remove('controls-disabled'); this.playerWrapper.classList.add('controls-visible'); } else { this.playerWrapper.classList.add('controls-disabled'); this.playerWrapper.classList.remove('controls-visible'); } this.debugLog(`Controls ${enabled ? 'enabled' : 'disabled'}`); } setControlsVisibility(config) { if (!this.container || !this.playerWrapper) { throw new Error('Player not initialized. Call initialize() first.'); } this.controlsVisibility = this.deepMergeControlsConfig(this.controlsVisibility, config); this.applyControlsVisibility(); this.debugLog('Controls visibility updated:', this.controlsVisibility); } initializeThumbnailPreview(config) { if (!config || !config.generationImage) { this.thumbnailPreviewEnabled = false; return; } this.thumbnailPreviewConfig = config; this.thumbnailPreviewEnabled = config.enabled !== false; this.thumbnailEntries = this.transformThumbnailData(config.generationImage); this.debugLog('Thumbnail preview initialized:', { enabled: this.thumbnailPreviewEnabled, entries: this.thumbnailEntries.length }); if (config.style) { this.applyThumbnailStyles(config.style); } if (config.preloadImages !== false && this.thumbnailEntries.length > 0) { this.preloadThumbnailImages(); } } transformThumbnailData(generationImage) { const entries = []; for (const [url, timeRange] of Object.entries(generationImage)) { const parts = timeRange.split('-'); if (parts.length === 2) { const startTime = parseFloat(parts[0]); const endTime = parseFloat(parts[1]); if (!isNaN(startTime) && !isNaN(endTime)) { entries.push({ url, startTime, endTime }); } } } entries.sort((a, b) => a.startTime - b.startTime); return entries; } findThumbnailForTime(time) { if (this.thumbnailEntries.length === 0) return null; let left = 0; let right = this.thumbnailEntries.length - 1; let result = null; while (left <= right) { const mid = Math.floor((left + right) / 2); const entry = this.thumbnailEntries[mid]; if (time >= entry.startTime && time < entry.endTime) { return entry; } else if (time < entry.startTime) { right = mid - 1; } else { left = mid + 1; } } if (left > 0 && left <= this.thumbnailEntries.length) { const prevEntry = this.thumbnailEntries[left - 1]; if (time >= prevEntry.startTime && time < prevEntry.endTime) { return prevEntry; } } return result; } preloadThumbnailImages() { this.debugLog('Preloading', this.thumbnailEntries.length, 'thumbnail images'); for (const entry of this.thumbnailEntries) { if (this.preloadedThumbnails.has(entry.url)) continue; const img = new Image(); img.onload = () => { this.preloadedThumbnails.set(entry.url, img); this.debugLog('Preloaded thumbnail:', entry.url); }; img.onerror = () => { this.debugWarn('Failed to preload thumbnail:', entry.url); }; img.src = entry.url; } } applyThumbnailStyles(style) { if (!style) return; const wrapper = this.cachedElements.thumbnailPreview; const imageWrapper = wrapper?.querySelector('.uvf-thumbnail-preview-image-wrapper'); if (imageWrapper) { if (style.width) { imageWrapper.style.width = `${style.width}px`; } if (style.height) { imageWrapper.style.height = `${style.height}px`; } if (style.borderRadius !== undefined) { imageWrapper.style.borderRadius = `${style.borderRadius}px`; } } } updateThumbnailPreview(e) { if (!this.thumbnailPreviewEnabled || this.thumbnailEntries.length === 0) { return; } const progressBar = this.cachedElements.progressBar; const thumbnailPreview = this.cachedElements.thumbnailPreview; const thumbnailImage = this.cachedElements.thumbnailImage; const thumbnailTime = this.cachedElements.thumbnailTime; if (!progressBar || !thumbnailPreview || !thumbnailImage || !this.video) { return; } const rect = progressBar.getBoundingClientRect(); const x = e.clientX - rect.left; const percent = Math.max(0, Math.min(1, x / rect.width)); const time = percent * this.video.duration; const entry = this.findThumbnailForTime(time); if (entry) { thumbnailPreview.classList.add('visible'); const thumbnailWidth = 160; const halfWidth = thumbnailWidth / 2; const minX = halfWidth; const maxX = rect.width - halfWidth; const clampedX = Math.max(minX, Math.min(maxX, x)); thumbnailPreview.style.left = `${clampedX}px`; if (this.currentThumbnailUrl !== entry.url) { this.currentThumbnailUrl = entry.url; thumbnailImage.classList.remove('loaded'); const preloaded = this.preloadedThumbnails.get(entry.url); if (preloaded) { thumbnailImage.src = preloaded.src; thumbnailImage.classList.add('loaded'); } else { thumbnailImage.onload = () => { thumbnailImage.classList.add('loaded'); }; thumbnailImage.src = entry.url; } } if (thumbnailTime && this.thumbnailPreviewConfig?.showTimeInThumbnail !== false) { thumbnailTime.textContent = this.formatTime(time); thumbnailTime.style.display = 'block'; } else if (thumbnailTime) { thumbnailTime.style.display = 'none'; } } else { this.hideThumbnailPreview(); } } hideThumbnailPreview() { const thumbnailPreview = this.cachedElements.thumbnailPreview; if (thumbnailPreview) { thumbnailPreview.classList.remove('visible'); } this.currentThumbnailUrl = null; } setThumbnailPreview(config) { if (!config) { this.thumbnailPreviewEnabled = false; this.thumbnailPreviewConfig = null; this.thumbnailEntries = []; this.preloadedThumbnails.clear(); this.hideThumbnailPreview(); return; } this.initializeThumbnailPreview(config); } async initialize(container, config) { this.instanceId = `player-${++WebPlayer.instanceCounter}`; console.log(`[WebPlayer] Instance ${this.instanceId} initializing`); console.log('WebPlayer.initialize called with config:', config); if (config && config.controls !== undefined) { this.useCustomControls = config.controls; console.log('[WebPlayer] Controls set to:', this.useCustomControls); } if (config && config.settings) { console.log('Settings config found:', config.settings); this.settingsConfig = { enabled: config.settings.enabled !== undefined ? config.settings.enabled : true, speed: config.settings.speed !== undefined ? config.settings.speed : true, quality: config.settings.quality !== undefined ? config.settings.quality : true, subtitles: config.settings.subtitles !== undefined ? config.settings.subtitles : true }; console.log('Settings config applied:', this.settingsConfig); } else { console.log('No settings config found, using defaults:', this.settingsConfig); } this.config = config || {}; this.controlsVisibility = this.resolveControlVisibility(); console.log('Controls visibility resolved:', this.controlsVisibility); if (config && config.chapters) { console.log('Chapter config found:', config.chapters); this.chapterConfig = { enabled: config.chapters.enabled || false, data: config.chapters.data, dataUrl: config.chapters.dataUrl, autoHide: config.chapters.autoHide !== undefined ? config.chapters.autoHide : true, autoHideDelay: config.chapters.autoHideDelay || 5000, showChapterMarkers: config.chapters.showChapterMarkers !== undefined ? config.chapters.showChapterMarkers : true, skipButtonPosition: config.chapters.skipButtonPosition || 'bottom-right', customStyles: config.chapters.customStyles || {}, userPreferences: config.chapters.userPreferences || { autoSkipIntro: false, autoSkipRecap: false, autoSkipCredits: false, showSkipButtons: true, skipButtonTimeout: 5000, rememberChoices: true } }; console.log('Chapter config applied:', this.chapterConfig); } else { console.log('No chapter config found, chapters disabled'); } if (config && config.qualityFilter) { console.log('Quality filter config found:', config.qualityFilter); this.qualityFilter = config.qualityFilter; } if (config && config.premiumQualities) { console.log('Premium qualities config found:', config.premiumQualities); this.premiumQualities = config.premiumQualities; } if (config && config.youtubeNativeControls !== undefined) { console.log('YouTube native controls config found:', config.youtubeNativeControls); this.youtubeNativeControls = config.youtubeNativeControls; } else if (this.useCustomControls) { console.log('Custom controls enabled, defaulting YouTube native controls to false'); this.youtubeNativeControls = false; } if (config && config.thumbnailPreview) { console.log('Thumbnail preview config found:', config.thumbnailPreview); this.initializeThumbnailPreview(config.thumbnailPreview); } await super.initialize(container, config); } async setupPlayer() { if (!this.container) { throw new Error('Container element is required'); } this.injectStyles(); const wrapper = document.createElement('div'); wrapper.className = 'uvf-player-wrapper'; this.playerWrapper = wrapper; const videoContainer = document.createElement('div'); videoContainer.className = 'uvf-video-container'; this.video = document.createElement('video'); this.video.className = 'uvf-video'; this.video.controls = false; this.video.autoplay = false; this.video.muted = this.config.muted ?? false; this.state.isMuted = this.video.muted; this.video.loop = this.config.isLive ? false : (this.config.loop ?? false); this.video.playsInline = this.config.playsInline ?? true; this.video.preload = this.config.preload ?? 'metadata'; this.video.webkitAllowsAirPlay = true; this.video.setAttribute('x-webkit-airplay', 'allow'); if (this.config.crossOrigin) { this.video.crossOrigin = this.config.crossOrigin; } this.subtitleOverlay = document.createElement('div'); this.subtitleOverlay.className = 'uvf-subtitle-overlay'; videoContainer.appendChild(this.subtitleOverlay); this.watermarkCanvas = document.createElement('canvas'); this.watermarkCanvas.className = 'uvf-watermark-layer'; this.flashTickerContainer = document.createElement('div'); this.flashTickerContainer.className = 'uvf-flash-ticker-container'; this.flashTickerContainer.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0; pointer-events: none; `; videoContainer.appendChild(this.video); videoContainer.appendChild(this.watermarkCanvas); videoContainer.appendChild(this.flashTickerContainer); wrapper.appendChild(videoContainer); this.createCustomControls(wrapper); if (!this.useCustomControls) { wrapper.classList.add('controls-disabled'); } else { wrapper.classList.add('controls-visible'); } this.container.innerHTML = ''; this.container.appendChild(wrapper); this.applyControlsVisibility(); this.cacheElementReferences(); requestAnimationFrame(() => { if (this.controlsContainer && this.playerWrapper) { this.playerWrapper.style.setProperty('--uvf-ctrl-height', `${this.controlsContainer.getBoundingClientRect().height}px`); } }); this.applyScrollbarPreferencesFromDataset(); this.setupVideoEventListeners(); this.setupControlsEventListeners(); this.setupKeyboardShortcuts(); this.setupWatermark(); this.setupFullscreenListeners(); this.setupUserInteractionTracking(); if (this.chapterConfig.enabled && this.video) { this.setupChapterManager(); } try { const pw = this.config.paywall || null; if (pw && pw.enabled) { const { PaywallController } = await import('./paywall/PaywallController'); this.paywallController = new PaywallController(pw, { getOverlayContainer: () => this.playerWrapper, onResume: (accessInfo) => { try { const isFullAccess = accessInfo?.paymentSuccessful === true || accessInfo?.accessGranted === true; const isFreePreview = !isFullAccess; this.debugLog('onResume callback triggered', { isFullAccess, isFreePreview, accessInfo }); if (isFullAccess) { this.debugLog('Full access granted - lifting all restrictions'); this.previewGateHit = false; this.paymentSuccessTime = Date.now(); this.paymentSuccessful = true; this.isPaywallActive = false; this.overlayRemovalAttempts = 0; if (this.authValidationInterval) { this.debugLog('Clearing security monitoring interval'); clearInterval(this.authValidationInterval); this.authValidationInterval = null; } this.forceCleanupOverlays(); this.debugLog('Payment successful - all security restrictions lifted, resuming playback'); } else { this.debugLog('Free preview mode - maintaining restrictions'); if (accessInfo?.freeDuration && accessInfo.freeDuration > 0) { this.debugLog(`Setting free duration from server: ${accessInfo.freeDuration}s`); this.setFreeDuration(accessInfo.freeDuration); } this.previewGateHit = false; this.isPaywallActive = false; this.overlayRemovalAttempts = 0; if (this.authValidationInterval) { clearInterval(this.authValidationInterval); this.authValidationInterval = null; } this.forceCleanupOverlays(); } setTimeout(() => { this.play(); }, 150); } catch (error) { this.debugError('Error in onResume callback:', error); } }, onShow: () => { this.isPaywallActive = true; this.startOverlayMonitoring(); try { this.requestPause(); } catch (_) { } }, onClose: () => { this.debugLog('onClose callback triggered - paywall closing'); this.isPaywallActive = false; if (this.authValidationInterval) { this.debugLog('Clearing security monitoring interval on close'); clearInterval(this.authValidationInterval); this.authValidationInterval = null; } this.overlayRemovalAttempts = 0; } }); this.on('onFreePreviewEnded', () => { this.debugLog('onFreePreviewEnded event triggered, calling paywallController.openOverlay()'); try { this.paywallController?.openOverlay(); } catch (error) { this.debugError('Error calling paywallController.openOverlay():', error); } }); } } catch (_) { } this.setupCastContextSafe(); this.updateMetadataUI(); } setupVideoEventListeners() { if (!this.video) return; this.video.addEventListener('play', () => { if (!this.paymentSuccessful && this.config.freeDuration && this.config.freeDuration > 0) { const lim = Number(this.config.freeDuration); const cur = (this.video?.currentTime || 0); if (!this.previewGateHit && cur >= lim) { try { this.video?.pause(); } catch (_) { } this.showNotification('Free preview ended. Please rent to continue.'); return; } } this.state.isPlaying = true; this.state.isPaused = false; this.emit('onPlay'); }); this.video.addEventListener('playing', () => { this.debugLog('▢️ playing event fired'); if (this._deferredPause) { this._deferredPause = false; try { this.video?.pause(); } catch (_) { } } this.setBuffering(false); const loading = this.cachedElements.loading; if (loading) { loading.classList.remove('active'); if (this.config.isLive && this.config.liveWaitingMessages && !this.isWaitingForLiveStream) { this.debugLog('βœ… Live stream buffering complete, hiding messages'); loading.classList.remove('with-message'); if (this.liveMessageRotationTimer) { clearInterval(this.liveMessageRotationTimer); this.liveMessageRotationTimer = null; } if (this.liveBufferingTimeoutTimer) { this.debugLog('⏱️ Clearing buffering timeout timer'); clearTimeout(this.liveBufferingTimeoutTimer); this.liveBufferingTimeoutTimer = null; } const messageEl = loading.querySelector('.uvf-loading-message'); if (messageEl) { messageEl.textContent = ''; } this.liveMessageIndex = 0; } } }); this.video.addEventListener('pause', () => { this.state.isPlaying = false; this.state.isPaused = true; this.emit('onPause'); }); this.video.addEventListener('ended', () => { this.state.isEnded = true; this.state.isPlaying = false; if (this.config.isLive) { this.debugLog('πŸ”΄ Live stream ended'); if (this.liveBufferingTimeoutTimer) { clearTimeout(this.liveBufferingTimeoutTimer); this.liveBufferingTimeoutTimer = null; } const loading = this.cachedElements.loading; if (loading) loading.classList.add('active'); } this.emit('onEnded'); }); this.video.addEventListener('timeupdate', () => { if (!this.video) return; const t = this.video.currentTime || 0; this.updateTime(t); this.emit('onTimeUpdate', t); if (Math.floor(t) % 5 === 0 && Math.floor(t) !== Math.floor((this.video?.currentTime || 0) - 0.1)) { console.log(`[DEBUG] onTimeUpdate emitted: ${t.toFixed(2)}s`); } this.enforceFreePreviewGate(t); if (this.coreChapterManager) { this.coreChapterManager.processTimeUpdate(t); } }); this.video.addEventListener('progress', () => { this.updateBufferProgress(); }); this.video.addEventListener('waiting', () => { this.debugLog('⏸️ waiting event fired (buffering)'); if (this.isAdPlaying) return; this.setBuffering(true); const loading = this.cachedElements.loading; if (loading) { loading.classList.add('active'); if (this.config.isLive && this.config.liveWaitingMessages && !this.isWaitingForLiveStream) { this.debugLog('πŸ”„ Live stream buffering, showing custom messages'); loading.classList.add('with-message'); if (!this.liveMessageRotationTimer) { const messages = this.getLiveWaitingMessages(); this.liveMessageIndex = 0; this.updateLiveWaitingMessage(messages[0]); const rotationInterval = this.config.liveMessageRotationInterval || 2500; this.liveMessageRotationTimer = setInterval(() => { this.rotateLoadingMessage(); }, rotationInterval); } if (!this.liveBufferingTimeoutTimer) { const timeout = this.config.liveBufferingTimeout ?? 30000; this.debugLog(`⏱️ Starting buffering timeout timer (${timeout}ms)`); this.liveBufferingTimeoutTimer = setTimeout(() => { this.debugLog('⚠️ Buffering timeout exceeded, entering retry mode'); this.startLiveStreamWaiting(); }, timeout); } } } }); this.video.addEventListener('canplay', () => { this.debugLog('πŸ“‘ canplay event fired'); if (this.isLoadingFallback) { this.debugLog('βœ… Fallback source loaded successfully!'); this.isLoadingFallback = false; this.lastFailedUrl = ''; } if (this.isFallbackPosterMode) { this.debugLog('βœ… Exiting fallback poster mode - video source loaded'); this.isFallbackPosterMode = false; const posterOverlay = this.playerWrapper?.querySelector('#uvf-fallback-poster'); if (posterOverlay) { posterOverlay.remove(); } if (this.video) { this.video.style.display = ''; } } this.setBuffering(false); const loading = this.cachedElements.loading; if (loading) loading.classList.remove('active'); if (this.isWaitingForLiveStream) { this.debugLog('βœ… Live stream is now ready!'); this.stopLiveStreamWaiting(true); } if (this.isShowingLiveCountdown) { this.debugLog('βœ… Stream became available during countdown'); this.stopLiveCountdown(); } if (this.config.startTime !== undefined && this.config.startTime > 0 && !this.hasAppliedStartTime && this.video && this.video.duration > 0) { const startTime = Math.min(this.config.startTime, this.video.duration); this.debugLog(`⏩ Applying startTime at canplay: ${startTime}s (continue watching)`);