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
JavaScript
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)`);