unified-video-framework 1.4.370 → 1.4.371

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,10 @@
3
3
  */
4
4
 
5
5
  import { BasePlayer } from '../../core/dist/BasePlayer';
6
- import {
7
- VideoSource,
8
- PlayerConfig,
9
- Quality,
6
+ import {
7
+ VideoSource,
8
+ PlayerConfig,
9
+ Quality,
10
10
  SubtitleTrack,
11
11
  PlayerError,
12
12
  ChapterManager as CoreChapterManager,
@@ -47,13 +47,13 @@ export class WebPlayer extends BasePlayer {
47
47
  private volumeHideTimeout: NodeJS.Timeout | null = null;
48
48
  private hideControlsTimeout: NodeJS.Timeout | null = null;
49
49
  private isVolumeSliding: boolean = false;
50
- private availableQualities: Array<{value: string, label: string}> = [];
51
- private availableSubtitles: Array<{value: string, label: string}> = [];
50
+ private availableQualities: Array<{ value: string, label: string }> = [];
51
+ private availableSubtitles: Array<{ value: string, label: string }> = [];
52
52
  private currentQuality = 'auto';
53
53
  private currentSubtitle = 'off';
54
54
  private currentPlaybackRate = 1;
55
55
  private isDragging: boolean = false;
56
-
56
+
57
57
  // Settings configuration
58
58
  private settingsConfig = {
59
59
  enabled: true, // Show settings button
@@ -73,14 +73,14 @@ export class WebPlayer extends BasePlayer {
73
73
  private previewGateHit: boolean = false;
74
74
  private paymentSuccessTime: number = 0;
75
75
  private paymentSuccessful: boolean = false;
76
-
76
+
77
77
  // Security state to prevent paywall bypass
78
78
  private isPaywallActive: boolean = false;
79
79
  private authValidationInterval: any = null;
80
80
  private overlayRemovalAttempts: number = 0;
81
81
  private maxOverlayRemovalAttempts: number = 3;
82
82
  private lastSecurityCheck: number = 0;
83
-
83
+
84
84
  // Cast state
85
85
  private castContext: any = null;
86
86
  private remotePlayer: any = null;
@@ -98,14 +98,14 @@ export class WebPlayer extends BasePlayer {
98
98
  private _deferredPause = false;
99
99
  private _lastToggleAt = 0;
100
100
  private _TOGGLE_DEBOUNCE_MS = 120;
101
-
101
+
102
102
  // Fullscreen fallback tracking
103
103
  private hasTriedButtonFallback: boolean = false;
104
104
  private lastUserInteraction: number = 0;
105
-
105
+
106
106
  // Progress bar tooltip state
107
107
  private showTimeTooltip: boolean = false;
108
-
108
+
109
109
  // Advanced tap handling state
110
110
  private tapStartTime: number = 0;
111
111
  private tapStartX: number = 0;
@@ -118,10 +118,10 @@ export class WebPlayer extends BasePlayer {
118
118
  private longPressPlaybackRate: number = 1;
119
119
  private tapResetTimer: NodeJS.Timeout | null = null;
120
120
  private fastBackwardInterval: NodeJS.Timeout | null = null;
121
- private handleSingleTap: () => void = () => {};
122
- private handleDoubleTap: (tapX: number) => void = () => {};
123
- private handleLongPress: (tapX: number) => void = () => {};
124
- private handleLongPressEnd: () => void = () => {};
121
+ private handleSingleTap: () => void = () => { };
122
+ private handleDoubleTap: (tapX: number) => void = () => { };
123
+ private handleLongPress: (tapX: number) => void = () => { };
124
+ private handleLongPressEnd: () => void = () => { };
125
125
 
126
126
  // Autoplay enhancement state
127
127
  private autoplayCapabilities: {
@@ -130,11 +130,11 @@ export class WebPlayer extends BasePlayer {
130
130
  canAutoplayUnmuted: boolean;
131
131
  lastCheck: number;
132
132
  } = {
133
- canAutoplay: false,
134
- canAutoplayMuted: false,
135
- canAutoplayUnmuted: false,
136
- lastCheck: 0
137
- };
133
+ canAutoplay: false,
134
+ canAutoplayMuted: false,
135
+ canAutoplayUnmuted: false,
136
+ lastCheck: 0
137
+ };
138
138
  private autoplayRetryPending: boolean = false;
139
139
  private autoplayRetryAttempts: number = 0;
140
140
  private maxAutoplayRetries: number = 3;
@@ -146,16 +146,16 @@ export class WebPlayer extends BasePlayer {
146
146
  private hasAppliedStartTime: boolean = false;
147
147
  private coreChapterManager: CoreChapterManager | null = null;
148
148
  private chapterConfig: ChapterConfig = { enabled: false };
149
-
149
+
150
150
  // Quality filter
151
151
  private qualityFilter: any = null;
152
-
152
+
153
153
  // Premium qualities configuration
154
154
  private premiumQualities: any = null;
155
-
155
+
156
156
  // YouTube native controls configuration
157
157
  private youtubeNativeControls: boolean = true;
158
-
158
+
159
159
  // Ad playing state (set by Google Ads Manager)
160
160
  private isAdPlaying: boolean = false;
161
161
 
@@ -190,7 +190,7 @@ export class WebPlayer extends BasePlayer {
190
190
  async initialize(container: HTMLElement | string, config?: any): Promise<void> {
191
191
  // Debug log the config being passed
192
192
  console.log('WebPlayer.initialize called with config:', config);
193
-
193
+
194
194
  // Set useCustomControls based on controls and customControls config
195
195
  // Priority: customControls > controls
196
196
  if (config) {
@@ -204,7 +204,7 @@ export class WebPlayer extends BasePlayer {
204
204
  console.log('[WebPlayer] Controls set to:', this.useCustomControls);
205
205
  }
206
206
  }
207
-
207
+
208
208
  // Configure settings menu options
209
209
  if (config && config.settings) {
210
210
  console.log('Settings config found:', config.settings);
@@ -218,7 +218,7 @@ export class WebPlayer extends BasePlayer {
218
218
  } else {
219
219
  console.log('No settings config found, using defaults:', this.settingsConfig);
220
220
  }
221
-
221
+
222
222
  // Configure chapters if provided
223
223
  if (config && config.chapters) {
224
224
  console.log('Chapter config found:', config.chapters);
@@ -244,25 +244,25 @@ export class WebPlayer extends BasePlayer {
244
244
  } else {
245
245
  console.log('No chapter config found, chapters disabled');
246
246
  }
247
-
247
+
248
248
  // Configure quality filter if provided
249
249
  if (config && config.qualityFilter) {
250
250
  console.log('Quality filter config found:', config.qualityFilter);
251
251
  this.qualityFilter = config.qualityFilter;
252
252
  }
253
-
253
+
254
254
  // Configure premium qualities if provided
255
255
  if (config && config.premiumQualities) {
256
256
  console.log('Premium qualities config found:', config.premiumQualities);
257
257
  this.premiumQualities = config.premiumQualities;
258
258
  }
259
-
259
+
260
260
  // Configure YouTube native controls if provided
261
261
  if (config && config.youtubeNativeControls !== undefined) {
262
262
  console.log('YouTube native controls config found:', config.youtubeNativeControls);
263
263
  this.youtubeNativeControls = config.youtubeNativeControls;
264
264
  }
265
-
265
+
266
266
  // Call parent initialize
267
267
  await super.initialize(container, config);
268
268
  }
@@ -279,7 +279,7 @@ export class WebPlayer extends BasePlayer {
279
279
  const wrapper = document.createElement('div');
280
280
  wrapper.className = 'uvf-player-wrapper';
281
281
  this.playerWrapper = wrapper;
282
-
282
+
283
283
  // Create video container
284
284
  const videoContainer = document.createElement('div');
285
285
  videoContainer.className = 'uvf-video-container';
@@ -296,11 +296,11 @@ export class WebPlayer extends BasePlayer {
296
296
  this.video.loop = this.config.loop ?? false;
297
297
  this.video.playsInline = this.config.playsInline ?? true;
298
298
  this.video.preload = this.config.preload ?? 'metadata';
299
-
299
+
300
300
  // Enable AirPlay for iOS devices
301
301
  (this.video as any).webkitAllowsAirPlay = true;
302
302
  this.video.setAttribute('x-webkit-airplay', 'allow');
303
-
303
+
304
304
  if (this.config.crossOrigin) {
305
305
  this.video.crossOrigin = this.config.crossOrigin;
306
306
  }
@@ -329,15 +329,15 @@ export class WebPlayer extends BasePlayer {
329
329
 
330
330
  // Always create custom controls
331
331
  this.createCustomControls(videoContainer);
332
-
332
+
333
333
  // Apply controls-disabled class if controls={false}
334
334
  if (!this.useCustomControls) {
335
335
  wrapper.classList.add('controls-disabled');
336
336
  }
337
-
337
+
338
338
  // Assemble the player
339
339
  wrapper.appendChild(videoContainer);
340
-
340
+
341
341
  // Add to container
342
342
  this.container.innerHTML = '';
343
343
  this.container.appendChild(wrapper);
@@ -352,7 +352,7 @@ export class WebPlayer extends BasePlayer {
352
352
  this.setupWatermark();
353
353
  this.setupFullscreenListeners();
354
354
  this.setupUserInteractionTracking();
355
-
355
+
356
356
  // Initialize chapter manager if enabled
357
357
  if (this.chapterConfig.enabled && this.video) {
358
358
  this.setupChapterManager();
@@ -365,58 +365,58 @@ export class WebPlayer extends BasePlayer {
365
365
  const { PaywallController } = await import('./paywall/PaywallController');
366
366
  this.paywallController = new (PaywallController as any)(pw, {
367
367
  getOverlayContainer: () => this.playerWrapper,
368
- onResume: () => {
369
- try {
368
+ onResume: () => {
369
+ try {
370
370
  this.debugLog('onResume callback triggered - payment/auth successful');
371
-
371
+
372
372
  // Reset all security state after successful payment/auth
373
373
  this.previewGateHit = false;
374
374
  this.paymentSuccessTime = Date.now();
375
375
  this.paymentSuccessful = true;
376
376
  this.isPaywallActive = false;
377
377
  this.overlayRemovalAttempts = 0;
378
-
378
+
379
379
  // Clear security monitoring immediately
380
380
  if (this.authValidationInterval) {
381
381
  this.debugLog('Clearing security monitoring interval');
382
382
  clearInterval(this.authValidationInterval);
383
383
  this.authValidationInterval = null;
384
384
  }
385
-
385
+
386
386
  // Force cleanup of any remaining overlays
387
387
  this.forceCleanupOverlays();
388
-
388
+
389
389
  this.debugLog('Payment successful - all security restrictions lifted, resuming playback');
390
-
390
+
391
391
  // Give a small delay to ensure overlay is properly closed before resuming
392
392
  setTimeout(() => {
393
- this.play();
393
+ this.play();
394
394
  }, 150); // Slightly longer delay for complete cleanup
395
- } catch(error) {
395
+ } catch (error) {
396
396
  this.debugError('Error in onResume callback:', error);
397
- }
397
+ }
398
398
  },
399
- onShow: () => {
399
+ onShow: () => {
400
400
  // Activate security monitoring when paywall is shown
401
401
  this.isPaywallActive = true;
402
402
  this.startOverlayMonitoring();
403
-
403
+
404
404
  // Use safe pause method to avoid race conditions
405
- try { this.requestPause(); } catch(_) {}
405
+ try { this.requestPause(); } catch (_) { }
406
406
  },
407
407
  onClose: () => {
408
408
  this.debugLog('onClose callback triggered - paywall closing');
409
-
409
+
410
410
  // Deactivate security monitoring when paywall is closed
411
411
  this.isPaywallActive = false;
412
-
412
+
413
413
  // Clear monitoring interval
414
414
  if (this.authValidationInterval) {
415
415
  this.debugLog('Clearing security monitoring interval on close');
416
416
  clearInterval(this.authValidationInterval);
417
417
  this.authValidationInterval = null;
418
418
  }
419
-
419
+
420
420
  // Reset overlay removal attempts counter
421
421
  this.overlayRemovalAttempts = 0;
422
422
  }
@@ -424,14 +424,14 @@ export class WebPlayer extends BasePlayer {
424
424
  // When free preview ends, open overlay
425
425
  this.on('onFreePreviewEnded' as any, () => {
426
426
  this.debugLog('onFreePreviewEnded event triggered, calling paywallController.openOverlay()');
427
- try {
428
- this.paywallController?.openOverlay();
429
- } catch(error) {
427
+ try {
428
+ this.paywallController?.openOverlay();
429
+ } catch (error) {
430
430
  this.debugError('Error calling paywallController.openOverlay():', error);
431
431
  }
432
432
  });
433
433
  }
434
- } catch (_) {}
434
+ } catch (_) { }
435
435
 
436
436
  // Attempt to bind Cast context if available
437
437
  this.setupCastContextSafe();
@@ -451,7 +451,7 @@ export class WebPlayer extends BasePlayer {
451
451
  const lim = Number(this.config.freeDuration);
452
452
  const cur = (this.video?.currentTime || 0);
453
453
  if (!this.previewGateHit && cur >= lim) {
454
- try { this.video?.pause(); } catch (_) {}
454
+ try { this.video?.pause(); } catch (_) { }
455
455
  this.showNotification('Free preview ended. Please rent to continue.');
456
456
  return;
457
457
  }
@@ -465,7 +465,7 @@ export class WebPlayer extends BasePlayer {
465
465
  // Handle deferred pause requests
466
466
  if (this._deferredPause) {
467
467
  this._deferredPause = false;
468
- try { this.video?.pause(); } catch (_) {}
468
+ try { this.video?.pause(); } catch (_) { }
469
469
  }
470
470
 
471
471
  // Stop buffering state
@@ -540,11 +540,11 @@ export class WebPlayer extends BasePlayer {
540
540
 
541
541
  // Update time display when video is ready to play
542
542
  this.updateTimeDisplay();
543
-
543
+
544
544
  // Handle deferred pause requests
545
545
  if (this._deferredPause) {
546
546
  this._deferredPause = false;
547
- try { this.video?.pause(); } catch (_) {}
547
+ try { this.video?.pause(); } catch (_) { }
548
548
  }
549
549
 
550
550
  // Attempt autoplay once when video is ready to play
@@ -578,9 +578,9 @@ export class WebPlayer extends BasePlayer {
578
578
 
579
579
  // Apply startTime if configured and not yet applied
580
580
  if (this.config.startTime !== undefined &&
581
- this.config.startTime > 0 &&
582
- !this.hasAppliedStartTime &&
583
- this.video.duration > 0) {
581
+ this.config.startTime > 0 &&
582
+ !this.hasAppliedStartTime &&
583
+ this.video.duration > 0) {
584
584
  const startTime = Math.min(this.config.startTime, this.video.duration);
585
585
  this.debugLog(`⏩ Seeking to startTime: ${startTime}s`);
586
586
  this.seek(startTime);
@@ -675,7 +675,7 @@ export class WebPlayer extends BasePlayer {
675
675
 
676
676
  private updateBufferProgress(): void {
677
677
  if (!this.video) return;
678
-
678
+
679
679
  const buffered = this.video.buffered;
680
680
  if (buffered.length > 0) {
681
681
  const bufferedEnd = buffered.end(buffered.length - 1);
@@ -968,17 +968,17 @@ export class WebPlayer extends BasePlayer {
968
968
  }
969
969
 
970
970
  const url = source.url.toLowerCase();
971
-
971
+
972
972
  // Check for YouTube URLs
973
973
  if (YouTubeExtractor.isYouTubeUrl(url)) {
974
974
  return 'youtube';
975
975
  }
976
-
976
+
977
977
  if (url.includes('.m3u8')) return 'hls';
978
978
  if (url.includes('.mpd')) return 'dash';
979
979
  if (url.includes('.mp4')) return 'mp4';
980
980
  if (url.includes('.webm')) return 'webm';
981
-
981
+
982
982
  return 'mp4'; // default
983
983
  }
984
984
 
@@ -1011,13 +1011,13 @@ export class WebPlayer extends BasePlayer {
1011
1011
 
1012
1012
  // Update settings menu with detected qualities
1013
1013
  this.updateSettingsMenu();
1014
-
1014
+
1015
1015
  // Apply quality filter automatically if configured (for auto quality mode)
1016
1016
  if (this.qualityFilter || (this.premiumQualities && this.premiumQualities.enabled)) {
1017
1017
  this.debugLog('Applying quality filter on HLS manifest load');
1018
1018
  this.applyHLSQualityFilter();
1019
1019
  }
1020
-
1020
+
1021
1021
  // Note: Autoplay is now handled in the 'canplay' event when video is ready
1022
1022
  });
1023
1023
 
@@ -1109,10 +1109,10 @@ export class WebPlayer extends BasePlayer {
1109
1109
  label: `${info.height}p`,
1110
1110
  index: index
1111
1111
  }));
1112
-
1112
+
1113
1113
  // Update settings menu with detected qualities
1114
1114
  this.updateSettingsMenu();
1115
-
1115
+
1116
1116
  // Apply quality filter automatically if configured (for auto quality mode)
1117
1117
  if (this.qualityFilter || (this.premiumQualities && this.premiumQualities.enabled)) {
1118
1118
  this.debugLog('Applying quality filter on DASH stream initialization');
@@ -1150,7 +1150,7 @@ export class WebPlayer extends BasePlayer {
1150
1150
  private async loadYouTube(url: string, source: any): Promise<void> {
1151
1151
  try {
1152
1152
  this.debugLog('Loading YouTube video:', url);
1153
-
1153
+
1154
1154
  // Extract video ID and fetch metadata
1155
1155
  const videoId = YouTubeExtractor.extractVideoId(url);
1156
1156
  if (!videoId) {
@@ -1159,7 +1159,7 @@ export class WebPlayer extends BasePlayer {
1159
1159
 
1160
1160
  // Fetch YouTube metadata (title, thumbnail)
1161
1161
  const metadata = await YouTubeExtractor.getVideoMetadata(url);
1162
-
1162
+
1163
1163
  // Store metadata for later use
1164
1164
  this.source = {
1165
1165
  url: source.url || url,
@@ -1182,7 +1182,7 @@ export class WebPlayer extends BasePlayer {
1182
1182
 
1183
1183
  // Create YouTube iframe player with custom controls integration
1184
1184
  await this.createYouTubePlayer(videoId);
1185
-
1185
+
1186
1186
  this.debugLog('✅ YouTube video loaded successfully');
1187
1187
  } catch (error) {
1188
1188
  this.debugError('Failed to load YouTube video:', error);
@@ -1319,8 +1319,8 @@ export class WebPlayer extends BasePlayer {
1319
1319
 
1320
1320
  // Apply startTime if configured and not yet applied
1321
1321
  if (this.config.startTime !== undefined &&
1322
- this.config.startTime > 0 &&
1323
- !this.hasAppliedStartTime) {
1322
+ this.config.startTime > 0 &&
1323
+ !this.hasAppliedStartTime) {
1324
1324
  this.debugLog(`⏩ Seeking YouTube player to startTime: ${this.config.startTime}s`);
1325
1325
  this.youtubePlayer.seekTo(this.config.startTime, true);
1326
1326
  this.hasAppliedStartTime = true;
@@ -1347,7 +1347,7 @@ export class WebPlayer extends BasePlayer {
1347
1347
 
1348
1348
  private onYouTubePlayerStateChange(event: any): void {
1349
1349
  const state = event.data;
1350
-
1350
+
1351
1351
  switch (state) {
1352
1352
  case window.YT.PlayerState.PLAYING:
1353
1353
  this.state.isPlaying = true;
@@ -1356,7 +1356,7 @@ export class WebPlayer extends BasePlayer {
1356
1356
  this.updateYouTubeUI('playing');
1357
1357
  this.emit('onPlay');
1358
1358
  break;
1359
-
1359
+
1360
1360
  case window.YT.PlayerState.PAUSED:
1361
1361
  this.state.isPlaying = false;
1362
1362
  this.state.isPaused = true;
@@ -1364,13 +1364,13 @@ export class WebPlayer extends BasePlayer {
1364
1364
  this.updateYouTubeUI('paused');
1365
1365
  this.emit('onPause');
1366
1366
  break;
1367
-
1367
+
1368
1368
  case window.YT.PlayerState.BUFFERING:
1369
1369
  this.state.isBuffering = true;
1370
1370
  this.updateYouTubeUI('buffering');
1371
1371
  this.emit('onBuffering', true);
1372
1372
  break;
1373
-
1373
+
1374
1374
  case window.YT.PlayerState.ENDED:
1375
1375
  this.state.isPlaying = false;
1376
1376
  this.state.isPaused = true;
@@ -1389,7 +1389,7 @@ export class WebPlayer extends BasePlayer {
1389
1389
  }, 100);
1390
1390
  }
1391
1391
  break;
1392
-
1392
+
1393
1393
  case window.YT.PlayerState.CUED:
1394
1394
  this.state.duration = this.youtubePlayer.getDuration();
1395
1395
  this.updateYouTubeUI('cued');
@@ -1401,7 +1401,7 @@ export class WebPlayer extends BasePlayer {
1401
1401
  const playIcon = document.getElementById('uvf-play-icon');
1402
1402
  const pauseIcon = document.getElementById('uvf-pause-icon');
1403
1403
  const centerPlay = document.getElementById('uvf-center-play');
1404
-
1404
+
1405
1405
  if (state === 'playing' || state === 'buffering') {
1406
1406
  if (playIcon) playIcon.style.display = 'none';
1407
1407
  if (pauseIcon) pauseIcon.style.display = 'block';
@@ -1416,7 +1416,7 @@ export class WebPlayer extends BasePlayer {
1416
1416
  private onYouTubePlayerError(event: any): void {
1417
1417
  const errorCode = event.data;
1418
1418
  let errorMessage = 'YouTube player error';
1419
-
1419
+
1420
1420
  switch (errorCode) {
1421
1421
  case 2:
1422
1422
  errorMessage = 'Invalid video ID';
@@ -1432,7 +1432,7 @@ export class WebPlayer extends BasePlayer {
1432
1432
  errorMessage = 'Video cannot be embedded';
1433
1433
  break;
1434
1434
  }
1435
-
1435
+
1436
1436
  this.handleError({
1437
1437
  code: 'YOUTUBE_ERROR',
1438
1438
  message: errorMessage,
@@ -1441,7 +1441,7 @@ export class WebPlayer extends BasePlayer {
1441
1441
  details: { errorCode }
1442
1442
  });
1443
1443
  }
1444
-
1444
+
1445
1445
  private hideCustomControls(): void {
1446
1446
  // Hide all custom control elements
1447
1447
  const controlsBar = document.getElementById('uvf-controls') as HTMLElement;
@@ -1451,7 +1451,7 @@ export class WebPlayer extends BasePlayer {
1451
1451
  controlsBar.style.opacity = '0';
1452
1452
  controlsBar.style.pointerEvents = 'none';
1453
1453
  }
1454
-
1454
+
1455
1455
  const topBar = document.querySelector('.uvf-top-bar') as HTMLElement;
1456
1456
  if (topBar) {
1457
1457
  topBar.style.display = 'none';
@@ -1459,7 +1459,7 @@ export class WebPlayer extends BasePlayer {
1459
1459
  topBar.style.opacity = '0';
1460
1460
  topBar.style.pointerEvents = 'none';
1461
1461
  }
1462
-
1462
+
1463
1463
  const centerPlayContainer = document.querySelector('.uvf-center-play-container') as HTMLElement;
1464
1464
  if (centerPlayContainer) {
1465
1465
  centerPlayContainer.style.display = 'none';
@@ -1475,21 +1475,21 @@ export class WebPlayer extends BasePlayer {
1475
1475
  if (this.youtubeTimeTrackingInterval) {
1476
1476
  clearInterval(this.youtubeTimeTrackingInterval);
1477
1477
  }
1478
-
1478
+
1479
1479
  this.youtubeTimeTrackingInterval = setInterval(() => {
1480
1480
  if (this.youtubePlayer && this.youtubePlayerReady) {
1481
1481
  try {
1482
1482
  const currentTime = this.youtubePlayer.getCurrentTime();
1483
1483
  const duration = this.youtubePlayer.getDuration();
1484
1484
  const buffered = this.youtubePlayer.getVideoLoadedFraction() * 100;
1485
-
1485
+
1486
1486
  this.state.currentTime = currentTime || 0;
1487
1487
  this.state.duration = duration || 0;
1488
1488
  this.state.bufferedPercentage = buffered || 0;
1489
-
1489
+
1490
1490
  // Update UI progress bar
1491
1491
  this.updateYouTubeProgressBar(currentTime, duration, buffered);
1492
-
1492
+
1493
1493
  this.emit('onTimeUpdate', this.state.currentTime);
1494
1494
  this.emit('onProgress', this.state.bufferedPercentage);
1495
1495
  } catch (error) {
@@ -1501,27 +1501,27 @@ export class WebPlayer extends BasePlayer {
1501
1501
 
1502
1502
  private updateYouTubeProgressBar(currentTime: number, duration: number, buffered: number): void {
1503
1503
  if (!duration || duration === 0) return;
1504
-
1504
+
1505
1505
  const percent = (currentTime / duration) * 100;
1506
-
1506
+
1507
1507
  // Update progress filled
1508
1508
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
1509
1509
  if (progressFilled && !this.isDragging) {
1510
1510
  progressFilled.style.width = percent + '%';
1511
1511
  }
1512
-
1512
+
1513
1513
  // Update progress handle
1514
1514
  const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
1515
1515
  if (progressHandle && !this.isDragging) {
1516
1516
  progressHandle.style.left = percent + '%';
1517
1517
  }
1518
-
1518
+
1519
1519
  // Update buffered progress
1520
1520
  const progressBuffered = document.getElementById('uvf-progress-buffered') as HTMLElement;
1521
1521
  if (progressBuffered) {
1522
1522
  progressBuffered.style.width = buffered + '%';
1523
1523
  }
1524
-
1524
+
1525
1525
  // Update time display
1526
1526
  this.updateTimeDisplay();
1527
1527
  }
@@ -1550,7 +1550,7 @@ export class WebPlayer extends BasePlayer {
1550
1550
  track.kind = subtitle.kind || 'subtitles';
1551
1551
  track.label = subtitle.label;
1552
1552
  track.src = subtitle.url || '';
1553
-
1553
+
1554
1554
  if (subtitle.default || index === 0) {
1555
1555
  track.default = true;
1556
1556
  }
@@ -1665,7 +1665,7 @@ export class WebPlayer extends BasePlayer {
1665
1665
 
1666
1666
  // Fallback: Check if user has interacted with the page
1667
1667
  const hasInteracted = this.lastUserInteraction > 0 &&
1668
- (Date.now() - this.lastUserInteraction) < 5000;
1668
+ (Date.now() - this.lastUserInteraction) < 5000;
1669
1669
 
1670
1670
  this.debugLog(`🎯 Recent user interaction: ${hasInteracted}`);
1671
1671
  return hasInteracted;
@@ -1694,7 +1694,7 @@ export class WebPlayer extends BasePlayer {
1694
1694
  // 1. Browser supports unmuted autoplay OR user has activated the page
1695
1695
  // 2. User hasn't explicitly set muted=true
1696
1696
  const shouldTryUnmuted = (this.autoplayCapabilities.canAutoplayUnmuted || hasActivation)
1697
- && this.config.muted !== true;
1697
+ && this.config.muted !== true;
1698
1698
 
1699
1699
  if (shouldTryUnmuted) {
1700
1700
  this.video.muted = false;
@@ -1788,16 +1788,16 @@ export class WebPlayer extends BasePlayer {
1788
1788
  this.debugLog('🎯 Autoplay retry armed - waiting for user interaction');
1789
1789
  }
1790
1790
 
1791
-
1791
+
1792
1792
  /**
1793
1793
  * Show YouTube-style unmute button when video autoplays muted
1794
1794
  */
1795
1795
  private showUnmuteButton(): void {
1796
1796
  // Remove existing unmute button
1797
1797
  this.hideUnmuteButton();
1798
-
1798
+
1799
1799
  this.debugLog('🔇 Showing unmute button - video autoplaying muted');
1800
-
1800
+
1801
1801
  const unmuteBtn = document.createElement('button');
1802
1802
  unmuteBtn.id = 'uvf-unmute-btn';
1803
1803
  unmuteBtn.className = 'uvf-unmute-btn';
@@ -1808,7 +1808,7 @@ export class WebPlayer extends BasePlayer {
1808
1808
  </svg>
1809
1809
  <span class="uvf-unmute-text">Tap to unmute</span>
1810
1810
  `;
1811
-
1811
+
1812
1812
  // Click handler to unmute
1813
1813
  unmuteBtn.addEventListener('click', (e) => {
1814
1814
  e.stopPropagation();
@@ -1816,7 +1816,7 @@ export class WebPlayer extends BasePlayer {
1816
1816
  this.debugLog('🔊 Video unmuted by user');
1817
1817
  this.hideUnmuteButton();
1818
1818
  });
1819
-
1819
+
1820
1820
  // Add enhanced styles
1821
1821
  const style = document.createElement('style');
1822
1822
  style.textContent = `
@@ -1885,23 +1885,23 @@ export class WebPlayer extends BasePlayer {
1885
1885
  }
1886
1886
  }
1887
1887
  `;
1888
-
1888
+
1889
1889
  // Add to page if not already added
1890
1890
  if (!document.getElementById('uvf-unmute-styles')) {
1891
1891
  style.id = 'uvf-unmute-styles';
1892
1892
  document.head.appendChild(style);
1893
1893
  }
1894
-
1894
+
1895
1895
  // Add to player
1896
1896
  if (this.playerWrapper) {
1897
1897
  this.playerWrapper.appendChild(unmuteBtn);
1898
1898
  this.debugLog('✅ Unmute button added to player');
1899
-
1899
+
1900
1900
  // Enable clicking anywhere on video to unmute
1901
1901
  this.setupClickToUnmute();
1902
1902
  }
1903
1903
  }
1904
-
1904
+
1905
1905
  /**
1906
1906
  * Set up click anywhere on video to unmute when unmute button is visible
1907
1907
  */
@@ -1910,21 +1910,21 @@ export class WebPlayer extends BasePlayer {
1910
1910
  if (this.clickToUnmuteHandler) {
1911
1911
  this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
1912
1912
  }
1913
-
1913
+
1914
1914
  this.clickToUnmuteHandler = (e: MouseEvent) => {
1915
1915
  const unmuteBtn = document.getElementById('uvf-unmute-btn');
1916
-
1916
+
1917
1917
  // Only handle if unmute button is visible (video is muted)
1918
1918
  if (!unmuteBtn || !this.video) return;
1919
-
1919
+
1920
1920
  // Don't unmute if clicking on controls or buttons
1921
1921
  const target = e.target as HTMLElement;
1922
- if (target.closest('.uvf-controls-container') ||
1923
- target.closest('button') ||
1924
- target.closest('.uvf-settings-menu')) {
1922
+ if (target.closest('.uvf-controls-container') ||
1923
+ target.closest('button') ||
1924
+ target.closest('.uvf-settings-menu')) {
1925
1925
  return;
1926
1926
  }
1927
-
1927
+
1928
1928
  // Stop the event from triggering play/pause
1929
1929
  e.stopPropagation();
1930
1930
  e.preventDefault();
@@ -1933,54 +1933,54 @@ export class WebPlayer extends BasePlayer {
1933
1933
  this.unmute();
1934
1934
  this.debugLog('🔊 Video unmuted by clicking on player');
1935
1935
  this.hideUnmuteButton();
1936
-
1936
+
1937
1937
  // Clean up the handler
1938
1938
  if (this.clickToUnmuteHandler) {
1939
1939
  this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
1940
1940
  this.clickToUnmuteHandler = null;
1941
1941
  }
1942
1942
  };
1943
-
1943
+
1944
1944
  // Use capture phase to intercept clicks before they reach the video element
1945
1945
  this.playerWrapper?.addEventListener('click', this.clickToUnmuteHandler, true);
1946
1946
  this.debugLog('👆 Click anywhere to unmute enabled');
1947
1947
  }
1948
-
1948
+
1949
1949
  /**
1950
1950
  * Hide unmute button
1951
1951
  */
1952
1952
  private clickToUnmuteHandler: ((e: MouseEvent) => void) | null = null;
1953
-
1953
+
1954
1954
  private hideUnmuteButton(): void {
1955
1955
  const unmuteBtn = document.getElementById('uvf-unmute-btn');
1956
1956
  if (unmuteBtn) {
1957
1957
  unmuteBtn.remove();
1958
1958
  this.debugLog('Unmute button removed');
1959
1959
  }
1960
-
1960
+
1961
1961
  // Remove click to unmute handler when button is hidden
1962
1962
  if (this.clickToUnmuteHandler) {
1963
1963
  this.playerWrapper?.removeEventListener('click', this.clickToUnmuteHandler, true);
1964
1964
  this.clickToUnmuteHandler = null;
1965
1965
  }
1966
1966
  }
1967
-
1967
+
1968
1968
  private updateTimeTooltip(e: MouseEvent): void {
1969
1969
  const progressBar = document.getElementById('uvf-progress-bar');
1970
1970
  const tooltip = document.getElementById('uvf-time-tooltip');
1971
1971
  if (!progressBar || !tooltip || !this.video) return;
1972
-
1972
+
1973
1973
  const rect = progressBar.getBoundingClientRect();
1974
1974
  const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
1975
1975
  const percent = (x / rect.width);
1976
1976
  const time = percent * this.video.duration;
1977
-
1977
+
1978
1978
  // Update tooltip content and position
1979
1979
  tooltip.textContent = this.formatTime(time);
1980
1980
  tooltip.style.left = `${x}px`;
1981
1981
  tooltip.classList.add('visible');
1982
1982
  }
1983
-
1983
+
1984
1984
  private hideTimeTooltip(): void {
1985
1985
  const tooltip = document.getElementById('uvf-time-tooltip');
1986
1986
  if (tooltip) {
@@ -1991,17 +1991,17 @@ export class WebPlayer extends BasePlayer {
1991
1991
  private setupUserInteractionTracking(): void {
1992
1992
  // Track various user interactions to enable autoplay
1993
1993
  const interactionEvents = ['click', 'mousedown', 'keydown', 'touchstart'];
1994
-
1994
+
1995
1995
  const updateLastInteraction = () => {
1996
1996
  this.lastUserInteraction = Date.now();
1997
1997
  this.debugLog('User interaction detected at:', this.lastUserInteraction);
1998
1998
  };
1999
-
1999
+
2000
2000
  // Listen on document for global interactions
2001
2001
  interactionEvents.forEach(eventType => {
2002
2002
  document.addEventListener(eventType, updateLastInteraction, { passive: true });
2003
2003
  });
2004
-
2004
+
2005
2005
  // Also listen on player wrapper for more specific interactions
2006
2006
  if (this.playerWrapper) {
2007
2007
  interactionEvents.forEach(eventType => {
@@ -2062,14 +2062,14 @@ export class WebPlayer extends BasePlayer {
2062
2062
  // Benign: pause() raced play(); ignore the error.
2063
2063
  return;
2064
2064
  }
2065
-
2065
+
2066
2066
  // Check if this is an autoplay restriction error
2067
2067
  if (this.isAutoplayRestrictionError(err)) {
2068
2068
  this.debugWarn('Autoplay blocked by browser policy');
2069
2069
  // Throw error so intelligent autoplay can handle fallback
2070
2070
  throw err;
2071
2071
  }
2072
-
2072
+
2073
2073
  this.handleError({
2074
2074
  code: 'PLAY_ERROR',
2075
2075
  message: `Failed to start playbook: ${err}`,
@@ -2108,7 +2108,7 @@ export class WebPlayer extends BasePlayer {
2108
2108
  public requestPause(): void {
2109
2109
  this._deferredPause = true;
2110
2110
  if (!this._playPromise && this.video && this.video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
2111
- try { this.video.pause(); } catch (_) {}
2111
+ try { this.video.pause(); } catch (_) { }
2112
2112
  }
2113
2113
  }
2114
2114
 
@@ -2120,16 +2120,16 @@ export class WebPlayer extends BasePlayer {
2120
2120
  this.debugWarn('Cannot set currentTime: video element not available');
2121
2121
  return false;
2122
2122
  }
2123
-
2123
+
2124
2124
  // Validate the time value is finite
2125
2125
  if (!isFinite(time) || isNaN(time)) {
2126
2126
  this.debugWarn('Attempted to set invalid currentTime value:', time);
2127
2127
  return false;
2128
2128
  }
2129
-
2129
+
2130
2130
  // Ensure time is non-negative
2131
2131
  const safeTime = Math.max(0, time);
2132
-
2132
+
2133
2133
  // Additional validation: check if video duration is available and valid
2134
2134
  const duration = this.video.duration;
2135
2135
  if (isFinite(duration) && duration > 0) {
@@ -2164,13 +2164,13 @@ export class WebPlayer extends BasePlayer {
2164
2164
  }
2165
2165
 
2166
2166
  if (!this.video) return;
2167
-
2167
+
2168
2168
  // Validate input time
2169
2169
  if (!isFinite(time) || isNaN(time)) {
2170
2170
  this.debugWarn('Invalid seek time:', time);
2171
2171
  return;
2172
2172
  }
2173
-
2173
+
2174
2174
  // Security check: Prevent seeking beyond free preview limit
2175
2175
  const freeDuration = Number(this.config.freeDuration || 0);
2176
2176
  if (freeDuration > 0 && !this.paymentSuccessful) {
@@ -2183,7 +2183,7 @@ export class WebPlayer extends BasePlayer {
2183
2183
  return;
2184
2184
  }
2185
2185
  }
2186
-
2186
+
2187
2187
  // Use safe setter with validated time
2188
2188
  this.safeSetCurrentTime(time);
2189
2189
  }
@@ -2259,7 +2259,7 @@ export class WebPlayer extends BasePlayer {
2259
2259
  } else if (this.dash) {
2260
2260
  this.dash.setQualityFor('video', index);
2261
2261
  }
2262
-
2262
+
2263
2263
  this.currentQualityIndex = index;
2264
2264
  this.autoQuality = false;
2265
2265
  }
@@ -2454,7 +2454,7 @@ export class WebPlayer extends BasePlayer {
2454
2454
 
2455
2455
  setAutoQuality(enabled: boolean): void {
2456
2456
  this.autoQuality = enabled;
2457
-
2457
+
2458
2458
  if (this.hls) {
2459
2459
  if (enabled) {
2460
2460
  // Apply quality filter constraints when enabling auto
@@ -2476,7 +2476,7 @@ export class WebPlayer extends BasePlayer {
2476
2476
  }
2477
2477
  }
2478
2478
  });
2479
-
2479
+
2480
2480
  // Apply quality filter constraints when enabling auto
2481
2481
  if (enabled && (this.qualityFilter || (this.premiumQualities && this.premiumQualities.enabled))) {
2482
2482
  this.applyDASHQualityFilter();
@@ -2491,7 +2491,7 @@ export class WebPlayer extends BasePlayer {
2491
2491
  // iOS Safari special handling - use video element fullscreen
2492
2492
  if (this.isIOSDevice() && this.video) {
2493
2493
  this.debugLog('iOS device detected - using video element fullscreen');
2494
-
2494
+
2495
2495
  try {
2496
2496
  // iOS Safari supports video fullscreen but not element fullscreen
2497
2497
  if ((this.video as any).webkitEnterFullscreen) {
@@ -2514,7 +2514,7 @@ export class WebPlayer extends BasePlayer {
2514
2514
  // Fall through to try standard fullscreen
2515
2515
  }
2516
2516
  }
2517
-
2517
+
2518
2518
  // Check if fullscreen is supported for non-iOS devices
2519
2519
  if (!this.isFullscreenSupported()) {
2520
2520
  this.debugWarn('Fullscreen not supported by browser');
@@ -2524,19 +2524,19 @@ export class WebPlayer extends BasePlayer {
2524
2524
  }
2525
2525
  return;
2526
2526
  }
2527
-
2527
+
2528
2528
  // Check if already in fullscreen
2529
2529
  if (this.isFullscreen()) {
2530
2530
  this.debugLog('Already in fullscreen mode');
2531
2531
  return;
2532
2532
  }
2533
-
2533
+
2534
2534
  // Target the player wrapper to maintain custom controls
2535
2535
  const element = this.playerWrapper;
2536
-
2536
+
2537
2537
  // Try different fullscreen APIs with better error handling
2538
2538
  let fullscreenSuccess = false;
2539
-
2539
+
2540
2540
  if (element.requestFullscreen) {
2541
2541
  try {
2542
2542
  await element.requestFullscreen();
@@ -2566,17 +2566,17 @@ export class WebPlayer extends BasePlayer {
2566
2566
  this.debugWarn('MS fullscreen request failed:', (err as Error).message);
2567
2567
  }
2568
2568
  }
2569
-
2569
+
2570
2570
  if (fullscreenSuccess) {
2571
2571
  // Add fullscreen class for styling
2572
2572
  this.playerWrapper.classList.add('uvf-fullscreen');
2573
2573
  this.emit('onFullscreenChanged', true);
2574
-
2574
+
2575
2575
  // Lock to landscape orientation on mobile devices
2576
2576
  await this.lockOrientationLandscape();
2577
2577
  } else {
2578
2578
  this.debugWarn('All fullscreen methods failed');
2579
-
2579
+
2580
2580
  // Provide helpful feedback based on device
2581
2581
  if (this.isIOSDevice()) {
2582
2582
  this.showShortcutIndicator('Fullscreen not available - use device controls');
@@ -2586,7 +2586,7 @@ export class WebPlayer extends BasePlayer {
2586
2586
  this.showShortcutIndicator('Fullscreen not supported in this browser');
2587
2587
  }
2588
2588
  }
2589
-
2589
+
2590
2590
  } catch (error) {
2591
2591
  this.debugWarn('Failed to enter fullscreen:', (error as Error).message);
2592
2592
  // Don't re-throw the error to prevent breaking the user experience
@@ -2613,16 +2613,16 @@ export class WebPlayer extends BasePlayer {
2613
2613
  // Fall through to try standard methods
2614
2614
  }
2615
2615
  }
2616
-
2616
+
2617
2617
  // Check if we're actually in fullscreen
2618
2618
  if (!this.isFullscreen()) {
2619
2619
  this.debugLog('Not in fullscreen mode');
2620
2620
  return;
2621
2621
  }
2622
-
2622
+
2623
2623
  // Try different exit fullscreen methods
2624
2624
  let exitSuccess = false;
2625
-
2625
+
2626
2626
  if (document.exitFullscreen) {
2627
2627
  try {
2628
2628
  await document.exitFullscreen();
@@ -2652,7 +2652,7 @@ export class WebPlayer extends BasePlayer {
2652
2652
  this.debugWarn('MS exit fullscreen failed:', (err as Error).message);
2653
2653
  }
2654
2654
  }
2655
-
2655
+
2656
2656
  if (exitSuccess || !this.isFullscreen()) {
2657
2657
  // Remove fullscreen class
2658
2658
  if (this.playerWrapper) {
@@ -2668,7 +2668,7 @@ export class WebPlayer extends BasePlayer {
2668
2668
  this.playerWrapper.classList.remove('uvf-fullscreen');
2669
2669
  }
2670
2670
  }
2671
-
2671
+
2672
2672
  } catch (error) {
2673
2673
  this.debugWarn('Failed to exit fullscreen:', (error as Error).message);
2674
2674
  // Don't re-throw the error to prevent breaking the user experience
@@ -2716,7 +2716,7 @@ export class WebPlayer extends BasePlayer {
2716
2716
  */
2717
2717
  showFullscreenTip(): void {
2718
2718
  this.showShortcutIndicator('💡 Double-click or use ⌨️ F key for fullscreen');
2719
-
2719
+
2720
2720
  // Also show in debug log
2721
2721
  this.debugLog('Tip: Double-click the video area or press F key for fullscreen, or use the fullscreen button in controls');
2722
2722
  }
@@ -2735,7 +2735,7 @@ export class WebPlayer extends BasePlayer {
2735
2735
  // Check for Brave's specific properties
2736
2736
  (window as any).chrome && (window as any).chrome.app && (window as any).chrome.app.isInstalled === false
2737
2737
  );
2738
-
2738
+
2739
2739
  this.debugLog('Browser detection - Is Brave:', isBrave, 'User Agent:', userAgent);
2740
2740
  return isBrave;
2741
2741
  }
@@ -2747,10 +2747,10 @@ export class WebPlayer extends BasePlayer {
2747
2747
  try {
2748
2748
  // Check if fullscreen is enabled
2749
2749
  const fullscreenEnabled = document.fullscreenEnabled ||
2750
- (document as any).webkitFullscreenEnabled ||
2751
- (document as any).mozFullScreenEnabled ||
2752
- (document as any).msFullscreenEnabled;
2753
-
2750
+ (document as any).webkitFullscreenEnabled ||
2751
+ (document as any).mozFullScreenEnabled ||
2752
+ (document as any).msFullscreenEnabled;
2753
+
2754
2754
  this.debugLog('Fullscreen permissions check:', {
2755
2755
  fullscreenEnabled,
2756
2756
  documentFullscreenEnabled: document.fullscreenEnabled,
@@ -2764,7 +2764,7 @@ export class WebPlayer extends BasePlayer {
2764
2764
  isBrave: this.isBraveBrowser(),
2765
2765
  isPrivate: this.isPrivateWindow()
2766
2766
  });
2767
-
2767
+
2768
2768
  // Check permissions API if available
2769
2769
  if ('permissions' in navigator) {
2770
2770
  try {
@@ -2774,7 +2774,7 @@ export class WebPlayer extends BasePlayer {
2774
2774
  this.debugLog('Permissions API check failed:', (err as Error).message);
2775
2775
  }
2776
2776
  }
2777
-
2777
+
2778
2778
  } catch (error) {
2779
2779
  this.debugWarn('Permission check failed:', (error as Error).message);
2780
2780
  }
@@ -2797,13 +2797,13 @@ export class WebPlayer extends BasePlayer {
2797
2797
  );
2798
2798
  }) as any;
2799
2799
  }
2800
-
2800
+
2801
2801
  // Firefox detection
2802
2802
  if ('MozAppearance' in document.documentElement.style) {
2803
2803
  if (window.indexedDB === null) return true;
2804
2804
  if (window.indexedDB === undefined) return true;
2805
2805
  }
2806
-
2806
+
2807
2807
  // Safari detection
2808
2808
  try {
2809
2809
  window.localStorage.setItem('test', '1');
@@ -2812,11 +2812,11 @@ export class WebPlayer extends BasePlayer {
2812
2812
  } catch {
2813
2813
  return true;
2814
2814
  }
2815
-
2815
+
2816
2816
  } catch {
2817
2817
  return false;
2818
2818
  }
2819
-
2819
+
2820
2820
  return false;
2821
2821
  }
2822
2822
 
@@ -2825,11 +2825,11 @@ export class WebPlayer extends BasePlayer {
2825
2825
  */
2826
2826
  triggerFullscreenButton(): void {
2827
2827
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
2828
-
2828
+
2829
2829
  // Enhanced debugging for Brave browser
2830
2830
  const isBrave = this.isBraveBrowser();
2831
2831
  const isPrivate = this.isPrivateWindow();
2832
-
2832
+
2833
2833
  this.debugLog('Fullscreen trigger attempt:', {
2834
2834
  buttonExists: !!fullscreenBtn,
2835
2835
  isBrave,
@@ -2839,24 +2839,24 @@ export class WebPlayer extends BasePlayer {
2839
2839
  lastUserInteraction: this.lastUserInteraction,
2840
2840
  timeSinceInteraction: Date.now() - this.lastUserInteraction
2841
2841
  });
2842
-
2842
+
2843
2843
  // Run permissions check
2844
2844
  this.checkFullscreenPermissions();
2845
-
2845
+
2846
2846
  if (fullscreenBtn) {
2847
2847
  this.debugLog('Triggering fullscreen button click');
2848
-
2848
+
2849
2849
  // Special handling for Brave browser
2850
2850
  if (isBrave) {
2851
2851
  this.debugLog('Applying Brave browser specific fullscreen handling');
2852
-
2852
+
2853
2853
  // For Brave, we need to ensure the gesture is absolutely fresh
2854
2854
  if (Date.now() - this.lastUserInteraction > 1000) {
2855
2855
  this.debugWarn('User gesture may be stale for Brave browser');
2856
2856
  this.showTemporaryMessage('Click the fullscreen button directly in Brave browser');
2857
2857
  return;
2858
2858
  }
2859
-
2859
+
2860
2860
  // Request permissions first in Brave if needed
2861
2861
  this.requestFullscreenPermissionBrave().then(() => {
2862
2862
  this.performFullscreenButtonClick(fullscreenBtn);
@@ -2866,11 +2866,11 @@ export class WebPlayer extends BasePlayer {
2866
2866
  } else {
2867
2867
  this.performFullscreenButtonClick(fullscreenBtn);
2868
2868
  }
2869
-
2869
+
2870
2870
  } else {
2871
2871
  this.debugWarn('Fullscreen button not found');
2872
2872
  this.showShortcutIndicator('Fullscreen Button Missing');
2873
-
2873
+
2874
2874
  // Enhanced guidance for Brave
2875
2875
  if (isBrave) {
2876
2876
  this.showTemporaryMessage('Brave: Please use fullscreen button in controls');
@@ -2914,10 +2914,10 @@ export class WebPlayer extends BasePlayer {
2914
2914
  isTrusted: true
2915
2915
  } as any)
2916
2916
  ];
2917
-
2917
+
2918
2918
  // Log each event dispatch
2919
2919
  this.debugLog('Dispatching mouse events:', events.length);
2920
-
2920
+
2921
2921
  // Dispatch all events in sequence
2922
2922
  events.forEach((event, index) => {
2923
2923
  try {
@@ -2927,7 +2927,7 @@ export class WebPlayer extends BasePlayer {
2927
2927
  this.debugWarn(`Event ${index + 1} dispatch failed:`, (error as Error).message);
2928
2928
  }
2929
2929
  });
2930
-
2930
+
2931
2931
  // Also try direct click method with enhanced error handling
2932
2932
  try {
2933
2933
  fullscreenBtn.click();
@@ -2935,7 +2935,7 @@ export class WebPlayer extends BasePlayer {
2935
2935
  } catch (error) {
2936
2936
  this.debugWarn('Direct button click failed:', (error as Error).message);
2937
2937
  }
2938
-
2938
+
2939
2939
  // Focus the button to ensure gesture context
2940
2940
  try {
2941
2941
  fullscreenBtn.focus();
@@ -2943,7 +2943,7 @@ export class WebPlayer extends BasePlayer {
2943
2943
  } catch (error) {
2944
2944
  this.debugLog('Button focus failed:', (error as Error).message);
2945
2945
  }
2946
-
2946
+
2947
2947
  // Show that we're attempting fullscreen via button
2948
2948
  this.showShortcutIndicator('Fullscreen');
2949
2949
  }
@@ -2973,35 +2973,35 @@ export class WebPlayer extends BasePlayer {
2973
2973
  }
2974
2974
 
2975
2975
  this.debugLog('Attempting Brave-specific fullscreen entry');
2976
-
2976
+
2977
2977
  // First, check if we can request permissions
2978
2978
  await this.requestFullscreenPermissionBrave();
2979
-
2979
+
2980
2980
  // Check current fullscreen state
2981
- if (document.fullscreenElement ||
2982
- (document as any).webkitFullscreenElement ||
2983
- (document as any).mozFullScreenElement ||
2984
- (document as any).msFullscreenElement) {
2981
+ if (document.fullscreenElement ||
2982
+ (document as any).webkitFullscreenElement ||
2983
+ (document as any).mozFullScreenElement ||
2984
+ (document as any).msFullscreenElement) {
2985
2985
  this.debugLog('Already in fullscreen mode');
2986
2986
  return;
2987
2987
  }
2988
-
2988
+
2989
2989
  // Enhanced permission and capability checking for Brave
2990
2990
  const fullscreenEnabled = document.fullscreenEnabled ||
2991
- (document as any).webkitFullscreenEnabled ||
2992
- (document as any).mozFullScreenEnabled ||
2993
- (document as any).msFullscreenEnabled;
2994
-
2991
+ (document as any).webkitFullscreenEnabled ||
2992
+ (document as any).mozFullScreenEnabled ||
2993
+ (document as any).msFullscreenEnabled;
2994
+
2995
2995
  if (!fullscreenEnabled) {
2996
2996
  throw new Error('Fullscreen not supported or disabled in Brave settings');
2997
2997
  }
2998
-
2998
+
2999
2999
  // Check if the site has fullscreen blocked
3000
3000
  if ('permissions' in navigator) {
3001
3001
  try {
3002
3002
  const permission = await (navigator as any).permissions.query({ name: 'fullscreen' });
3003
3003
  this.debugLog('Brave fullscreen permission state:', permission.state);
3004
-
3004
+
3005
3005
  if (permission.state === 'denied') {
3006
3006
  throw new Error('Fullscreen permission denied in Brave site settings');
3007
3007
  }
@@ -3009,10 +3009,10 @@ export class WebPlayer extends BasePlayer {
3009
3009
  this.debugLog('Permission check failed:', (permError as Error).message);
3010
3010
  }
3011
3011
  }
3012
-
3012
+
3013
3013
  // Try multiple fullscreen methods with proper error handling
3014
3014
  let fullscreenError: Error | null = null;
3015
-
3015
+
3016
3016
  try {
3017
3017
  // Try standard fullscreen API first
3018
3018
  if (this.playerWrapper.requestFullscreen) {
@@ -3039,22 +3039,22 @@ export class WebPlayer extends BasePlayer {
3039
3039
  else {
3040
3040
  throw new Error('No fullscreen API available');
3041
3041
  }
3042
-
3042
+
3043
3043
  // If we get here, fullscreen was successful
3044
3044
  this.playerWrapper.classList.add('uvf-fullscreen');
3045
3045
  this.emit('onFullscreenChanged', true);
3046
3046
  this.debugLog('Brave fullscreen entry successful');
3047
-
3047
+
3048
3048
  } catch (error) {
3049
3049
  fullscreenError = error as Error;
3050
3050
  this.debugWarn('Brave fullscreen attempt failed:', fullscreenError.message);
3051
-
3051
+
3052
3052
  // Provide specific guidance for common Brave issues
3053
- if (fullscreenError.message.includes('denied') ||
3054
- fullscreenError.message.includes('not allowed')) {
3053
+ if (fullscreenError.message.includes('denied') ||
3054
+ fullscreenError.message.includes('not allowed')) {
3055
3055
  throw new Error('Brave Browser: Fullscreen blocked. Check site permissions in Settings > Site and Shields Settings');
3056
- } else if (fullscreenError.message.includes('gesture') ||
3057
- fullscreenError.message.includes('user activation')) {
3056
+ } else if (fullscreenError.message.includes('gesture') ||
3057
+ fullscreenError.message.includes('user activation')) {
3058
3058
  throw new Error('Brave Browser: User interaction required. Click the fullscreen button directly');
3059
3059
  } else {
3060
3060
  throw new Error(`Brave Browser: ${fullscreenError.message}`);
@@ -3070,7 +3070,7 @@ export class WebPlayer extends BasePlayer {
3070
3070
  if (existingMsg) {
3071
3071
  existingMsg.remove();
3072
3072
  }
3073
-
3073
+
3074
3074
  const msgDiv = document.createElement('div');
3075
3075
  msgDiv.id = 'uvf-temp-message';
3076
3076
  msgDiv.textContent = message;
@@ -3087,9 +3087,9 @@ export class WebPlayer extends BasePlayer {
3087
3087
  z-index: 10001;
3088
3088
  pointer-events: none;
3089
3089
  `;
3090
-
3090
+
3091
3091
  document.body.appendChild(msgDiv);
3092
-
3092
+
3093
3093
  setTimeout(() => {
3094
3094
  if (msgDiv.parentElement) {
3095
3095
  msgDiv.remove();
@@ -3106,27 +3106,27 @@ export class WebPlayer extends BasePlayer {
3106
3106
  }
3107
3107
 
3108
3108
  // Check if fullscreen is supported
3109
- if (!document.fullscreenEnabled &&
3110
- !(document as any).webkitFullscreenEnabled &&
3111
- !(document as any).mozFullScreenEnabled &&
3112
- !(document as any).msFullscreenEnabled) {
3109
+ if (!document.fullscreenEnabled &&
3110
+ !(document as any).webkitFullscreenEnabled &&
3111
+ !(document as any).mozFullScreenEnabled &&
3112
+ !(document as any).msFullscreenEnabled) {
3113
3113
  throw new Error('Fullscreen not supported by browser');
3114
3114
  }
3115
-
3115
+
3116
3116
  // Check if already in fullscreen
3117
- if (document.fullscreenElement ||
3118
- (document as any).webkitFullscreenElement ||
3119
- (document as any).mozFullScreenElement ||
3120
- (document as any).msFullscreenElement) {
3117
+ if (document.fullscreenElement ||
3118
+ (document as any).webkitFullscreenElement ||
3119
+ (document as any).mozFullScreenElement ||
3120
+ (document as any).msFullscreenElement) {
3121
3121
  this.debugLog('Already in fullscreen mode');
3122
3122
  return;
3123
3123
  }
3124
-
3124
+
3125
3125
  this.debugLog('Attempting synchronous fullscreen');
3126
-
3126
+
3127
3127
  // Target the player wrapper to maintain custom controls
3128
3128
  const element = this.playerWrapper;
3129
-
3129
+
3130
3130
  // Call fullscreen API synchronously (no await) to preserve user gesture
3131
3131
  if (element.requestFullscreen) {
3132
3132
  element.requestFullscreen().then(() => {
@@ -3165,23 +3165,23 @@ export class WebPlayer extends BasePlayer {
3165
3165
 
3166
3166
  try {
3167
3167
  // Check if fullscreen is supported
3168
- if (!document.fullscreenEnabled &&
3169
- !(document as any).webkitFullscreenEnabled &&
3170
- !(document as any).mozFullScreenEnabled &&
3171
- !(document as any).msFullscreenEnabled) {
3168
+ if (!document.fullscreenEnabled &&
3169
+ !(document as any).webkitFullscreenEnabled &&
3170
+ !(document as any).mozFullScreenEnabled &&
3171
+ !(document as any).msFullscreenEnabled) {
3172
3172
  this.debugWarn('Fullscreen not supported by browser');
3173
3173
  return false;
3174
3174
  }
3175
-
3175
+
3176
3176
  // Check if already in fullscreen
3177
- if (document.fullscreenElement ||
3178
- (document as any).webkitFullscreenElement ||
3179
- (document as any).mozFullScreenElement ||
3180
- (document as any).msFullscreenElement) {
3177
+ if (document.fullscreenElement ||
3178
+ (document as any).webkitFullscreenElement ||
3179
+ (document as any).mozFullScreenElement ||
3180
+ (document as any).msFullscreenElement) {
3181
3181
  this.debugLog('Already in fullscreen mode');
3182
3182
  return false;
3183
3183
  }
3184
-
3184
+
3185
3185
  // Check if this is within a reasonable time of user interaction
3186
3186
  const timeSinceInteraction = Date.now() - this.lastUserInteraction;
3187
3187
  this.debugLog('Attempting fullscreen within user gesture context', {
@@ -3189,10 +3189,10 @@ export class WebPlayer extends BasePlayer {
3189
3189
  timeSinceInteraction,
3190
3190
  isTrusted: event.isTrusted
3191
3191
  });
3192
-
3192
+
3193
3193
  // Target the player wrapper to maintain custom controls
3194
3194
  const element = this.playerWrapper;
3195
-
3195
+
3196
3196
  // Try fullscreen immediately while in the event context
3197
3197
  if (element.requestFullscreen) {
3198
3198
  await element.requestFullscreen();
@@ -3206,13 +3206,13 @@ export class WebPlayer extends BasePlayer {
3206
3206
  this.debugWarn('Fullscreen API not supported by this browser');
3207
3207
  return false;
3208
3208
  }
3209
-
3209
+
3210
3210
  // Add fullscreen class for styling
3211
3211
  this.playerWrapper.classList.add('uvf-fullscreen');
3212
3212
  this.emit('onFullscreenChanged', true);
3213
3213
  this.debugLog('Successfully entered fullscreen');
3214
3214
  return true;
3215
-
3215
+
3216
3216
  } catch (error) {
3217
3217
  this.debugWarn('Failed to enter fullscreen:', (error as Error).message);
3218
3218
  return false;
@@ -3241,7 +3241,7 @@ export class WebPlayer extends BasePlayer {
3241
3241
  <button class="uvf-instruction-close" onclick="this.parentElement.parentElement.remove()">Got it</button>
3242
3242
  </div>
3243
3243
  `;
3244
-
3244
+
3245
3245
  overlay.style.cssText = `
3246
3246
  position: absolute;
3247
3247
  top: 0;
@@ -3257,7 +3257,7 @@ export class WebPlayer extends BasePlayer {
3257
3257
  color: white;
3258
3258
  text-align: center;
3259
3259
  `;
3260
-
3260
+
3261
3261
  const content = overlay.querySelector('.uvf-fullscreen-instruction-content') as HTMLElement;
3262
3262
  if (content) {
3263
3263
  content.style.cssText = `
@@ -3270,7 +3270,7 @@ export class WebPlayer extends BasePlayer {
3270
3270
  animation: fadeIn 0.3s ease-out;
3271
3271
  `;
3272
3272
  }
3273
-
3273
+
3274
3274
  const icon = overlay.querySelector('.uvf-fullscreen-icon') as HTMLElement;
3275
3275
  if (icon) {
3276
3276
  icon.style.cssText = `
@@ -3278,7 +3278,7 @@ export class WebPlayer extends BasePlayer {
3278
3278
  margin-bottom: 16px;
3279
3279
  `;
3280
3280
  }
3281
-
3281
+
3282
3282
  const title = overlay.querySelector('h3') as HTMLElement;
3283
3283
  if (title) {
3284
3284
  title.style.cssText = `
@@ -3287,7 +3287,7 @@ export class WebPlayer extends BasePlayer {
3287
3287
  font-weight: 600;
3288
3288
  `;
3289
3289
  }
3290
-
3290
+
3291
3291
  const text = overlay.querySelector('p') as HTMLElement;
3292
3292
  if (text) {
3293
3293
  text.style.cssText = `
@@ -3296,7 +3296,7 @@ export class WebPlayer extends BasePlayer {
3296
3296
  opacity: 0.9;
3297
3297
  `;
3298
3298
  }
3299
-
3299
+
3300
3300
  const pointer = overlay.querySelector('.uvf-fullscreen-pointer') as HTMLElement;
3301
3301
  if (pointer) {
3302
3302
  pointer.style.cssText = `
@@ -3305,7 +3305,7 @@ export class WebPlayer extends BasePlayer {
3305
3305
  margin-bottom: 24px;
3306
3306
  `;
3307
3307
  }
3308
-
3308
+
3309
3309
  const button = overlay.querySelector('.uvf-instruction-close') as HTMLElement;
3310
3310
  if (button) {
3311
3311
  button.style.cssText = `
@@ -3319,18 +3319,18 @@ export class WebPlayer extends BasePlayer {
3319
3319
  cursor: pointer;
3320
3320
  transition: all 0.2s ease;
3321
3321
  `;
3322
-
3322
+
3323
3323
  button.addEventListener('mouseenter', () => {
3324
3324
  button.style.transform = 'scale(1.05)';
3325
3325
  button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
3326
3326
  });
3327
-
3327
+
3328
3328
  button.addEventListener('mouseleave', () => {
3329
3329
  button.style.transform = 'scale(1)';
3330
3330
  button.style.boxShadow = 'none';
3331
3331
  });
3332
3332
  }
3333
-
3333
+
3334
3334
  // Add CSS animation
3335
3335
  if (!document.getElementById('uvf-fullscreen-animation-styles')) {
3336
3336
  const style = document.createElement('style');
@@ -3343,24 +3343,24 @@ export class WebPlayer extends BasePlayer {
3343
3343
  `;
3344
3344
  document.head.appendChild(style);
3345
3345
  }
3346
-
3346
+
3347
3347
  // Add to player wrapper
3348
3348
  if (this.playerWrapper) {
3349
3349
  this.playerWrapper.appendChild(overlay);
3350
3350
  }
3351
-
3351
+
3352
3352
  // Auto-remove after 5 seconds
3353
3353
  setTimeout(() => {
3354
3354
  if (overlay.parentElement) {
3355
3355
  overlay.remove();
3356
3356
  }
3357
3357
  }, 5000);
3358
-
3358
+
3359
3359
  // Also highlight the fullscreen button if it exists
3360
3360
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
3361
3361
  if (fullscreenBtn) {
3362
3362
  fullscreenBtn.style.animation = 'pulse 2s infinite';
3363
-
3363
+
3364
3364
  // Add pulse animation if not already added
3365
3365
  if (!document.getElementById('uvf-pulse-animation-styles')) {
3366
3366
  const style = document.createElement('style');
@@ -3374,13 +3374,13 @@ export class WebPlayer extends BasePlayer {
3374
3374
  `;
3375
3375
  document.head.appendChild(style);
3376
3376
  }
3377
-
3377
+
3378
3378
  // Remove pulse after 3 seconds
3379
3379
  setTimeout(() => {
3380
3380
  fullscreenBtn.style.animation = '';
3381
3381
  }, 3000);
3382
3382
  }
3383
-
3383
+
3384
3384
  this.debugLog('Showing fullscreen instructions overlay');
3385
3385
  }
3386
3386
 
@@ -3393,22 +3393,22 @@ export class WebPlayer extends BasePlayer {
3393
3393
  return true;
3394
3394
  } catch (error) {
3395
3395
  const errorMessage = (error as Error).message;
3396
-
3397
- if (errorMessage.includes('user gesture') ||
3398
- errorMessage.includes('user activation') ||
3399
- errorMessage.includes('Permissions check failed')) {
3400
-
3396
+
3397
+ if (errorMessage.includes('user gesture') ||
3398
+ errorMessage.includes('user activation') ||
3399
+ errorMessage.includes('Permissions check failed')) {
3400
+
3401
3401
  // Try using the fullscreen button as a last resort
3402
3402
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
3403
3403
  if (fullscreenBtn && !this.hasTriedButtonFallback) {
3404
3404
  this.hasTriedButtonFallback = true;
3405
3405
  this.debugLog('Attempting fullscreen via button as fallback');
3406
-
3406
+
3407
3407
  // Reset flag after a short delay
3408
3408
  setTimeout(() => {
3409
3409
  this.hasTriedButtonFallback = false;
3410
3410
  }, 1000);
3411
-
3411
+
3412
3412
  // Try clicking the button programmatically
3413
3413
  const clickEvent = new MouseEvent('click', {
3414
3414
  bubbles: true,
@@ -3433,7 +3433,7 @@ export class WebPlayer extends BasePlayer {
3433
3433
 
3434
3434
  protected applySubtitleTrack(track: any): void {
3435
3435
  if (!this.video) return;
3436
-
3436
+
3437
3437
  const tracks = this.video.textTracks;
3438
3438
  for (let i = 0; i < tracks.length; i++) {
3439
3439
  const textTrack = tracks[i];
@@ -3447,7 +3447,7 @@ export class WebPlayer extends BasePlayer {
3447
3447
 
3448
3448
  protected removeSubtitles(): void {
3449
3449
  if (!this.video) return;
3450
-
3450
+
3451
3451
  const tracks = this.video.textTracks;
3452
3452
  for (let i = 0; i < tracks.length; i++) {
3453
3453
  tracks[i].mode = 'hidden';
@@ -3456,7 +3456,7 @@ export class WebPlayer extends BasePlayer {
3456
3456
 
3457
3457
  private injectStyles(): void {
3458
3458
  if (document.getElementById('uvf-player-styles')) return;
3459
-
3459
+
3460
3460
  const style = document.createElement('style');
3461
3461
  style.id = 'uvf-player-styles';
3462
3462
  style.textContent = this.getPlayerStyles();
@@ -6680,17 +6680,17 @@ export class WebPlayer extends BasePlayer {
6680
6680
  * Creates framework branding logo
6681
6681
  * Only creates branding if showFrameworkBranding is not explicitly set to false
6682
6682
  */
6683
- private createFrameworkBranding(container: HTMLElement): void {
6684
- // Double-check configuration (defensive programming)
6685
- if ((this.config as any).showFrameworkBranding === false) {
6686
- this.debugLog('Framework branding disabled by configuration');
6687
- return;
6688
- }
6689
- const brandingContainer = document.createElement('div');
6690
- brandingContainer.className = 'uvf-framework-branding';
6691
- brandingContainer.setAttribute('title', 'Powered by flicknexs');
6692
-
6693
- const logoSvg = `
6683
+ private createFrameworkBranding(container: HTMLElement): void {
6684
+ // Double-check configuration (defensive programming)
6685
+ if ((this.config as any).showFrameworkBranding === false) {
6686
+ this.debugLog('Framework branding disabled by configuration');
6687
+ return;
6688
+ }
6689
+ const brandingContainer = document.createElement('div');
6690
+ brandingContainer.className = 'uvf-framework-branding';
6691
+ brandingContainer.setAttribute('title', 'Powered by flicknexs');
6692
+
6693
+ const logoSvg = `
6694
6694
  <svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" id="Layer_1" viewBox="0 0 180 29" class="uvf-logo-svg">
6695
6695
  <defs>
6696
6696
  <style>.cls-1{fill:#0d5ef8;}.cls-2{fill:#ff0366;opacity:0.94;}.cls-3{fill:#fff;}</style>
@@ -6710,35 +6710,35 @@ export class WebPlayer extends BasePlayer {
6710
6710
  <path class="cls-3" d="M17.5,8.84l-2.33.74c-1.12.36-2,.63-2.63.82-.92.28-1.78.52-2.58.74s-1.61.41-2.41.59C8.22,13.68,9,16.3,9.72,19.6s1.37,6.23,1.79,8.8a18.88,18.88,0,0,0,2-.44,14.36,14.36,0,0,0,1.8-.64c-.08-1-.2-2-.34-3.2s-.31-2.38-.5-3.69c.55-.16,1.14-.3,1.76-.43s1.31-.26,2.07-.38q-.2-1.15-.39-1.92a8.8,8.8,0,0,0-.5-1.42,16.83,16.83,0,0,0-1.74.38,14.8,14.8,0,0,0-1.73.6c-.1-.59-.2-1.18-.32-1.78s-.24-1.18-.37-1.76L15,13.26l3-.82a8.59,8.59,0,0,0,0-1,6.88,6.88,0,0,0-.08-.83q-.09-.54-.21-1a6.18,6.18,0,0,0-.29-.78Z"/>
6711
6711
  </svg>
6712
6712
  `;
6713
-
6714
- brandingContainer.innerHTML = logoSvg;
6715
-
6716
- // Add click handler to redirect to flicknexs.com
6717
- brandingContainer.addEventListener('click', (event) => {
6718
- event.preventDefault();
6719
- event.stopPropagation();
6720
-
6721
- // Open flicknexs.com in a new tab/window
6722
- const link = document.createElement('a');
6723
- link.href = 'https://flicknexs.com/';
6724
- link.target = '_blank';
6725
- link.rel = 'noopener noreferrer';
6726
- document.body.appendChild(link);
6727
- link.click();
6728
- document.body.removeChild(link);
6729
-
6730
- // Emit analytics event
6731
- this.emit('frameworkBrandingClick', {
6732
- timestamp: Date.now(),
6733
- url: 'https://flicknexs.com/',
6734
- userAgent: navigator.userAgent
6735
- });
6736
- });
6737
-
6738
- container.appendChild(brandingContainer);
6739
-
6740
- this.debugLog('Framework branding added');
6741
- }
6713
+
6714
+ brandingContainer.innerHTML = logoSvg;
6715
+
6716
+ // Add click handler to redirect to flicknexs.com
6717
+ brandingContainer.addEventListener('click', (event) => {
6718
+ event.preventDefault();
6719
+ event.stopPropagation();
6720
+
6721
+ // Open flicknexs.com in a new tab/window
6722
+ const link = document.createElement('a');
6723
+ link.href = 'https://flicknexs.com/';
6724
+ link.target = '_blank';
6725
+ link.rel = 'noopener noreferrer';
6726
+ document.body.appendChild(link);
6727
+ link.click();
6728
+ document.body.removeChild(link);
6729
+
6730
+ // Emit analytics event
6731
+ this.emit('frameworkBrandingClick', {
6732
+ timestamp: Date.now(),
6733
+ url: 'https://flicknexs.com/',
6734
+ userAgent: navigator.userAgent
6735
+ });
6736
+ });
6737
+
6738
+ container.appendChild(brandingContainer);
6739
+
6740
+ this.debugLog('Framework branding added');
6741
+ }
6742
6742
 
6743
6743
  /**
6744
6744
  * Create navigation buttons (back/close) based on configuration
@@ -6756,16 +6756,16 @@ export class WebPlayer extends BasePlayer {
6756
6756
  backBtn.id = 'uvf-back-btn';
6757
6757
  backBtn.title = backButton.title || 'Back';
6758
6758
  backBtn.setAttribute('aria-label', backButton.ariaLabel || 'Go back');
6759
-
6759
+
6760
6760
  // Get icon based on config
6761
6761
  const backIcon = this.getNavigationIcon(backButton.icon || 'arrow', backButton.customIcon);
6762
6762
  backBtn.innerHTML = backIcon;
6763
-
6763
+
6764
6764
  // Add click handler
6765
6765
  backBtn.addEventListener('click', async (e) => {
6766
6766
  e.preventDefault();
6767
6767
  e.stopPropagation();
6768
-
6768
+
6769
6769
  if (backButton.onClick) {
6770
6770
  await backButton.onClick();
6771
6771
  } else if (backButton.href) {
@@ -6778,10 +6778,10 @@ export class WebPlayer extends BasePlayer {
6778
6778
  // Default: go back in history
6779
6779
  window.history.back();
6780
6780
  }
6781
-
6781
+
6782
6782
  this.emit('navigationBackClicked');
6783
6783
  });
6784
-
6784
+
6785
6785
  container.appendChild(backBtn);
6786
6786
  }
6787
6787
 
@@ -6792,16 +6792,16 @@ export class WebPlayer extends BasePlayer {
6792
6792
  closeBtn.id = 'uvf-close-btn';
6793
6793
  closeBtn.title = closeButton.title || 'Close';
6794
6794
  closeBtn.setAttribute('aria-label', closeButton.ariaLabel || 'Close player');
6795
-
6795
+
6796
6796
  // Get icon based on config
6797
6797
  const closeIcon = this.getNavigationIcon(closeButton.icon || 'x', closeButton.customIcon);
6798
6798
  closeBtn.innerHTML = closeIcon;
6799
-
6799
+
6800
6800
  // Add click handler
6801
6801
  closeBtn.addEventListener('click', async (e) => {
6802
6802
  e.preventDefault();
6803
6803
  e.stopPropagation();
6804
-
6804
+
6805
6805
  if (closeButton.onClick) {
6806
6806
  await closeButton.onClick();
6807
6807
  } else {
@@ -6809,7 +6809,7 @@ export class WebPlayer extends BasePlayer {
6809
6809
  if (closeButton.exitFullscreen && this.isFullscreen()) {
6810
6810
  await this.exitFullscreen();
6811
6811
  }
6812
-
6812
+
6813
6813
  if (closeButton.closeModal) {
6814
6814
  // Hide player or remove from DOM
6815
6815
  const playerWrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
@@ -6818,10 +6818,10 @@ export class WebPlayer extends BasePlayer {
6818
6818
  }
6819
6819
  }
6820
6820
  }
6821
-
6821
+
6822
6822
  this.emit('navigationCloseClicked');
6823
6823
  });
6824
-
6824
+
6825
6825
  container.appendChild(closeBtn);
6826
6826
  }
6827
6827
  }
@@ -6843,22 +6843,22 @@ export class WebPlayer extends BasePlayer {
6843
6843
  return `<svg viewBox="0 0 24 24">
6844
6844
  <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
6845
6845
  </svg>`;
6846
-
6846
+
6847
6847
  case 'chevron':
6848
6848
  return `<svg viewBox="0 0 24 24">
6849
6849
  <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
6850
6850
  </svg>`;
6851
-
6851
+
6852
6852
  case 'x':
6853
6853
  return `<svg viewBox="0 0 24 24">
6854
6854
  <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
6855
6855
  </svg>`;
6856
-
6856
+
6857
6857
  case 'close':
6858
6858
  return `<svg viewBox="0 0 24 24">
6859
6859
  <path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z" fill="currentColor"/>
6860
6860
  </svg>`;
6861
-
6861
+
6862
6862
  default:
6863
6863
  return `<svg viewBox="0 0 24 24">
6864
6864
  <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
@@ -6875,21 +6875,21 @@ export class WebPlayer extends BasePlayer {
6875
6875
  const controlsGradient = document.createElement('div');
6876
6876
  controlsGradient.className = 'uvf-controls-gradient';
6877
6877
  container.appendChild(controlsGradient);
6878
-
6878
+
6879
6879
  // Combined top bar: navigation buttons → title → controls
6880
6880
  const topBar = document.createElement('div');
6881
6881
  topBar.className = 'uvf-top-bar';
6882
-
6882
+
6883
6883
  // Left side container for navigation + title
6884
6884
  const leftSide = document.createElement('div');
6885
6885
  leftSide.className = 'uvf-left-side';
6886
-
6886
+
6887
6887
  // Navigation buttons (back/close)
6888
6888
  const navigationControls = document.createElement('div');
6889
6889
  navigationControls.className = 'uvf-navigation-controls';
6890
6890
  this.createNavigationButtons(navigationControls);
6891
6891
  leftSide.appendChild(navigationControls);
6892
-
6892
+
6893
6893
  // Title bar (after navigation buttons)
6894
6894
  const titleBar = document.createElement('div');
6895
6895
  titleBar.className = 'uvf-title-bar';
@@ -6902,9 +6902,9 @@ export class WebPlayer extends BasePlayer {
6902
6902
  </div>
6903
6903
  `;
6904
6904
  leftSide.appendChild(titleBar);
6905
-
6905
+
6906
6906
  topBar.appendChild(leftSide);
6907
-
6907
+
6908
6908
  // Top controls (right side - Cast and Share buttons)
6909
6909
  const topControls = document.createElement('div');
6910
6910
  topControls.className = 'uvf-top-controls';
@@ -6927,12 +6927,12 @@ export class WebPlayer extends BasePlayer {
6927
6927
  </button>
6928
6928
  `;
6929
6929
  topBar.appendChild(topControls);
6930
-
6930
+
6931
6931
  container.appendChild(topBar);
6932
6932
 
6933
- // Add loading spinner
6933
+ // Add loading spinner (show by default until video is ready)
6934
6934
  const loadingContainer = document.createElement('div');
6935
- loadingContainer.className = 'uvf-loading-container';
6935
+ loadingContainer.className = 'uvf-loading-container active'; // Start with active class
6936
6936
  loadingContainer.id = 'uvf-loading';
6937
6937
  loadingContainer.innerHTML = '<div class="uvf-loading-spinner"></div>';
6938
6938
  container.appendChild(loadingContainer);
@@ -6940,15 +6940,15 @@ export class WebPlayer extends BasePlayer {
6940
6940
  // Add center play button container for proper responsive centering
6941
6941
  const centerPlayContainer = document.createElement('div');
6942
6942
  centerPlayContainer.className = 'uvf-center-play-container';
6943
-
6943
+
6944
6944
  const centerPlayBtn = document.createElement('div');
6945
6945
  centerPlayBtn.className = 'uvf-center-play-btn uvf-pulse';
6946
6946
  centerPlayBtn.id = 'uvf-center-play';
6947
6947
  centerPlayBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
6948
-
6948
+
6949
6949
  centerPlayContainer.appendChild(centerPlayBtn);
6950
6950
  container.appendChild(centerPlayContainer);
6951
-
6951
+
6952
6952
  // Add shortcut indicator
6953
6953
  const shortcutIndicator = document.createElement('div');
6954
6954
  shortcutIndicator.className = 'uvf-shortcut-indicator';
@@ -6959,11 +6959,11 @@ export class WebPlayer extends BasePlayer {
6959
6959
  const controlsBar = document.createElement('div');
6960
6960
  controlsBar.className = 'uvf-controls-bar';
6961
6961
  controlsBar.id = 'uvf-controls';
6962
-
6962
+
6963
6963
  // Time and branding section above seekbar
6964
6964
  const aboveSeekbarSection = document.createElement('div');
6965
6965
  aboveSeekbarSection.className = 'uvf-above-seekbar-section';
6966
-
6966
+
6967
6967
  // Time display (moved to above seekbar)
6968
6968
  const timeDisplay = document.createElement('div');
6969
6969
  timeDisplay.className = 'uvf-time-display uvf-above-seekbar';
@@ -6975,11 +6975,11 @@ export class WebPlayer extends BasePlayer {
6975
6975
  if ((this.config as any).showFrameworkBranding !== false) {
6976
6976
  this.createFrameworkBranding(aboveSeekbarSection);
6977
6977
  }
6978
-
6978
+
6979
6979
  // Progress section
6980
6980
  const progressSection = document.createElement('div');
6981
6981
  progressSection.className = 'uvf-progress-section';
6982
-
6982
+
6983
6983
  const progressBar = document.createElement('div');
6984
6984
  progressBar.className = 'uvf-progress-bar-wrapper';
6985
6985
  progressBar.id = 'uvf-progress-bar';
@@ -6992,11 +6992,11 @@ export class WebPlayer extends BasePlayer {
6992
6992
  <div class="uvf-time-tooltip" id="uvf-time-tooltip">00:00</div>
6993
6993
  `;
6994
6994
  progressSection.appendChild(progressBar);
6995
-
6995
+
6996
6996
  // Controls row
6997
6997
  const controlsRow = document.createElement('div');
6998
6998
  controlsRow.className = 'uvf-controls-row';
6999
-
6999
+
7000
7000
  // Play/Pause button
7001
7001
  const playPauseBtn = document.createElement('button');
7002
7002
  playPauseBtn.className = 'uvf-control-btn play-pause';
@@ -7010,7 +7010,7 @@ export class WebPlayer extends BasePlayer {
7010
7010
  </svg>
7011
7011
  `;
7012
7012
  controlsRow.appendChild(playPauseBtn);
7013
-
7013
+
7014
7014
  // Skip buttons with consistent icons
7015
7015
  const skipBackBtn = document.createElement('button');
7016
7016
  skipBackBtn.className = 'uvf-control-btn';
@@ -7019,7 +7019,7 @@ export class WebPlayer extends BasePlayer {
7019
7019
  skipBackBtn.setAttribute('aria-label', 'Skip backward 10 seconds');
7020
7020
  skipBackBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
7021
7021
  controlsRow.appendChild(skipBackBtn);
7022
-
7022
+
7023
7023
  const skipForwardBtn = document.createElement('button');
7024
7024
  skipForwardBtn.className = 'uvf-control-btn';
7025
7025
  skipForwardBtn.id = 'uvf-skip-forward';
@@ -7027,7 +7027,7 @@ export class WebPlayer extends BasePlayer {
7027
7027
  skipForwardBtn.setAttribute('aria-label', 'Skip forward 10 seconds');
7028
7028
  skipForwardBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
7029
7029
  controlsRow.appendChild(skipForwardBtn);
7030
-
7030
+
7031
7031
  // Volume control
7032
7032
  const volumeControl = document.createElement('div');
7033
7033
  volumeControl.className = 'uvf-volume-control';
@@ -7048,23 +7048,23 @@ export class WebPlayer extends BasePlayer {
7048
7048
  </div>
7049
7049
  `;
7050
7050
  controlsRow.appendChild(volumeControl);
7051
-
7051
+
7052
7052
  // Right controls
7053
7053
  const rightControls = document.createElement('div');
7054
7054
  rightControls.className = 'uvf-right-controls';
7055
-
7055
+
7056
7056
  // Quality badge
7057
7057
  const qualityBadge = document.createElement('div');
7058
7058
  qualityBadge.className = 'uvf-quality-badge';
7059
7059
  qualityBadge.id = 'uvf-quality-badge';
7060
7060
  qualityBadge.textContent = 'HD';
7061
7061
  rightControls.appendChild(qualityBadge);
7062
-
7062
+
7063
7063
  // Settings button with menu (show only if enabled)
7064
7064
  this.debugLog('Settings config check:', this.settingsConfig);
7065
7065
  this.debugLog('Settings enabled:', this.settingsConfig.enabled);
7066
7066
  this.debugLog('Custom controls enabled:', this.useCustomControls);
7067
-
7067
+
7068
7068
  if (this.settingsConfig.enabled) {
7069
7069
  this.debugLog('Creating settings button...');
7070
7070
  const settingsContainer = document.createElement('div');
@@ -7073,7 +7073,7 @@ export class WebPlayer extends BasePlayer {
7073
7073
  settingsContainer.style.display = 'flex';
7074
7074
  settingsContainer.style.alignItems = 'center';
7075
7075
  settingsContainer.style.justifyContent = 'center';
7076
-
7076
+
7077
7077
  const settingsBtn = document.createElement('button');
7078
7078
  settingsBtn.className = 'uvf-control-btn';
7079
7079
  settingsBtn.id = 'uvf-settings-btn';
@@ -7081,7 +7081,7 @@ export class WebPlayer extends BasePlayer {
7081
7081
  settingsBtn.setAttribute('aria-label', 'Settings');
7082
7082
  settingsBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>';
7083
7083
  settingsContainer.appendChild(settingsBtn);
7084
-
7084
+
7085
7085
  // Settings menu - will be populated dynamically based on video capabilities and configuration
7086
7086
  const settingsMenu = document.createElement('div');
7087
7087
  settingsMenu.className = 'uvf-settings-menu';
@@ -7091,9 +7091,9 @@ export class WebPlayer extends BasePlayer {
7091
7091
  // CSS handles initial hidden state
7092
7092
  settingsContainer.appendChild(settingsMenu);
7093
7093
  rightControls.appendChild(settingsContainer);
7094
-
7094
+
7095
7095
  this.debugLog('Settings button created and added to controls');
7096
-
7096
+
7097
7097
  // Add debugging for settings button visibility
7098
7098
  setTimeout(() => {
7099
7099
  const createdBtn = document.getElementById('uvf-settings-btn');
@@ -7107,10 +7107,10 @@ export class WebPlayer extends BasePlayer {
7107
7107
  this.debugLog('- Height:', computedStyle.height);
7108
7108
  this.debugLog('- Position:', computedStyle.position);
7109
7109
  this.debugLog('- Z-index:', computedStyle.zIndex);
7110
-
7110
+
7111
7111
  const rect = createdBtn.getBoundingClientRect();
7112
7112
  this.debugLog('- Bounding rect:', { width: rect.width, height: rect.height, top: rect.top, left: rect.left });
7113
-
7113
+
7114
7114
  // Check parent containers
7115
7115
  const settingsContainer = createdBtn.closest('.uvf-settings-container');
7116
7116
  if (settingsContainer) {
@@ -7120,7 +7120,7 @@ export class WebPlayer extends BasePlayer {
7120
7120
  this.debugLog('Settings container visibility:', containerStyle.visibility);
7121
7121
  this.debugLog('Settings container rect:', { width: containerRect.width, height: containerRect.height, top: containerRect.top, left: containerRect.left });
7122
7122
  }
7123
-
7123
+
7124
7124
  const rightControls = createdBtn.closest('.uvf-right-controls');
7125
7125
  if (rightControls) {
7126
7126
  const parentStyle = window.getComputedStyle(rightControls);
@@ -7132,7 +7132,7 @@ export class WebPlayer extends BasePlayer {
7132
7132
  } else {
7133
7133
  this.debugError('Settings button NOT found after creation!');
7134
7134
  }
7135
-
7135
+
7136
7136
  // Fallback: Force settings button visibility if it's not rendering properly
7137
7137
  setTimeout(() => {
7138
7138
  const btn = document.getElementById('uvf-settings-btn');
@@ -7152,7 +7152,7 @@ export class WebPlayer extends BasePlayer {
7152
7152
  btn.style.border = '1px solid rgba(255,255,255,0.1)';
7153
7153
  btn.style.position = 'relative';
7154
7154
  btn.style.zIndex = '10';
7155
-
7155
+
7156
7156
  // Also fix the container
7157
7157
  const container = btn.parentElement;
7158
7158
  if (container) {
@@ -7162,7 +7162,7 @@ export class WebPlayer extends BasePlayer {
7162
7162
  container.style.minWidth = '44px';
7163
7163
  container.style.minHeight = '44px';
7164
7164
  }
7165
-
7165
+
7166
7166
  this.debugLog('Fallback styles applied to settings button');
7167
7167
  }
7168
7168
  }
@@ -7171,7 +7171,7 @@ export class WebPlayer extends BasePlayer {
7171
7171
  } else {
7172
7172
  this.debugLog('Settings button NOT created - settings disabled');
7173
7173
  }
7174
-
7174
+
7175
7175
  // EPG button (Electronic Program Guide)
7176
7176
  const epgBtn = document.createElement('button');
7177
7177
  epgBtn.className = 'uvf-control-btn';
@@ -7185,42 +7185,42 @@ export class WebPlayer extends BasePlayer {
7185
7185
  </svg>`;
7186
7186
  epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
7187
7187
  rightControls.appendChild(epgBtn);
7188
-
7188
+
7189
7189
  // PiP button - only show on desktop/supported browsers
7190
7190
  const pipBtn = document.createElement('button');
7191
7191
  pipBtn.className = 'uvf-control-btn';
7192
7192
  pipBtn.id = 'uvf-pip-btn';
7193
7193
  pipBtn.title = 'Picture-in-Picture';
7194
7194
  pipBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
7195
-
7195
+
7196
7196
  // Hide PiP button on mobile devices and browsers that don't support it
7197
7197
  if (this.isMobileDevice() || !this.isPipSupported()) {
7198
7198
  pipBtn.style.display = 'none';
7199
7199
  }
7200
-
7200
+
7201
7201
  rightControls.appendChild(pipBtn);
7202
-
7202
+
7203
7203
  // Fullscreen button
7204
7204
  const fullscreenBtn = document.createElement('button');
7205
7205
  fullscreenBtn.className = 'uvf-control-btn';
7206
7206
  fullscreenBtn.id = 'uvf-fullscreen-btn';
7207
7207
  fullscreenBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
7208
7208
  rightControls.appendChild(fullscreenBtn);
7209
-
7209
+
7210
7210
  controlsRow.appendChild(rightControls);
7211
-
7211
+
7212
7212
  // Assemble controls bar
7213
7213
  controlsBar.appendChild(aboveSeekbarSection);
7214
7214
  controlsBar.appendChild(progressSection);
7215
7215
  controlsBar.appendChild(controlsRow);
7216
7216
  container.appendChild(controlsBar);
7217
-
7217
+
7218
7218
  this.controlsContainer = controlsBar;
7219
7219
  }
7220
7220
 
7221
7221
  private setupControlsEventListeners(): void {
7222
7222
  if (!this.useCustomControls || !this.video) return;
7223
-
7223
+
7224
7224
  const wrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
7225
7225
  const centerPlay = document.getElementById('uvf-center-play');
7226
7226
  const playPauseBtn = document.getElementById('uvf-play-pause');
@@ -7232,25 +7232,25 @@ export class WebPlayer extends BasePlayer {
7232
7232
  const progressBar = document.getElementById('uvf-progress-bar');
7233
7233
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
7234
7234
  const settingsBtn = document.getElementById('uvf-settings-btn');
7235
-
7235
+
7236
7236
  // Get the event target (video element or YouTube player)
7237
7237
  const getEventTarget = () => this.youtubePlayer && this.youtubePlayerReady ? this.youtubePlayer : this.video;
7238
-
7238
+
7239
7239
  // Disable right-click context menu
7240
7240
  this.video.addEventListener('contextmenu', (e) => {
7241
7241
  e.preventDefault();
7242
7242
  return false;
7243
7243
  });
7244
-
7244
+
7245
7245
  wrapper?.addEventListener('contextmenu', (e) => {
7246
7246
  e.preventDefault();
7247
7247
  return false;
7248
7248
  });
7249
-
7249
+
7250
7250
  // Play/Pause
7251
7251
  centerPlay?.addEventListener('click', () => this.togglePlayPause());
7252
7252
  playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
7253
-
7253
+
7254
7254
  // Video click behavior will be handled by the comprehensive tap system below
7255
7255
  // Desktop click for play/pause
7256
7256
  if (!this.isMobileDevice()) {
@@ -7258,20 +7258,20 @@ export class WebPlayer extends BasePlayer {
7258
7258
  this.togglePlayPause();
7259
7259
  });
7260
7260
  }
7261
-
7261
+
7262
7262
  // Update play/pause icons
7263
7263
  this.video.addEventListener('play', () => {
7264
7264
  const playIcon = document.getElementById('uvf-play-icon');
7265
7265
  const pauseIcon = document.getElementById('uvf-pause-icon');
7266
7266
  if (playIcon) playIcon.style.display = 'none';
7267
7267
  if (pauseIcon) pauseIcon.style.display = 'block';
7268
-
7268
+
7269
7269
  // Hide center play button when playing
7270
7270
  if (centerPlay) {
7271
7271
  centerPlay.classList.add('hidden');
7272
7272
  this.debugLog('Center play button hidden - video playing');
7273
7273
  }
7274
-
7274
+
7275
7275
  // Schedule hide controls
7276
7276
  setTimeout(() => {
7277
7277
  if (this.state.isPlaying) {
@@ -7279,13 +7279,13 @@ export class WebPlayer extends BasePlayer {
7279
7279
  }
7280
7280
  }, 1000);
7281
7281
  });
7282
-
7282
+
7283
7283
  this.video.addEventListener('pause', () => {
7284
7284
  const playIcon = document.getElementById('uvf-play-icon');
7285
7285
  const pauseIcon = document.getElementById('uvf-pause-icon');
7286
7286
  if (playIcon) playIcon.style.display = 'block';
7287
7287
  if (pauseIcon) pauseIcon.style.display = 'none';
7288
-
7288
+
7289
7289
  // Show center play button when paused
7290
7290
  if (centerPlay) {
7291
7291
  centerPlay.classList.remove('hidden');
@@ -7293,7 +7293,7 @@ export class WebPlayer extends BasePlayer {
7293
7293
  }
7294
7294
  this.showControls();
7295
7295
  });
7296
-
7296
+
7297
7297
  // Ensure center play button is visible initially when video is paused/stopped
7298
7298
  this.video.addEventListener('loadeddata', () => {
7299
7299
  if (centerPlay && (this.video?.paused || this.video?.ended)) {
@@ -7301,14 +7301,14 @@ export class WebPlayer extends BasePlayer {
7301
7301
  this.debugLog('Center play button shown - video loaded and paused');
7302
7302
  }
7303
7303
  });
7304
-
7304
+
7305
7305
  this.video.addEventListener('ended', () => {
7306
7306
  if (centerPlay) {
7307
7307
  centerPlay.classList.remove('hidden');
7308
7308
  this.debugLog('Center play button shown - video ended');
7309
7309
  }
7310
7310
  });
7311
-
7311
+
7312
7312
  // Skip buttons with null safety - works with both video and YouTube
7313
7313
  skipBackBtn?.addEventListener('click', () => {
7314
7314
  const currentTime = this.getCurrentTime();
@@ -7326,19 +7326,19 @@ export class WebPlayer extends BasePlayer {
7326
7326
  this.showShortcutIndicator('+10s');
7327
7327
  }
7328
7328
  });
7329
-
7329
+
7330
7330
  // Volume control
7331
7331
  volumeBtn?.addEventListener('click', (e) => {
7332
7332
  e.stopPropagation();
7333
7333
  this.toggleMuteAction();
7334
7334
  });
7335
-
7335
+
7336
7336
  // Volume panel interactions
7337
7337
  volumeBtn?.addEventListener('mouseenter', () => {
7338
7338
  if (this.volumeHideTimeout) clearTimeout(this.volumeHideTimeout);
7339
7339
  volumePanel?.classList.add('active');
7340
7340
  });
7341
-
7341
+
7342
7342
  volumeBtn?.addEventListener('mouseleave', () => {
7343
7343
  this.volumeHideTimeout = setTimeout(() => {
7344
7344
  if (!volumePanel?.matches(':hover')) {
@@ -7346,12 +7346,12 @@ export class WebPlayer extends BasePlayer {
7346
7346
  }
7347
7347
  }, 800);
7348
7348
  });
7349
-
7349
+
7350
7350
  volumePanel?.addEventListener('mouseenter', () => {
7351
7351
  if (this.volumeHideTimeout) clearTimeout(this.volumeHideTimeout);
7352
7352
  volumePanel.classList.add('active');
7353
7353
  });
7354
-
7354
+
7355
7355
  volumePanel?.addEventListener('mouseleave', () => {
7356
7356
  if (!this.isVolumeSliding) {
7357
7357
  setTimeout(() => {
@@ -7361,7 +7361,7 @@ export class WebPlayer extends BasePlayer {
7361
7361
  }, 1500);
7362
7362
  }
7363
7363
  });
7364
-
7364
+
7365
7365
  // Volume slider
7366
7366
  volumeSlider?.addEventListener('mousedown', (e) => {
7367
7367
  e.stopPropagation();
@@ -7369,43 +7369,43 @@ export class WebPlayer extends BasePlayer {
7369
7369
  volumePanel?.classList.add('active');
7370
7370
  this.handleVolumeChange(e as MouseEvent);
7371
7371
  });
7372
-
7372
+
7373
7373
  volumeSlider?.addEventListener('click', (e) => {
7374
7374
  e.stopPropagation();
7375
7375
  this.handleVolumeChange(e as MouseEvent);
7376
7376
  });
7377
-
7378
-
7377
+
7378
+
7379
7379
  // Progress bar interactions
7380
7380
  progressBar?.addEventListener('click', (e) => {
7381
7381
  this.seekToPosition(e as MouseEvent);
7382
7382
  });
7383
-
7383
+
7384
7384
  progressBar?.addEventListener('mousedown', (e) => {
7385
7385
  this.isDragging = true;
7386
7386
  this.showTimeTooltip = true;
7387
7387
  this.seekToPosition(e as MouseEvent);
7388
7388
  this.updateTimeTooltip(e as MouseEvent);
7389
7389
  });
7390
-
7390
+
7391
7391
  // Hover tooltip functionality
7392
7392
  progressBar?.addEventListener('mouseenter', () => {
7393
7393
  this.showTimeTooltip = true;
7394
7394
  });
7395
-
7395
+
7396
7396
  progressBar?.addEventListener('mouseleave', () => {
7397
7397
  if (!this.isDragging) {
7398
7398
  this.showTimeTooltip = false;
7399
7399
  this.hideTimeTooltip();
7400
7400
  }
7401
7401
  });
7402
-
7402
+
7403
7403
  progressBar?.addEventListener('mousemove', (e) => {
7404
7404
  if (this.showTimeTooltip) {
7405
7405
  this.updateTimeTooltip(e as MouseEvent);
7406
7406
  }
7407
7407
  });
7408
-
7408
+
7409
7409
  // Touch support for mobile devices
7410
7410
  progressBar?.addEventListener('touchstart', (e) => {
7411
7411
  e.preventDefault(); // Prevent scrolling
@@ -7417,7 +7417,7 @@ export class WebPlayer extends BasePlayer {
7417
7417
  });
7418
7418
  this.seekToPosition(mouseEvent);
7419
7419
  }, { passive: false });
7420
-
7420
+
7421
7421
  // Global mouse and touch events for enhanced dragging
7422
7422
  document.addEventListener('mousemove', (e) => {
7423
7423
  if (this.isVolumeSliding) {
@@ -7429,7 +7429,7 @@ export class WebPlayer extends BasePlayer {
7429
7429
  this.updateTimeTooltip(e);
7430
7430
  }
7431
7431
  });
7432
-
7432
+
7433
7433
  document.addEventListener('touchmove', (e) => {
7434
7434
  if (this.isDragging && progressBar) {
7435
7435
  e.preventDefault(); // Prevent scrolling
@@ -7441,7 +7441,7 @@ export class WebPlayer extends BasePlayer {
7441
7441
  this.seekToPosition(mouseEvent);
7442
7442
  }
7443
7443
  }, { passive: false });
7444
-
7444
+
7445
7445
  document.addEventListener('mouseup', () => {
7446
7446
  if (this.isVolumeSliding) {
7447
7447
  this.isVolumeSliding = false;
@@ -7451,7 +7451,7 @@ export class WebPlayer extends BasePlayer {
7451
7451
  }
7452
7452
  }, 2000);
7453
7453
  }
7454
-
7454
+
7455
7455
  if (this.isDragging) {
7456
7456
  this.isDragging = false;
7457
7457
  // Remove dragging class from handle
@@ -7464,7 +7464,7 @@ export class WebPlayer extends BasePlayer {
7464
7464
  }
7465
7465
  }
7466
7466
  });
7467
-
7467
+
7468
7468
  document.addEventListener('touchend', () => {
7469
7469
  if (this.isDragging) {
7470
7470
  this.isDragging = false;
@@ -7476,26 +7476,26 @@ export class WebPlayer extends BasePlayer {
7476
7476
  this.hideTimeTooltip();
7477
7477
  }
7478
7478
  });
7479
-
7479
+
7480
7480
  // Update progress bar
7481
7481
  this.video.addEventListener('timeupdate', () => {
7482
7482
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
7483
7483
  const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
7484
-
7484
+
7485
7485
  if (this.video && progressFilled) {
7486
7486
  const percent = (this.video.currentTime / this.video.duration) * 100;
7487
7487
  progressFilled.style.width = percent + '%';
7488
-
7488
+
7489
7489
  // Update handle position (only when not dragging)
7490
7490
  if (progressHandle && !this.isDragging) {
7491
7491
  progressHandle.style.left = percent + '%';
7492
7492
  }
7493
7493
  }
7494
-
7494
+
7495
7495
  // Update time display using the dedicated method
7496
7496
  this.updateTimeDisplay();
7497
7497
  });
7498
-
7498
+
7499
7499
  // Update buffered progress
7500
7500
  this.video.addEventListener('progress', () => {
7501
7501
  const progressBuffered = document.getElementById('uvf-progress-buffered') as HTMLElement;
@@ -7504,7 +7504,7 @@ export class WebPlayer extends BasePlayer {
7504
7504
  progressBuffered.style.width = buffered + '%';
7505
7505
  }
7506
7506
  });
7507
-
7507
+
7508
7508
  // Update volume display
7509
7509
  this.video.addEventListener('volumechange', () => {
7510
7510
  const volumeFill = document.getElementById('uvf-volume-fill') as HTMLElement;
@@ -7533,7 +7533,7 @@ export class WebPlayer extends BasePlayer {
7533
7533
  }
7534
7534
  }
7535
7535
  });
7536
-
7536
+
7537
7537
  // Fullscreen button with enhanced cross-platform support
7538
7538
  fullscreenBtn?.addEventListener('click', (event) => {
7539
7539
  // Enhanced debugging for all platforms
@@ -7542,7 +7542,7 @@ export class WebPlayer extends BasePlayer {
7542
7542
  const isIOS = this.isIOSDevice();
7543
7543
  const isAndroid = this.isAndroidDevice();
7544
7544
  const isMobile = this.isMobileDevice();
7545
-
7545
+
7546
7546
  this.debugLog('Fullscreen button clicked:', {
7547
7547
  isBrave,
7548
7548
  isPrivate,
@@ -7555,13 +7555,13 @@ export class WebPlayer extends BasePlayer {
7555
7555
  timestamp: Date.now(),
7556
7556
  fullscreenSupported: this.isFullscreenSupported()
7557
7557
  });
7558
-
7558
+
7559
7559
  // Update user interaction timestamp
7560
7560
  this.lastUserInteraction = Date.now();
7561
-
7561
+
7562
7562
  // Run permissions check before attempting fullscreen
7563
7563
  this.checkFullscreenPermissions();
7564
-
7564
+
7565
7565
  if (this.isFullscreen()) {
7566
7566
  this.debugLog('Exiting fullscreen via button');
7567
7567
  this.exitFullscreen().catch(err => {
@@ -7569,18 +7569,18 @@ export class WebPlayer extends BasePlayer {
7569
7569
  });
7570
7570
  } else {
7571
7571
  this.debugLog('Entering fullscreen via button');
7572
-
7572
+
7573
7573
  // iOS Safari special message
7574
7574
  if (isIOS) {
7575
7575
  this.showShortcutIndicator('Using iOS video fullscreen');
7576
7576
  } else if (isAndroid) {
7577
7577
  this.showShortcutIndicator('Entering fullscreen - rotate to landscape');
7578
7578
  }
7579
-
7579
+
7580
7580
  // Use enhanced cross-platform fullscreen method
7581
7581
  this.enterFullscreen().catch(err => {
7582
7582
  this.debugWarn('Fullscreen button failed:', err.message);
7583
-
7583
+
7584
7584
  // Platform-specific error messages
7585
7585
  if (isIOS) {
7586
7586
  this.showTemporaryMessage('iOS: Use device rotation or video controls for fullscreen');
@@ -7594,69 +7594,70 @@ export class WebPlayer extends BasePlayer {
7594
7594
  });
7595
7595
  }
7596
7596
  });
7597
-
7597
+
7598
7598
  // Update fullscreen button icon based on state
7599
7599
  const updateFullscreenIcon = () => {
7600
7600
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
7601
7601
  if (fullscreenBtn) {
7602
- fullscreenBtn.innerHTML = this.isFullscreen()
7602
+ fullscreenBtn.innerHTML = this.isFullscreen()
7603
7603
  ? '<svg viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>'
7604
7604
  : '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
7605
7605
  }
7606
7606
  };
7607
-
7607
+
7608
7608
  // Listen for fullscreen state changes to update icon
7609
7609
  this.on('onFullscreenChanged', updateFullscreenIcon);
7610
-
7610
+
7611
7611
  // Loading states
7612
7612
  this.video.addEventListener('waiting', () => {
7613
7613
  const loading = document.getElementById('uvf-loading');
7614
7614
  if (loading) loading.classList.add('active');
7615
7615
  });
7616
-
7616
+
7617
7617
  this.video.addEventListener('canplay', () => {
7618
7618
  const loading = document.getElementById('uvf-loading');
7619
7619
  if (loading) loading.classList.remove('active');
7620
7620
  // Update settings menu when video is ready
7621
7621
  this.updateSettingsMenu();
7622
+ this.debugLog('📡 canplay event fired - video ready to play');
7622
7623
  });
7623
7624
 
7624
7625
  this.video.addEventListener('loadedmetadata', () => {
7625
7626
  // Update settings menu when metadata is loaded
7626
7627
  this.updateSettingsMenu();
7627
7628
  });
7628
-
7629
+
7629
7630
  // Note: Enhanced mouse movement and control visibility handled in setupFullscreenListeners()
7630
-
7631
+
7631
7632
  this.controlsContainer?.addEventListener('mouseenter', () => {
7632
7633
  if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
7633
7634
  });
7634
-
7635
+
7635
7636
  this.controlsContainer?.addEventListener('mouseleave', () => {
7636
7637
  if (this.state.isPlaying) {
7637
7638
  this.scheduleHideControls();
7638
7639
  }
7639
7640
  });
7640
-
7641
-
7641
+
7642
+
7642
7643
  // Settings menu - dynamically populated
7643
7644
  const settingsMenu = document.getElementById('uvf-settings-menu');
7644
7645
  this.debugLog('Settings menu element found:', !!settingsMenu);
7645
7646
  this.debugLog('Settings button found:', !!settingsBtn);
7646
-
7647
+
7647
7648
  settingsBtn?.addEventListener('click', (e) => {
7648
7649
  e.stopPropagation();
7649
7650
  this.debugLog('Settings button clicked!');
7650
7651
  this.debugLog('Settings menu before update:', settingsMenu?.innerHTML?.length || 0, 'characters');
7651
-
7652
+
7652
7653
  // Update the menu content before showing it
7653
7654
  this.updateSettingsMenu();
7654
-
7655
+
7655
7656
  this.debugLog('Settings menu after update:', settingsMenu?.innerHTML?.length || 0, 'characters');
7656
7657
  this.debugLog('Settings menu classes before toggle:', Array.from(settingsMenu?.classList || []).join(' '));
7657
-
7658
+
7658
7659
  settingsMenu?.classList.toggle('active');
7659
-
7660
+
7660
7661
  // Force visibility if menu is active, hide if not active
7661
7662
  if (settingsMenu) {
7662
7663
  if (settingsMenu.classList.contains('active')) {
@@ -7681,13 +7682,13 @@ export class WebPlayer extends BasePlayer {
7681
7682
  this.debugLog('Applied fallback styles to hide menu');
7682
7683
  }
7683
7684
  }
7684
-
7685
+
7685
7686
  this.debugLog('Settings menu classes after toggle:', Array.from(settingsMenu?.classList || []).join(' '));
7686
7687
  this.debugLog('Settings menu computed display:', window.getComputedStyle(settingsMenu || document.body).display);
7687
7688
  this.debugLog('Settings menu computed visibility:', window.getComputedStyle(settingsMenu || document.body).visibility);
7688
7689
  this.debugLog('Settings menu computed opacity:', window.getComputedStyle(settingsMenu || document.body).opacity);
7689
7690
  });
7690
-
7691
+
7691
7692
  // EPG button
7692
7693
  const epgBtn = document.getElementById('uvf-epg-btn');
7693
7694
  epgBtn?.addEventListener('click', (e) => {
@@ -7696,16 +7697,16 @@ export class WebPlayer extends BasePlayer {
7696
7697
  // Trigger custom event for EPG toggle
7697
7698
  this.emit('epgToggle', {});
7698
7699
  });
7699
-
7700
+
7700
7701
  // PiP button
7701
7702
  const pipBtn = document.getElementById('uvf-pip-btn');
7702
7703
  pipBtn?.addEventListener('click', () => this.togglePiP());
7703
-
7704
+
7704
7705
  // Top control buttons
7705
7706
  const castBtn = document.getElementById('uvf-cast-btn');
7706
7707
  const stopCastBtn = document.getElementById('uvf-stop-cast-btn');
7707
7708
  const shareBtn = document.getElementById('uvf-share-btn');
7708
-
7709
+
7709
7710
  // Update cast button icon and functionality for iOS (AirPlay)
7710
7711
  if (this.isIOSDevice() && castBtn) {
7711
7712
  castBtn.innerHTML = `
@@ -7716,19 +7717,19 @@ export class WebPlayer extends BasePlayer {
7716
7717
  castBtn.setAttribute('title', 'AirPlay');
7717
7718
  castBtn.setAttribute('aria-label', 'AirPlay');
7718
7719
  }
7719
-
7720
+
7720
7721
  castBtn?.addEventListener('click', () => this.onCastButtonClick());
7721
7722
  stopCastBtn?.addEventListener('click', () => this.stopCasting());
7722
7723
  shareBtn?.addEventListener('click', () => this.shareVideo());
7723
-
7724
+
7724
7725
  // Hide settings menu when clicking outside or pressing Escape
7725
7726
  document.addEventListener('click', (e) => {
7726
- if (!(e.target as HTMLElement).closest('#uvf-settings-btn') &&
7727
- !(e.target as HTMLElement).closest('#uvf-settings-menu')) {
7727
+ if (!(e.target as HTMLElement).closest('#uvf-settings-btn') &&
7728
+ !(e.target as HTMLElement).closest('#uvf-settings-menu')) {
7728
7729
  this.hideSettingsMenu();
7729
7730
  }
7730
7731
  });
7731
-
7732
+
7732
7733
  // Add Escape key handler for settings menu
7733
7734
  document.addEventListener('keydown', (e) => {
7734
7735
  if (e.key === 'Escape' && settingsMenu?.classList.contains('active')) {
@@ -7736,7 +7737,7 @@ export class WebPlayer extends BasePlayer {
7736
7737
  }
7737
7738
  });
7738
7739
  }
7739
-
7740
+
7740
7741
  protected setupKeyboardShortcuts(): void {
7741
7742
  // Add keyboard event listener to both document and player wrapper for better coverage
7742
7743
  const handleKeydown = (e: KeyboardEvent) => {
@@ -7745,23 +7746,23 @@ export class WebPlayer extends BasePlayer {
7745
7746
  if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
7746
7747
  return;
7747
7748
  }
7748
-
7749
+
7749
7750
  // Block all keyboard controls during ads (Google IMA handles ad controls)
7750
7751
  if (this.isAdPlaying) {
7751
7752
  this.debugLog('Keyboard blocked: Ad is playing');
7752
7753
  e.preventDefault();
7753
7754
  return;
7754
7755
  }
7755
-
7756
+
7756
7757
  // Debug logging
7757
7758
  this.debugLog('Keyboard event:', e.key, 'target:', target.tagName);
7758
-
7759
+
7759
7760
  let shortcutText = '';
7760
-
7761
+
7761
7762
  // Update interaction timestamp
7762
7763
  this.lastUserInteraction = Date.now();
7763
-
7764
- switch(e.key) {
7764
+
7765
+ switch (e.key) {
7765
7766
  case ' ':
7766
7767
  case 'Spacebar': // For older browsers
7767
7768
  case 'k':
@@ -7772,13 +7773,13 @@ export class WebPlayer extends BasePlayer {
7772
7773
  videoPaused: this.video?.paused,
7773
7774
  videoExists: !!this.video
7774
7775
  });
7775
-
7776
+
7776
7777
  // Determine what action we're about to take based on current video state
7777
7778
  const willPlay = this.video?.paused || false;
7778
7779
  this.debugLog('Will perform action:', willPlay ? 'PLAY' : 'PAUSE');
7779
-
7780
+
7780
7781
  this.togglePlayPause();
7781
-
7782
+
7782
7783
  // Show the action we're taking, not the current state
7783
7784
  shortcutText = willPlay ? 'Play' : 'Pause';
7784
7785
  this.debugLog('Showing icon:', shortcutText);
@@ -7844,7 +7845,7 @@ export class WebPlayer extends BasePlayer {
7844
7845
  break;
7845
7846
  case 'f':
7846
7847
  e.preventDefault();
7847
-
7848
+
7848
7849
  if (!document.fullscreenElement) {
7849
7850
  // Always use the fullscreen button for maximum reliability
7850
7851
  this.triggerFullscreenButton();
@@ -7888,26 +7889,26 @@ export class WebPlayer extends BasePlayer {
7888
7889
  }
7889
7890
  break;
7890
7891
  }
7891
-
7892
+
7892
7893
  if (shortcutText) {
7893
7894
  this.debugLog('Showing shortcut indicator:', shortcutText);
7894
7895
  this.showShortcutIndicator(shortcutText);
7895
7896
  }
7896
7897
  };
7897
-
7898
+
7898
7899
  // Add event listeners to multiple targets for better coverage
7899
7900
  document.addEventListener('keydown', handleKeydown, { capture: true });
7900
-
7901
+
7901
7902
  // Also add to the player wrapper if it exists
7902
7903
  if (this.playerWrapper) {
7903
7904
  this.playerWrapper.addEventListener('keydown', handleKeydown);
7904
7905
  this.playerWrapper.setAttribute('tabindex', '0'); // Make it focusable
7905
-
7906
+
7906
7907
  // Add visual feedback when player is focused for better UX
7907
7908
  this.playerWrapper.addEventListener('focus', () => {
7908
7909
  this.debugLog('Player focused - keyboard shortcuts available');
7909
7910
  });
7910
-
7911
+
7911
7912
  // Auto-focus the player when clicked to enable keyboard shortcuts
7912
7913
  this.playerWrapper.addEventListener('click', (e) => {
7913
7914
  // Don't focus if clicking on a control button
@@ -7918,17 +7919,17 @@ export class WebPlayer extends BasePlayer {
7918
7919
  this.lastUserInteraction = Date.now();
7919
7920
  }
7920
7921
  });
7921
-
7922
+
7922
7923
  // Also focus on any interaction with the video area
7923
7924
  this.playerWrapper.addEventListener('mousedown', () => {
7924
7925
  this.playerWrapper?.focus();
7925
7926
  this.lastUserInteraction = Date.now();
7926
7927
  });
7927
-
7928
+
7928
7929
  // Advanced tap handling system for mobile
7929
7930
  this.setupAdvancedTapHandling();
7930
7931
  }
7931
-
7932
+
7932
7933
  // Add to the video element
7933
7934
  if (this.video) {
7934
7935
  this.video.addEventListener('keydown', handleKeydown);
@@ -7937,25 +7938,25 @@ export class WebPlayer extends BasePlayer {
7937
7938
 
7938
7939
  protected setupWatermark(): void {
7939
7940
  if (!this.watermarkCanvas) return;
7940
-
7941
+
7941
7942
  // Get watermark configuration
7942
7943
  const watermarkConfig = (this.config as any).watermark;
7943
-
7944
+
7944
7945
  // Check if watermark is disabled or not configured
7945
7946
  if (!watermarkConfig || watermarkConfig.enabled === false) {
7946
7947
  this.debugLog('Watermark disabled or not configured');
7947
7948
  return;
7948
7949
  }
7949
-
7950
+
7950
7951
  // If watermark config exists but enabled is not explicitly set, default to disabled
7951
7952
  if (watermarkConfig.enabled !== true) {
7952
7953
  this.debugLog('Watermark not explicitly enabled');
7953
7954
  return;
7954
7955
  }
7955
-
7956
+
7956
7957
  const ctx = this.watermarkCanvas.getContext('2d');
7957
7958
  if (!ctx) return;
7958
-
7959
+
7959
7960
  // Default configuration values
7960
7961
  const config = {
7961
7962
  text: watermarkConfig.text || 'PREMIUM',
@@ -7971,31 +7972,31 @@ export class WebPlayer extends BasePlayer {
7971
7972
  gradientColors: watermarkConfig.style?.gradientColors || ['#ff0000', '#ff4d4f']
7972
7973
  }
7973
7974
  };
7974
-
7975
+
7975
7976
  this.debugLog('Watermark configuration:', config);
7976
-
7977
+
7977
7978
  const renderWatermark = () => {
7978
7979
  const container = this.watermarkCanvas!.parentElement;
7979
7980
  if (!container) return;
7980
-
7981
+
7981
7982
  this.watermarkCanvas!.width = container.offsetWidth;
7982
7983
  this.watermarkCanvas!.height = container.offsetHeight;
7983
-
7984
+
7984
7985
  ctx.clearRect(0, 0, this.watermarkCanvas!.width, this.watermarkCanvas!.height);
7985
-
7986
+
7986
7987
  // Build watermark text
7987
7988
  let text = config.text;
7988
7989
  if (config.showTime) {
7989
7990
  const timeStr = new Date().toLocaleTimeString();
7990
7991
  text += ` • ${timeStr}`;
7991
7992
  }
7992
-
7993
+
7993
7994
  // Set up styling
7994
7995
  ctx.save();
7995
7996
  ctx.globalAlpha = config.style.opacity;
7996
7997
  ctx.font = `${config.style.fontSize}px ${config.style.fontFamily}`;
7997
7998
  ctx.textAlign = 'left';
7998
-
7999
+
7999
8000
  // Set fill style
8000
8001
  if (config.style.color) {
8001
8002
  // Use solid color
@@ -8005,7 +8006,7 @@ export class WebPlayer extends BasePlayer {
8005
8006
  const wrapper = this.playerWrapper as HTMLElement | null;
8006
8007
  let c1 = config.style.gradientColors[0];
8007
8008
  let c2 = config.style.gradientColors[1];
8008
-
8009
+
8009
8010
  // Try to get theme colors if using defaults
8010
8011
  if (!watermarkConfig.style?.gradientColors) {
8011
8012
  try {
@@ -8016,18 +8017,18 @@ export class WebPlayer extends BasePlayer {
8016
8017
  if (v1) c1 = v1;
8017
8018
  if (v2) c2 = v2;
8018
8019
  }
8019
- } catch (_) {}
8020
+ } catch (_) { }
8020
8021
  }
8021
-
8022
+
8022
8023
  const gradient = ctx.createLinearGradient(0, 0, 200, 0);
8023
8024
  gradient.addColorStop(0, c1);
8024
8025
  gradient.addColorStop(1, c2);
8025
8026
  ctx.fillStyle = gradient;
8026
8027
  }
8027
-
8028
+
8028
8029
  // Calculate position
8029
8030
  let x: number, y: number;
8030
-
8031
+
8031
8032
  if (config.randomPosition) {
8032
8033
  // Random position (default behavior)
8033
8034
  x = 20 + Math.random() * Math.max(0, this.watermarkCanvas!.width - 200);
@@ -8036,7 +8037,7 @@ export class WebPlayer extends BasePlayer {
8036
8037
  // Fixed or calculated position
8037
8038
  const posX = config.position.x;
8038
8039
  const posY = config.position.y;
8039
-
8040
+
8040
8041
  // Calculate X position
8041
8042
  if (typeof posX === 'number') {
8042
8043
  x = posX;
@@ -8060,7 +8061,7 @@ export class WebPlayer extends BasePlayer {
8060
8061
  x = 20; // default left
8061
8062
  }
8062
8063
  }
8063
-
8064
+
8064
8065
  // Calculate Y position
8065
8066
  if (typeof posY === 'number') {
8066
8067
  y = posY;
@@ -8083,18 +8084,18 @@ export class WebPlayer extends BasePlayer {
8083
8084
  }
8084
8085
  }
8085
8086
  }
8086
-
8087
+
8087
8088
  // Render the watermark
8088
8089
  ctx.fillText(text, x, y);
8089
8090
  ctx.restore();
8090
-
8091
+
8091
8092
  this.debugLog('Watermark rendered:', { text, x, y });
8092
8093
  };
8093
-
8094
+
8094
8095
  // Set up interval with configured frequency
8095
8096
  setInterval(renderWatermark, config.updateInterval);
8096
8097
  renderWatermark(); // Render immediately
8097
-
8098
+
8098
8099
  this.debugLog('Watermark setup complete with update interval:', config.updateInterval + 'ms');
8099
8100
  }
8100
8101
 
@@ -8109,12 +8110,12 @@ export class WebPlayer extends BasePlayer {
8109
8110
  import('./paywall/PaywallController').then((m: any) => {
8110
8111
  this.paywallController = new m.PaywallController(config, {
8111
8112
  getOverlayContainer: () => this.playerWrapper,
8112
- onResume: (accessInfo?: any) => {
8113
- try {
8113
+ onResume: (accessInfo?: any) => {
8114
+ try {
8114
8115
  // Reset preview gate after successful auth/payment
8115
8116
  this.previewGateHit = false;
8116
8117
  this.paymentSuccessTime = Date.now();
8117
-
8118
+
8118
8119
  // Check if access was granted via email auth
8119
8120
  if (accessInfo && (accessInfo.accessGranted || accessInfo.paymentSuccessful)) {
8120
8121
  this.paymentSuccessful = true;
@@ -8123,22 +8124,22 @@ export class WebPlayer extends BasePlayer {
8123
8124
  this.paymentSuccessful = true;
8124
8125
  this.debugLog('Payment successful (via setPaywallConfig) - preview gate permanently disabled, resuming playback');
8125
8126
  }
8126
-
8127
- this.play();
8128
- } catch(_) {}
8127
+
8128
+ this.play();
8129
+ } catch (_) { }
8129
8130
  },
8130
8131
  onShow: () => {
8131
8132
  // Use safe pause method to avoid race conditions
8132
- try { this.requestPause(); } catch(_) {}
8133
+ try { this.requestPause(); } catch (_) { }
8133
8134
  },
8134
8135
  onClose: () => {
8135
8136
  // Resume video if auth was successful
8136
8137
  }
8137
8138
  });
8138
- }).catch(() => {});
8139
+ }).catch(() => { });
8139
8140
  }
8140
8141
  }
8141
- } catch (_) {}
8142
+ } catch (_) { }
8142
8143
  }
8143
8144
 
8144
8145
  private togglePlayPause(): void {
@@ -8148,12 +8149,12 @@ export class WebPlayer extends BasePlayer {
8148
8149
  youtubePlayerReady: this.youtubePlayerReady,
8149
8150
  playerState: this.state
8150
8151
  });
8151
-
8152
+
8152
8153
  // Handle YouTube player
8153
8154
  if (this.youtubePlayer && this.youtubePlayerReady) {
8154
8155
  const playerState = this.youtubePlayer.getPlayerState();
8155
8156
  this.debugLog('YouTube player state:', playerState);
8156
-
8157
+
8157
8158
  if (playerState === window.YT.PlayerState.PLAYING) {
8158
8159
  this.debugLog('YouTube video is playing, calling pause()');
8159
8160
  this.pause();
@@ -8163,13 +8164,13 @@ export class WebPlayer extends BasePlayer {
8163
8164
  }
8164
8165
  return;
8165
8166
  }
8166
-
8167
+
8167
8168
  // Handle regular video element
8168
8169
  if (!this.video) {
8169
8170
  this.debugError('No video element or YouTube player available for toggle');
8170
8171
  return;
8171
8172
  }
8172
-
8173
+
8173
8174
  if (this.video.paused) {
8174
8175
  this.debugLog('Video is paused, calling play()');
8175
8176
  this.play();
@@ -8185,25 +8186,25 @@ export class WebPlayer extends BasePlayer {
8185
8186
  const lim = Number(this.config.freeDuration || 0);
8186
8187
  if (!(lim > 0)) return;
8187
8188
  if (this.previewGateHit && !fromSeek) return;
8188
-
8189
+
8189
8190
  // Don't trigger gate if payment was successful for this session
8190
8191
  if (this.paymentSuccessful) {
8191
8192
  this.debugLog('Skipping preview gate - payment was successful, access granted permanently for this session');
8192
8193
  return;
8193
8194
  }
8194
-
8195
+
8195
8196
  // Don't trigger gate if payment was successful recently (within 5 seconds)
8196
8197
  const timeSincePayment = Date.now() - this.paymentSuccessTime;
8197
8198
  if (this.paymentSuccessTime > 0 && timeSincePayment < 5000) {
8198
8199
  this.debugLog('Skipping preview gate - recent payment success:', timeSincePayment + 'ms ago');
8199
8200
  return;
8200
8201
  }
8201
-
8202
+
8202
8203
  if (current >= lim - 0.01 && !this.previewGateHit) {
8203
8204
  this.previewGateHit = true;
8204
8205
  this.showNotification('Free preview ended.');
8205
8206
  this.emit('onFreePreviewEnded');
8206
-
8207
+
8207
8208
  // Trigger paywall controller which will handle auth/payment flow
8208
8209
  this.debugLog('Free preview gate hit, paywallController exists:', !!this.paywallController);
8209
8210
  if (this.paywallController) {
@@ -8219,18 +8220,18 @@ export class WebPlayer extends BasePlayer {
8219
8220
  if (this.remotePlayer && this.remotePlayer.isPaused === false) {
8220
8221
  this.remoteController.playOrPause();
8221
8222
  }
8222
- } catch (_) {}
8223
+ } catch (_) { }
8223
8224
  } else if (this.video) {
8224
- try {
8225
+ try {
8225
8226
  // Use deferred pause to avoid race conditions
8226
- this.requestPause();
8227
+ this.requestPause();
8227
8228
  if (fromSeek || ((this.video.currentTime || 0) > lim)) {
8228
8229
  this.safeSetCurrentTime(lim - 0.1);
8229
8230
  }
8230
- } catch (_) {}
8231
+ } catch (_) { }
8231
8232
  }
8232
8233
  }
8233
- } catch (_) {}
8234
+ } catch (_) { }
8234
8235
  }
8235
8236
 
8236
8237
  // Public runtime controls for free preview
@@ -8247,7 +8248,7 @@ export class WebPlayer extends BasePlayer {
8247
8248
  const cur = this.video ? (this.video.currentTime || 0) : 0;
8248
8249
  this.enforceFreePreviewGate(cur, true);
8249
8250
  }
8250
- } catch (_) {}
8251
+ } catch (_) { }
8251
8252
  }
8252
8253
  public resetFreePreviewGate(): void {
8253
8254
  // Only reset if payment hasn't been successful
@@ -8255,7 +8256,7 @@ export class WebPlayer extends BasePlayer {
8255
8256
  this.previewGateHit = false;
8256
8257
  }
8257
8258
  }
8258
-
8259
+
8259
8260
  public resetPaymentStatus(): void {
8260
8261
  this.paymentSuccessful = false;
8261
8262
  this.paymentSuccessTime = 0;
@@ -8265,7 +8266,7 @@ export class WebPlayer extends BasePlayer {
8265
8266
 
8266
8267
  private toggleMuteAction(): void {
8267
8268
  if (this.isCasting && this.remoteController) {
8268
- try { this.remoteController.muteOrUnmute(); } catch (_) {}
8269
+ try { this.remoteController.muteOrUnmute(); } catch (_) { }
8269
8270
  return;
8270
8271
  }
8271
8272
  if (this.video?.muted) {
@@ -8284,7 +8285,7 @@ export class WebPlayer extends BasePlayer {
8284
8285
  const isMobileUserAgent = mobileKeywords.some(keyword => userAgent.includes(keyword));
8285
8286
  const isSmallScreen = window.innerWidth <= 768;
8286
8287
  const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
8287
-
8288
+
8288
8289
  return isMobileUserAgent || (isSmallScreen && hasTouchScreen);
8289
8290
  }
8290
8291
 
@@ -8391,21 +8392,21 @@ export class WebPlayer extends BasePlayer {
8391
8392
  private handleVolumeChange(e: MouseEvent): void {
8392
8393
  const slider = document.getElementById('uvf-volume-slider');
8393
8394
  if (!slider) return;
8394
-
8395
+
8395
8396
  const rect = slider.getBoundingClientRect();
8396
8397
  const x = e.clientX - rect.left;
8397
8398
  const width = rect.width;
8398
8399
  const percent = Math.max(0, Math.min(1, x / width));
8399
-
8400
+
8400
8401
  if (this.isCasting && this.remoteController && this.remotePlayer) {
8401
8402
  try {
8402
8403
  if (this.remotePlayer.isMuted) {
8403
- try { this.remoteController.muteOrUnmute(); } catch (_) {}
8404
+ try { this.remoteController.muteOrUnmute(); } catch (_) { }
8404
8405
  this.remotePlayer.isMuted = false;
8405
8406
  }
8406
8407
  this.remotePlayer.volumeLevel = percent;
8407
8408
  this.remoteController.setVolumeLevel();
8408
- } catch (_) {}
8409
+ } catch (_) { }
8409
8410
  this.updateVolumeUIFromRemote();
8410
8411
  } else if (this.video) {
8411
8412
  this.setVolume(percent);
@@ -8418,25 +8419,25 @@ export class WebPlayer extends BasePlayer {
8418
8419
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
8419
8420
  const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
8420
8421
  if (!progressBar || !this.video) return;
8421
-
8422
+
8422
8423
  const duration = this.video.duration;
8423
8424
  // Validate duration before calculating seek time
8424
8425
  if (!isFinite(duration) || isNaN(duration) || duration <= 0) {
8425
8426
  this.debugWarn('Invalid video duration, cannot seek via progress bar');
8426
8427
  return;
8427
8428
  }
8428
-
8429
+
8429
8430
  const rect = progressBar.getBoundingClientRect();
8430
8431
  const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
8431
8432
  const percent = (x / rect.width) * 100;
8432
8433
  const time = (percent / 100) * duration;
8433
-
8434
+
8434
8435
  // Validate calculated time
8435
8436
  if (!isFinite(time) || isNaN(time)) {
8436
8437
  this.debugWarn('Calculated seek time is invalid:', time);
8437
8438
  return;
8438
8439
  }
8439
-
8440
+
8440
8441
  // Update UI immediately for responsive feedback
8441
8442
  if (progressFilled) {
8442
8443
  progressFilled.style.width = percent + '%';
@@ -8450,17 +8451,17 @@ export class WebPlayer extends BasePlayer {
8450
8451
  progressHandle.classList.remove('dragging');
8451
8452
  }
8452
8453
  }
8453
-
8454
+
8454
8455
  this.seek(time);
8455
8456
  }
8456
8457
 
8457
8458
  private formatTime(seconds: number): string {
8458
8459
  if (!seconds || isNaN(seconds)) return '00:00';
8459
-
8460
+
8460
8461
  const hours = Math.floor(seconds / 3600);
8461
8462
  const minutes = Math.floor((seconds % 3600) / 60);
8462
8463
  const secs = Math.floor(seconds % 60);
8463
-
8464
+
8464
8465
  if (hours > 0) {
8465
8466
  return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
8466
8467
  } else {
@@ -8471,10 +8472,10 @@ export class WebPlayer extends BasePlayer {
8471
8472
  private updateTimeDisplay(): void {
8472
8473
  const timeDisplay = document.getElementById('uvf-time-display');
8473
8474
  if (!timeDisplay) return;
8474
-
8475
+
8475
8476
  let current = 0;
8476
8477
  let duration = 0;
8477
-
8478
+
8478
8479
  // Get time from YouTube player if available
8479
8480
  if (this.youtubePlayer && this.youtubePlayerReady) {
8480
8481
  try {
@@ -8487,7 +8488,7 @@ export class WebPlayer extends BasePlayer {
8487
8488
  current = this.video.currentTime || 0;
8488
8489
  duration = this.video.duration || 0;
8489
8490
  }
8490
-
8491
+
8491
8492
  const currentFormatted = this.formatTime(current);
8492
8493
  const durationFormatted = this.formatTime(duration);
8493
8494
  timeDisplay.textContent = `${currentFormatted} / ${durationFormatted}`;
@@ -8505,7 +8506,7 @@ export class WebPlayer extends BasePlayer {
8505
8506
 
8506
8507
  private hideControls(): void {
8507
8508
  if (!this.state.isPlaying) return;
8508
-
8509
+
8509
8510
  const wrapper = this.container?.querySelector('.uvf-player-wrapper');
8510
8511
  if (wrapper) {
8511
8512
  wrapper.classList.remove('controls-visible');
@@ -8515,7 +8516,7 @@ export class WebPlayer extends BasePlayer {
8515
8516
 
8516
8517
  private scheduleHideControls(): void {
8517
8518
  if (!this.state.isPlaying) return;
8518
-
8519
+
8519
8520
  if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
8520
8521
  // Use longer timeout in fullscreen for better UX
8521
8522
  const timeout = this.isFullscreen() ? 4000 : 3000;
@@ -8542,7 +8543,7 @@ export class WebPlayer extends BasePlayer {
8542
8543
  const TAP_MOVEMENT_THRESHOLD = 10; // pixels
8543
8544
  const SKIP_SECONDS = 10;
8544
8545
  const FAST_PLAYBACK_RATE = 2;
8545
-
8546
+
8546
8547
  // Track if we're currently in a double-tap window
8547
8548
  let inDoubleTapWindow = false;
8548
8549
 
@@ -8663,7 +8664,7 @@ export class WebPlayer extends BasePlayer {
8663
8664
  this.debugLog('Single tap detected - toggling controls');
8664
8665
  const wrapper = this.container?.querySelector('.uvf-player-wrapper');
8665
8666
  const areControlsVisible = wrapper?.classList.contains('controls-visible');
8666
-
8667
+
8667
8668
  if (areControlsVisible) {
8668
8669
  // Hide controls and top UI elements
8669
8670
  this.hideControls();
@@ -8672,7 +8673,7 @@ export class WebPlayer extends BasePlayer {
8672
8673
  // Show controls and top UI elements
8673
8674
  this.showControls();
8674
8675
  this.debugLog('Single tap: showing controls');
8675
-
8676
+
8676
8677
  // Schedule auto-hide if video is playing
8677
8678
  if (this.state.isPlaying) {
8678
8679
  this.scheduleHideControls();
@@ -8692,7 +8693,7 @@ export class WebPlayer extends BasePlayer {
8692
8693
 
8693
8694
  const currentTime = this.video.currentTime;
8694
8695
  const duration = this.video.duration;
8695
-
8696
+
8696
8697
  // Validate current time and duration before calculating new time
8697
8698
  if (!isFinite(currentTime) || isNaN(currentTime) || !isFinite(duration) || isNaN(duration)) {
8698
8699
  this.debugWarn('Invalid video time values, skipping double-tap action');
@@ -8788,19 +8789,19 @@ export class WebPlayer extends BasePlayer {
8788
8789
  this.fastBackwardInterval = null;
8789
8790
  }
8790
8791
  }
8791
-
8792
+
8792
8793
  private isFullscreen(): boolean {
8793
8794
  return !!(document.fullscreenElement ||
8794
- (document as any).webkitFullscreenElement ||
8795
- (document as any).mozFullScreenElement ||
8796
- (document as any).msFullscreenElement);
8795
+ (document as any).webkitFullscreenElement ||
8796
+ (document as any).mozFullScreenElement ||
8797
+ (document as any).msFullscreenElement);
8797
8798
  }
8798
-
8799
+
8799
8800
  private setupFullscreenListeners(): void {
8800
8801
  // Handle fullscreen changes from browser/keyboard shortcuts
8801
8802
  const handleFullscreenChange = () => {
8802
8803
  const isFullscreen = this.isFullscreen();
8803
-
8804
+
8804
8805
  if (this.playerWrapper) {
8805
8806
  if (isFullscreen) {
8806
8807
  this.playerWrapper.classList.add('uvf-fullscreen');
@@ -8808,36 +8809,36 @@ export class WebPlayer extends BasePlayer {
8808
8809
  this.playerWrapper.classList.remove('uvf-fullscreen');
8809
8810
  }
8810
8811
  }
8811
-
8812
+
8812
8813
  // Show controls when entering/exiting fullscreen
8813
8814
  this.showControls();
8814
8815
  if (isFullscreen && this.state.isPlaying) {
8815
8816
  this.scheduleHideControls();
8816
8817
  }
8817
-
8818
+
8818
8819
  this.emit('onFullscreenChanged', isFullscreen);
8819
8820
  };
8820
-
8821
+
8821
8822
  // Listen for fullscreen change events (all browser prefixes)
8822
8823
  document.addEventListener('fullscreenchange', handleFullscreenChange);
8823
8824
  document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
8824
8825
  document.addEventListener('mozfullscreenchange', handleFullscreenChange);
8825
8826
  document.addEventListener('MSFullscreenChange', handleFullscreenChange);
8826
-
8827
+
8827
8828
  // Enhanced mouse/touch movement detection for control visibility
8828
8829
  let lastMouseMoveTime = 0;
8829
8830
  let mouseInactivityTimeout: any = null;
8830
-
8831
+
8831
8832
  const handleMouseMovement = () => {
8832
8833
  const now = Date.now();
8833
8834
  lastMouseMoveTime = now;
8834
-
8835
+
8835
8836
  // Show controls immediately on mouse movement
8836
8837
  this.showControls();
8837
-
8838
+
8838
8839
  // Clear existing inactivity timeout
8839
8840
  clearTimeout(mouseInactivityTimeout);
8840
-
8841
+
8841
8842
  // Set new inactivity timeout
8842
8843
  if (this.state.isPlaying) {
8843
8844
  const timeout = this.isFullscreen() ? 4000 : 3000;
@@ -8849,7 +8850,7 @@ export class WebPlayer extends BasePlayer {
8849
8850
  }, timeout);
8850
8851
  }
8851
8852
  };
8852
-
8853
+
8853
8854
  // Touch movement detection for mobile - only for actual dragging/scrolling
8854
8855
  // Note: Don't handle touchstart here as it conflicts with advanced tap handling
8855
8856
  const handleTouchMovement = () => {
@@ -8859,7 +8860,7 @@ export class WebPlayer extends BasePlayer {
8859
8860
  this.scheduleHideControls();
8860
8861
  }
8861
8862
  };
8862
-
8863
+
8863
8864
  // Add event listeners to the player wrapper
8864
8865
  if (this.playerWrapper) {
8865
8866
  this.playerWrapper.addEventListener('mousemove', handleMouseMovement, { passive: true });
@@ -8869,9 +8870,9 @@ export class WebPlayer extends BasePlayer {
8869
8870
  this.playerWrapper.addEventListener('touchmove', handleTouchMovement, { passive: true });
8870
8871
  }
8871
8872
  }
8872
-
8873
8873
 
8874
-
8874
+
8875
+
8875
8876
  private showShortcutIndicator(text: string): void {
8876
8877
  const el = document.getElementById('uvf-shortcut-indicator');
8877
8878
  this.debugLog('showShortcutIndicator called with:', text, 'element found:', !!el);
@@ -8891,7 +8892,7 @@ export class WebPlayer extends BasePlayer {
8891
8892
  el.innerHTML = `<div class="uvf-ki uvf-ki-icon">${svg}</div>`;
8892
8893
  resetAnim();
8893
8894
  };
8894
- const setSkip = (dir: 'fwd'|'back', num: number) => {
8895
+ const setSkip = (dir: 'fwd' | 'back', num: number) => {
8895
8896
  el.classList.add('uvf-ki-icon');
8896
8897
  const svg = dir === 'fwd'
8897
8898
  ? `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12.01 19c-3.31 0-6-2.69-6-6s2.69-6 6-6V5l5 5-5 5V9c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4h2c0 3.31-2.69 6-6 6z"/></svg>`
@@ -8955,14 +8956,14 @@ export class WebPlayer extends BasePlayer {
8955
8956
  // auto-hide after animation
8956
8957
  clearTimeout(this._kiTo);
8957
8958
  this._kiTo = setTimeout(() => {
8958
- try { el.classList.remove('active'); } catch (_) {}
8959
+ try { el.classList.remove('active'); } catch (_) { }
8959
8960
  }, 1000);
8960
8961
  } catch (err) {
8961
8962
  try {
8962
8963
  (el as HTMLElement).textContent = String(text || '');
8963
8964
  el.classList.add('active');
8964
8965
  setTimeout(() => el.classList.remove('active'), 1000);
8965
- } catch(_) {}
8966
+ } catch (_) { }
8966
8967
  }
8967
8968
  }
8968
8969
 
@@ -9070,7 +9071,7 @@ export class WebPlayer extends BasePlayer {
9070
9071
  };
9071
9072
 
9072
9073
  this.coreChapterManager = new CoreChapterManager(coreChapterConfig);
9073
-
9074
+
9074
9075
  // Initialize the core chapter manager
9075
9076
  this.coreChapterManager.initialize();
9076
9077
 
@@ -9177,7 +9178,7 @@ export class WebPlayer extends BasePlayer {
9177
9178
  if (!this.chapterManager) {
9178
9179
  throw new Error('Chapter manager not initialized. Enable chapters in config first.');
9179
9180
  }
9180
-
9181
+
9181
9182
  try {
9182
9183
  await this.chapterManager.loadChapters(chapters);
9183
9184
  this.debugLog('Chapters loaded successfully');
@@ -9194,7 +9195,7 @@ export class WebPlayer extends BasePlayer {
9194
9195
  if (!this.chapterManager) {
9195
9196
  throw new Error('Chapter manager not initialized. Enable chapters in config first.');
9196
9197
  }
9197
-
9198
+
9198
9199
  try {
9199
9200
  await this.chapterManager.loadChaptersFromUrl(url);
9200
9201
  this.debugLog('Chapters loaded from URL successfully');
@@ -9211,7 +9212,7 @@ export class WebPlayer extends BasePlayer {
9211
9212
  if (!this.chapterManager || !this.video) {
9212
9213
  return null;
9213
9214
  }
9214
-
9215
+
9215
9216
  return this.chapterManager.getCurrentSegment(this.video.currentTime);
9216
9217
  }
9217
9218
 
@@ -9223,7 +9224,7 @@ export class WebPlayer extends BasePlayer {
9223
9224
  this.debugWarn('Cannot skip segment: chapter manager not initialized');
9224
9225
  return;
9225
9226
  }
9226
-
9227
+
9227
9228
  this.chapterManager.skipToSegment(segmentId);
9228
9229
  }
9229
9230
 
@@ -9234,7 +9235,7 @@ export class WebPlayer extends BasePlayer {
9234
9235
  if (!this.chapterManager) {
9235
9236
  return [];
9236
9237
  }
9237
-
9238
+
9238
9239
  return this.chapterManager.getSegments();
9239
9240
  }
9240
9241
 
@@ -9243,7 +9244,7 @@ export class WebPlayer extends BasePlayer {
9243
9244
  */
9244
9245
  public updateChapterConfig(newConfig: Partial<ChapterConfig>): void {
9245
9246
  this.chapterConfig = { ...this.chapterConfig, ...newConfig };
9246
-
9247
+
9247
9248
  if (this.chapterManager) {
9248
9249
  this.chapterManager.updateConfig(this.chapterConfig);
9249
9250
  }
@@ -9370,7 +9371,7 @@ export class WebPlayer extends BasePlayer {
9370
9371
  if (iconColor) wrapper.style.setProperty('--uvf-icon-color', iconColor);
9371
9372
  if (textPrimary) wrapper.style.setProperty('--uvf-text-primary', textPrimary);
9372
9373
  if (textSecondary) wrapper.style.setProperty('--uvf-text-secondary', textSecondary);
9373
-
9374
+
9374
9375
  // Set overlay colors for gradient backgrounds
9375
9376
  if (overlayStrong) wrapper.style.setProperty('--uvf-overlay-strong', overlayStrong);
9376
9377
  if (overlayMedium) wrapper.style.setProperty('--uvf-overlay-medium', overlayMedium);
@@ -9406,7 +9407,7 @@ export class WebPlayer extends BasePlayer {
9406
9407
  return { r: Math.round(nums[0]), g: Math.round(nums[1]), b: Math.round(nums[2]) };
9407
9408
  }
9408
9409
  }
9409
- } catch (_) {}
9410
+ } catch (_) { }
9410
9411
  return null;
9411
9412
  }
9412
9413
 
@@ -9431,26 +9432,26 @@ export class WebPlayer extends BasePlayer {
9431
9432
  const a = Math.max(0, Math.min(1, alpha));
9432
9433
  return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
9433
9434
  }
9434
-
9435
+
9435
9436
  private changeVolume(delta: number): void {
9436
9437
  if (this.isCasting && this.remoteController && this.remotePlayer) {
9437
9438
  const cur = this.remotePlayer.volumeLevel || 0;
9438
9439
  const next = Math.max(0, Math.min(1, cur + delta));
9439
9440
  try {
9440
9441
  if (this.remotePlayer.isMuted) {
9441
- try { this.remoteController.muteOrUnmute(); } catch (_) {}
9442
+ try { this.remoteController.muteOrUnmute(); } catch (_) { }
9442
9443
  this.remotePlayer.isMuted = false;
9443
9444
  }
9444
9445
  this.remotePlayer.volumeLevel = next;
9445
9446
  this.remoteController.setVolumeLevel();
9446
- } catch (_) {}
9447
+ } catch (_) { }
9447
9448
  this.updateVolumeUIFromRemote();
9448
9449
  return;
9449
9450
  }
9450
9451
  if (!this.video) return;
9451
9452
  this.video.volume = Math.max(0, Math.min(1, this.video.volume + delta));
9452
9453
  }
9453
-
9454
+
9454
9455
  private setSpeed(speed: number): void {
9455
9456
  // Handle YouTube player
9456
9457
  if (this.youtubePlayer && this.youtubePlayerReady) {
@@ -9462,7 +9463,7 @@ export class WebPlayer extends BasePlayer {
9462
9463
  } else if (this.video) {
9463
9464
  this.video.playbackRate = speed;
9464
9465
  }
9465
-
9466
+
9466
9467
  // Update UI
9467
9468
  document.querySelectorAll('.speed-option').forEach(option => {
9468
9469
  option.classList.remove('active');
@@ -9471,7 +9472,7 @@ export class WebPlayer extends BasePlayer {
9471
9472
  }
9472
9473
  });
9473
9474
  }
9474
-
9475
+
9475
9476
  private setQualityByLabel(quality: string): void {
9476
9477
  // Handle YouTube player
9477
9478
  if (this.youtubePlayer && this.youtubePlayerReady) {
@@ -9486,9 +9487,9 @@ export class WebPlayer extends BasePlayer {
9486
9487
  });
9487
9488
  return;
9488
9489
  }
9489
-
9490
+
9490
9491
  const qualityBadge = document.getElementById('uvf-quality-badge');
9491
-
9492
+
9492
9493
  // Update UI
9493
9494
  document.querySelectorAll('.quality-option').forEach(option => {
9494
9495
  option.classList.remove('active');
@@ -9496,7 +9497,7 @@ export class WebPlayer extends BasePlayer {
9496
9497
  option.classList.add('active');
9497
9498
  }
9498
9499
  });
9499
-
9500
+
9500
9501
  // Update badge
9501
9502
  if (qualityBadge) {
9502
9503
  if (quality === 'auto') {
@@ -9505,7 +9506,7 @@ export class WebPlayer extends BasePlayer {
9505
9506
  qualityBadge.textContent = quality + 'p';
9506
9507
  }
9507
9508
  }
9508
-
9509
+
9509
9510
  // If we have actual quality levels from HLS/DASH, apply them
9510
9511
  if (quality !== 'auto' && this.qualities.length > 0) {
9511
9512
  const qualityLevel = this.qualities.find(q => q.label === quality + 'p');
@@ -9517,7 +9518,7 @@ export class WebPlayer extends BasePlayer {
9517
9518
  this.setAutoQuality(true);
9518
9519
  }
9519
9520
  }
9520
-
9521
+
9521
9522
  private async togglePiP(): Promise<void> {
9522
9523
  try {
9523
9524
  if ((document as any).pictureInPictureElement) {
@@ -9529,14 +9530,14 @@ export class WebPlayer extends BasePlayer {
9529
9530
  console.error('PiP toggle failed:', error);
9530
9531
  }
9531
9532
  }
9532
-
9533
+
9533
9534
  private setupCastContextSafe(): void {
9534
9535
  try {
9535
9536
  const castNs = (window as any).cast;
9536
9537
  if (castNs && castNs.framework) {
9537
9538
  this.setupCastContext();
9538
9539
  }
9539
- } catch (_) {}
9540
+ } catch (_) { }
9540
9541
  }
9541
9542
 
9542
9543
  private setupCastContext(): void {
@@ -9549,14 +9550,14 @@ export class WebPlayer extends BasePlayer {
9549
9550
  try {
9550
9551
  const autoJoin = chromeNs?.cast?.AutoJoinPolicy?.ORIGIN_SCOPED;
9551
9552
  if (autoJoin) options.autoJoinPolicy = autoJoin;
9552
- } catch (_) {}
9553
+ } catch (_) { }
9553
9554
  this.castContext.setOptions(options);
9554
9555
  this.castContext.addEventListener(
9555
9556
  castNs.framework.CastContextEventType.SESSION_STATE_CHANGED,
9556
9557
  (ev: any) => {
9557
9558
  const state = ev.sessionState;
9558
9559
  if (state === castNs.framework.SessionState.SESSION_STARTED ||
9559
- state === castNs.framework.SessionState.SESSION_RESUMED) {
9560
+ state === castNs.framework.SessionState.SESSION_RESUMED) {
9560
9561
  this.enableCastRemoteControl();
9561
9562
  } else if (state === castNs.framework.SessionState.SESSION_ENDED) {
9562
9563
  this.disableCastRemoteControl();
@@ -9580,7 +9581,7 @@ export class WebPlayer extends BasePlayer {
9580
9581
  this._bindRemotePlayerEvents();
9581
9582
  }
9582
9583
  this.isCasting = true;
9583
- try { this.video?.pause(); } catch (_) {}
9584
+ try { this.video?.pause(); } catch (_) { }
9584
9585
  this._syncUIFromRemote();
9585
9586
  this._syncCastButtons();
9586
9587
  } catch (err) {
@@ -9703,7 +9704,7 @@ export class WebPlayer extends BasePlayer {
9703
9704
  const sess = castNs?.framework?.CastContext?.getInstance()?.getCurrentSession?.();
9704
9705
  const dev = sess && sess.getCastDevice ? sess.getCastDevice() : null;
9705
9706
  if (dev && dev.friendlyName) title += ` (${dev.friendlyName})`;
9706
- } catch (_) {}
9707
+ } catch (_) { }
9707
9708
  castBtn.setAttribute('title', title);
9708
9709
  castBtn.setAttribute('aria-label', title);
9709
9710
  } else {
@@ -9742,7 +9743,7 @@ export class WebPlayer extends BasePlayer {
9742
9743
  // Generate accordion-style menu
9743
9744
  this.generateAccordionMenu();
9744
9745
  }
9745
-
9746
+
9746
9747
  /**
9747
9748
  * Generate accordion-style settings menu
9748
9749
  */
@@ -9786,7 +9787,7 @@ export class WebPlayer extends BasePlayer {
9786
9787
  if (this.settingsConfig.quality && this.availableQualities.length > 0) {
9787
9788
  const currentQuality = this.availableQualities.find(q => q.value === this.currentQuality);
9788
9789
  const currentQualityLabel = currentQuality ? currentQuality.label : 'Auto';
9789
-
9790
+
9790
9791
  menuHTML += `
9791
9792
  <div class="uvf-accordion-item">
9792
9793
  <div class="uvf-accordion-header" data-section="quality">
@@ -9802,13 +9803,13 @@ export class WebPlayer extends BasePlayer {
9802
9803
  <div class="uvf-accordion-arrow">▼</div>
9803
9804
  </div>
9804
9805
  <div class="uvf-accordion-content" data-section="quality">`;
9805
-
9806
+
9806
9807
  this.availableQualities.forEach(quality => {
9807
9808
  const isActive = quality.value === this.currentQuality ? 'active' : '';
9808
9809
  const isPremium = this.isQualityPremium(quality);
9809
9810
  const isLocked = isPremium && !this.isPremiumUser();
9810
9811
  const qualityHeight = (quality as any).height || 0;
9811
-
9812
+
9812
9813
  if (isLocked) {
9813
9814
  const premiumLabel = this.premiumQualities?.premiumLabel || '🔒 Premium';
9814
9815
  menuHTML += `<div class="uvf-settings-option quality-option premium-locked ${isActive}" data-quality="${quality.value}" data-quality-height="${qualityHeight}" data-quality-label="${quality.label}">${quality.label} <span style="margin-left: 4px; opacity: 0.7; font-size: 11px;">${premiumLabel}</span></div>`;
@@ -9816,7 +9817,7 @@ export class WebPlayer extends BasePlayer {
9816
9817
  menuHTML += `<div class="uvf-settings-option quality-option ${isActive}" data-quality="${quality.value}">${quality.label}</div>`;
9817
9818
  }
9818
9819
  });
9819
-
9820
+
9820
9821
  menuHTML += `</div></div>`;
9821
9822
  }
9822
9823
 
@@ -9824,7 +9825,7 @@ export class WebPlayer extends BasePlayer {
9824
9825
  if (this.settingsConfig.subtitles && this.availableSubtitles.length > 0) {
9825
9826
  const currentSubtitle = this.availableSubtitles.find(s => s.value === this.currentSubtitle);
9826
9827
  const currentSubtitleLabel = currentSubtitle ? currentSubtitle.label : 'Off';
9827
-
9828
+
9828
9829
  menuHTML += `
9829
9830
  <div class="uvf-accordion-item">
9830
9831
  <div class="uvf-accordion-header" data-section="subtitles">
@@ -9840,18 +9841,18 @@ export class WebPlayer extends BasePlayer {
9840
9841
  <div class="uvf-accordion-arrow">▼</div>
9841
9842
  </div>
9842
9843
  <div class="uvf-accordion-content" data-section="subtitles">`;
9843
-
9844
+
9844
9845
  this.availableSubtitles.forEach(subtitle => {
9845
9846
  const isActive = subtitle.value === this.currentSubtitle ? 'active' : '';
9846
9847
  menuHTML += `<div class="uvf-settings-option subtitle-option ${isActive}" data-subtitle="${subtitle.value}">${subtitle.label}</div>`;
9847
9848
  });
9848
-
9849
+
9849
9850
  menuHTML += `</div></div>`;
9850
9851
  }
9851
-
9852
+
9852
9853
  // Close accordion container
9853
9854
  menuHTML += '</div>';
9854
-
9855
+
9855
9856
  // If no sections are enabled or available, show a message
9856
9857
  if (menuHTML === '<div class="uvf-settings-accordion"></div>') {
9857
9858
  menuHTML = '<div class="uvf-settings-accordion"><div class="uvf-settings-empty">No settings available</div></div>';
@@ -9859,7 +9860,7 @@ export class WebPlayer extends BasePlayer {
9859
9860
 
9860
9861
  this.debugLog('Generated menu HTML length:', menuHTML.length);
9861
9862
  this.debugLog('Generated menu HTML content:', menuHTML.substring(0, 200) + (menuHTML.length > 200 ? '...' : ''));
9862
-
9863
+
9863
9864
  settingsMenu.innerHTML = menuHTML;
9864
9865
  this.debugLog('Settings menu HTML set successfully');
9865
9866
 
@@ -9906,7 +9907,7 @@ export class WebPlayer extends BasePlayer {
9906
9907
  // Native video - add common resolutions based on current resolution
9907
9908
  const height = this.video.videoHeight;
9908
9909
  const commonQualities = [2160, 1440, 1080, 720, 480, 360];
9909
-
9910
+
9910
9911
  commonQualities.forEach(quality => {
9911
9912
  if (quality <= height) {
9912
9913
  detectedQualities.push({
@@ -9917,46 +9918,46 @@ export class WebPlayer extends BasePlayer {
9917
9918
  }
9918
9919
  });
9919
9920
  }
9920
-
9921
+
9921
9922
  // Apply quality filter if configured
9922
9923
  if (this.qualityFilter) {
9923
9924
  detectedQualities = this.applyQualityFilter(detectedQualities);
9924
9925
  }
9925
-
9926
+
9926
9927
  // Add filtered qualities to available list
9927
9928
  this.availableQualities.push(...detectedQualities);
9928
9929
  }
9929
-
9930
+
9930
9931
  /**
9931
9932
  * Apply quality filter to detected qualities
9932
9933
  */
9933
9934
  private applyQualityFilter(qualities: Array<{ value: string; label: string; height?: number }>): Array<{ value: string; label: string; height?: number }> {
9934
9935
  let filtered = [...qualities];
9935
9936
  const filter = this.qualityFilter;
9936
-
9937
+
9937
9938
  // Filter by allowed heights
9938
9939
  if (filter.allowedHeights && filter.allowedHeights.length > 0) {
9939
9940
  filtered = filtered.filter(q => q.height && filter.allowedHeights.includes(q.height));
9940
9941
  }
9941
-
9942
+
9942
9943
  // Filter by allowed labels
9943
9944
  if (filter.allowedLabels && filter.allowedLabels.length > 0) {
9944
9945
  filtered = filtered.filter(q => filter.allowedLabels.includes(q.label));
9945
9946
  }
9946
-
9947
+
9947
9948
  // Filter by minimum height
9948
9949
  if (filter.minHeight !== undefined) {
9949
9950
  filtered = filtered.filter(q => q.height && q.height >= filter.minHeight);
9950
9951
  }
9951
-
9952
+
9952
9953
  // Filter by maximum height
9953
9954
  if (filter.maxHeight !== undefined) {
9954
9955
  filtered = filtered.filter(q => q.height && q.height <= filter.maxHeight);
9955
9956
  }
9956
-
9957
+
9957
9958
  return filtered;
9958
9959
  }
9959
-
9960
+
9960
9961
  /**
9961
9962
  * Set quality filter (can be called at runtime)
9962
9963
  */
@@ -9967,7 +9968,7 @@ export class WebPlayer extends BasePlayer {
9967
9968
  this.updateSettingsMenu();
9968
9969
  }
9969
9970
  }
9970
-
9971
+
9971
9972
  /**
9972
9973
  * Set ad playing state (called by Google Ads Manager)
9973
9974
  */
@@ -9975,14 +9976,14 @@ export class WebPlayer extends BasePlayer {
9975
9976
  this.isAdPlaying = isPlaying;
9976
9977
  this.debugLog('Ad playing state:', isPlaying);
9977
9978
  }
9978
-
9979
+
9979
9980
  /**
9980
9981
  * Check if ad is currently playing
9981
9982
  */
9982
9983
  public isAdCurrentlyPlaying(): boolean {
9983
9984
  return this.isAdPlaying;
9984
9985
  }
9985
-
9986
+
9986
9987
  /**
9987
9988
  * Check if a quality level is premium
9988
9989
  */
@@ -9990,61 +9991,61 @@ export class WebPlayer extends BasePlayer {
9990
9991
  if (!this.premiumQualities || !this.premiumQualities.enabled) {
9991
9992
  return false;
9992
9993
  }
9993
-
9994
+
9994
9995
  const config = this.premiumQualities;
9995
9996
  const height = quality.height || 0;
9996
9997
  const label = quality.label || '';
9997
-
9998
+
9998
9999
  // Check by specific heights
9999
10000
  if (config.requiredHeights && config.requiredHeights.length > 0) {
10000
10001
  if (config.requiredHeights.includes(height)) return true;
10001
10002
  }
10002
-
10003
+
10003
10004
  // Check by specific labels
10004
10005
  if (config.requiredLabels && config.requiredLabels.length > 0) {
10005
10006
  if (config.requiredLabels.includes(label)) return true;
10006
10007
  }
10007
-
10008
+
10008
10009
  // Check by minimum height threshold
10009
10010
  if (config.minPremiumHeight !== undefined) {
10010
10011
  if (height >= config.minPremiumHeight) return true;
10011
10012
  }
10012
-
10013
+
10013
10014
  return false;
10014
10015
  }
10015
-
10016
+
10016
10017
  /**
10017
10018
  * Check if current user is premium
10018
10019
  */
10019
10020
  private isPremiumUser(): boolean {
10020
10021
  return this.premiumQualities?.isPremiumUser === true;
10021
10022
  }
10022
-
10023
+
10023
10024
  /**
10024
10025
  * Handle click on locked premium quality
10025
10026
  */
10026
10027
  private handlePremiumQualityClick(element: HTMLElement): void {
10027
10028
  const height = parseInt(element.dataset.qualityHeight || '0');
10028
10029
  const label = element.dataset.qualityLabel || '';
10029
-
10030
+
10030
10031
  this.debugLog(`Premium quality clicked: ${label} (${height}p)`);
10031
-
10032
+
10032
10033
  // Call custom callback if provided
10033
10034
  if (this.premiumQualities?.onPremiumQualityClick) {
10034
10035
  this.premiumQualities.onPremiumQualityClick({ height, label });
10035
10036
  }
10036
-
10037
+
10037
10038
  // Show notification
10038
10039
  const message = this.premiumQualities?.premiumLabel || 'Premium';
10039
10040
  this.showNotification(`${label} requires ${message.replace('🔒 ', '')}`);
10040
-
10041
+
10041
10042
  // Redirect to unlock URL if provided
10042
10043
  if (this.premiumQualities?.unlockUrl) {
10043
10044
  setTimeout(() => {
10044
10045
  window.location.href = this.premiumQualities.unlockUrl;
10045
10046
  }, 1500);
10046
10047
  }
10047
-
10048
+
10048
10049
  // Close settings menu
10049
10050
  this.hideSettingsMenu();
10050
10051
  }
@@ -10101,10 +10102,10 @@ export class WebPlayer extends BasePlayer {
10101
10102
  header.addEventListener('click', (e) => {
10102
10103
  e.preventDefault();
10103
10104
  e.stopPropagation();
10104
-
10105
+
10105
10106
  const accordionItem = header.parentElement;
10106
10107
  const section = header.getAttribute('data-section');
10107
-
10108
+
10108
10109
  if (accordionItem && section) {
10109
10110
  this.toggleAccordionSection(accordionItem, section);
10110
10111
  }
@@ -10127,13 +10128,13 @@ export class WebPlayer extends BasePlayer {
10127
10128
  e.stopPropagation();
10128
10129
  const target = e.target as HTMLElement;
10129
10130
  const quality = target.dataset.quality || 'auto';
10130
-
10131
+
10131
10132
  // Check if this is a locked premium quality
10132
10133
  if (target.classList.contains('premium-locked')) {
10133
10134
  this.handlePremiumQualityClick(target);
10134
10135
  return;
10135
10136
  }
10136
-
10137
+
10137
10138
  this.setQualityFromSettings(quality);
10138
10139
  this.updateAccordionAfterSelection('quality');
10139
10140
  });
@@ -10149,19 +10150,19 @@ export class WebPlayer extends BasePlayer {
10149
10150
  });
10150
10151
  });
10151
10152
  }
10152
-
10153
+
10153
10154
  /**
10154
10155
  * Toggle accordion section
10155
10156
  */
10156
10157
  private toggleAccordionSection(accordionItem: Element, section: string): void {
10157
10158
  const isExpanded = accordionItem.classList.contains('expanded');
10158
-
10159
+
10159
10160
  // If clicking the same section that's already expanded, just close it
10160
10161
  if (isExpanded) {
10161
10162
  accordionItem.classList.remove('expanded');
10162
10163
  return;
10163
10164
  }
10164
-
10165
+
10165
10166
  // Otherwise, close all sections and open the clicked one
10166
10167
  const settingsMenu = document.getElementById('uvf-settings-menu');
10167
10168
  if (settingsMenu) {
@@ -10169,30 +10170,30 @@ export class WebPlayer extends BasePlayer {
10169
10170
  item.classList.remove('expanded');
10170
10171
  });
10171
10172
  }
10172
-
10173
+
10173
10174
  // Open the clicked section
10174
10175
  accordionItem.classList.add('expanded');
10175
10176
  }
10176
-
10177
+
10177
10178
  /**
10178
10179
  * Hide settings menu with proper styling
10179
10180
  */
10180
10181
  private hideSettingsMenu(): void {
10181
10182
  const settingsMenu = document.getElementById('uvf-settings-menu');
10182
10183
  if (!settingsMenu) return;
10183
-
10184
+
10184
10185
  settingsMenu.classList.remove('active');
10185
-
10186
+
10186
10187
  // Apply fallback styles to ensure menu is hidden
10187
10188
  settingsMenu.style.display = 'none';
10188
10189
  settingsMenu.style.visibility = 'hidden';
10189
10190
  settingsMenu.style.opacity = '0';
10190
-
10191
+
10191
10192
  // Also close any expanded accordions
10192
10193
  settingsMenu.querySelectorAll('.uvf-accordion-item.expanded').forEach(item => {
10193
10194
  item.classList.remove('expanded');
10194
10195
  });
10195
-
10196
+
10196
10197
  this.debugLog('Settings menu hidden via hideSettingsMenu()');
10197
10198
  }
10198
10199
 
@@ -10207,7 +10208,7 @@ export class WebPlayer extends BasePlayer {
10207
10208
  item.classList.remove('expanded');
10208
10209
  });
10209
10210
  }
10210
-
10211
+
10211
10212
  // Close settings menu after selection on all devices
10212
10213
  setTimeout(() => {
10213
10214
  this.hideSettingsMenu();
@@ -10246,7 +10247,7 @@ export class WebPlayer extends BasePlayer {
10246
10247
  */
10247
10248
  private setQualityFromSettings(quality: string): void {
10248
10249
  this.currentQuality = quality;
10249
-
10250
+
10250
10251
  if (quality === 'auto') {
10251
10252
  // Enable auto quality with filter consideration
10252
10253
  if (this.hls) {
@@ -10267,7 +10268,7 @@ export class WebPlayer extends BasePlayer {
10267
10268
  } else {
10268
10269
  // Set specific quality
10269
10270
  const qualityIndex = parseInt(quality);
10270
-
10271
+
10271
10272
  if (this.hls && !isNaN(qualityIndex) && this.hls.levels[qualityIndex]) {
10272
10273
  this.hls.currentLevel = qualityIndex;
10273
10274
  } else if (this.dash && !isNaN(qualityIndex)) {
@@ -10275,38 +10276,38 @@ export class WebPlayer extends BasePlayer {
10275
10276
  this.dash.setQualityFor('video', qualityIndex);
10276
10277
  }
10277
10278
  }
10278
-
10279
+
10279
10280
  this.debugLog(`Quality set to ${quality}`);
10280
10281
  }
10281
-
10282
+
10282
10283
  /**
10283
10284
  * Apply quality filter to HLS auto quality selection
10284
10285
  */
10285
10286
  private applyHLSQualityFilter(): void {
10286
10287
  if (!this.hls || !this.hls.levels) return;
10287
-
10288
+
10288
10289
  const allowedLevels: number[] = [];
10289
-
10290
+
10290
10291
  this.hls.levels.forEach((level: any, index: number) => {
10291
10292
  let allowed = true;
10292
-
10293
+
10293
10294
  // Apply quality filter if exists
10294
10295
  if (this.qualityFilter) {
10295
10296
  const filter = this.qualityFilter;
10296
-
10297
+
10297
10298
  if (filter.allowedHeights && filter.allowedHeights.length > 0) {
10298
10299
  allowed = allowed && filter.allowedHeights.includes(level.height);
10299
10300
  }
10300
-
10301
+
10301
10302
  if (filter.minHeight !== undefined) {
10302
10303
  allowed = allowed && level.height >= filter.minHeight;
10303
10304
  }
10304
-
10305
+
10305
10306
  if (filter.maxHeight !== undefined) {
10306
10307
  allowed = allowed && level.height <= filter.maxHeight;
10307
10308
  }
10308
10309
  }
10309
-
10310
+
10310
10311
  // Apply premium quality restrictions for non-premium users
10311
10312
  if (this.premiumQualities && this.premiumQualities.enabled && !this.isPremiumUser()) {
10312
10313
  const isPremium = this.isQualityPremium({ height: level.height, label: `${level.height}p` });
@@ -10314,24 +10315,24 @@ export class WebPlayer extends BasePlayer {
10314
10315
  allowed = false; // Exclude premium qualities for non-premium users
10315
10316
  }
10316
10317
  }
10317
-
10318
+
10318
10319
  if (allowed) {
10319
10320
  allowedLevels.push(index);
10320
10321
  }
10321
10322
  });
10322
-
10323
+
10323
10324
  // Set max and min auto level based on filter and premium restrictions
10324
10325
  if (allowedLevels.length > 0) {
10325
10326
  // Set the maximum level cap
10326
10327
  this.hls.autoLevelCapping = Math.max(...allowedLevels);
10327
-
10328
+
10328
10329
  // Enable auto quality
10329
10330
  this.hls.currentLevel = -1;
10330
-
10331
+
10331
10332
  // For minimum level, we need to use a different approach
10332
10333
  // Monitor level changes and prevent switching below minimum
10333
10334
  const minLevel = Math.min(...allowedLevels);
10334
-
10335
+
10335
10336
  // Set up level switching listener to enforce minimum
10336
10337
  const enforceLevelConstraints = () => {
10337
10338
  if (this.hls && this.hls.currentLevel !== -1 && this.hls.currentLevel < minLevel) {
@@ -10339,52 +10340,52 @@ export class WebPlayer extends BasePlayer {
10339
10340
  this.hls.currentLevel = minLevel;
10340
10341
  }
10341
10342
  };
10342
-
10343
+
10343
10344
  // Remove old listener if exists
10344
10345
  if (this.hls.listeners('hlsLevelSwitching').length > 0) {
10345
10346
  this.hls.off('hlsLevelSwitching', enforceLevelConstraints);
10346
10347
  }
10347
-
10348
+
10348
10349
  // Add listener to enforce constraints
10349
10350
  this.hls.on('hlsLevelSwitching', enforceLevelConstraints);
10350
-
10351
+
10351
10352
  this.debugLog(`Auto quality limited to levels: ${allowedLevels.join(', ')} (min: ${minLevel}, max: ${Math.max(...allowedLevels)})`);
10352
10353
  } else {
10353
10354
  // No allowed levels - just use auto
10354
10355
  this.hls.currentLevel = -1;
10355
10356
  }
10356
10357
  }
10357
-
10358
+
10358
10359
  /**
10359
10360
  * Apply quality filter to DASH auto quality selection
10360
10361
  */
10361
10362
  private applyDASHQualityFilter(): void {
10362
10363
  if (!this.dash) return;
10363
-
10364
+
10364
10365
  try {
10365
10366
  const videoQualities = this.dash.getBitrateInfoListFor('video');
10366
10367
  const allowedIndices: number[] = [];
10367
-
10368
+
10368
10369
  videoQualities.forEach((quality: any, index: number) => {
10369
10370
  let allowed = true;
10370
-
10371
+
10371
10372
  // Apply quality filter if exists
10372
10373
  if (this.qualityFilter) {
10373
10374
  const filter = this.qualityFilter;
10374
-
10375
+
10375
10376
  if (filter.allowedHeights && filter.allowedHeights.length > 0) {
10376
10377
  allowed = allowed && filter.allowedHeights.includes(quality.height);
10377
10378
  }
10378
-
10379
+
10379
10380
  if (filter.minHeight !== undefined) {
10380
10381
  allowed = allowed && quality.height >= filter.minHeight;
10381
10382
  }
10382
-
10383
+
10383
10384
  if (filter.maxHeight !== undefined) {
10384
10385
  allowed = allowed && quality.height <= filter.maxHeight;
10385
10386
  }
10386
10387
  }
10387
-
10388
+
10388
10389
  // Apply premium quality restrictions for non-premium users
10389
10390
  if (this.premiumQualities && this.premiumQualities.enabled && !this.isPremiumUser()) {
10390
10391
  const isPremium = this.isQualityPremium({ height: quality.height, label: `${quality.height}p` });
@@ -10392,12 +10393,12 @@ export class WebPlayer extends BasePlayer {
10392
10393
  allowed = false; // Exclude premium qualities for non-premium users
10393
10394
  }
10394
10395
  }
10395
-
10396
+
10396
10397
  if (allowed) {
10397
10398
  allowedIndices.push(index);
10398
10399
  }
10399
10400
  });
10400
-
10401
+
10401
10402
  // Set quality bounds for DASH
10402
10403
  if (allowedIndices.length > 0) {
10403
10404
  const settings = {
@@ -10422,7 +10423,7 @@ export class WebPlayer extends BasePlayer {
10422
10423
  */
10423
10424
  private setSubtitle(subtitle: string): void {
10424
10425
  this.currentSubtitle = subtitle;
10425
-
10426
+
10426
10427
  if (this.youtubePlayer && this.youtubePlayerReady) {
10427
10428
  // YouTube subtitles - limited control
10428
10429
  if (subtitle === 'off') {
@@ -10441,7 +10442,7 @@ export class WebPlayer extends BasePlayer {
10441
10442
  this.showShortcutIndicator('Subtitle language selection not available for YouTube');
10442
10443
  return;
10443
10444
  }
10444
-
10445
+
10445
10446
  if (subtitle === 'off') {
10446
10447
  // Disable all subtitles
10447
10448
  if (this.video?.textTracks) {
@@ -10467,7 +10468,7 @@ export class WebPlayer extends BasePlayer {
10467
10468
  });
10468
10469
  }
10469
10470
  }
10470
-
10471
+
10471
10472
  this.debugLog(`Subtitle set to ${subtitle}`);
10472
10473
  }
10473
10474
 
@@ -10485,11 +10486,11 @@ export class WebPlayer extends BasePlayer {
10485
10486
  if (tid) ids = [tid];
10486
10487
  }
10487
10488
  if (typeof media.setActiveTracks === 'function') {
10488
- media.setActiveTracks(ids, () => {}, () => {});
10489
+ media.setActiveTracks(ids, () => { }, () => { });
10489
10490
  } else if (typeof media.setActiveTrackIds === 'function') {
10490
10491
  media.setActiveTrackIds(ids);
10491
10492
  }
10492
- } catch (_) {}
10493
+ } catch (_) { }
10493
10494
  }
10494
10495
 
10495
10496
  private onCastButtonClick(): void {
@@ -10498,20 +10499,20 @@ export class WebPlayer extends BasePlayer {
10498
10499
  this.showAirPlayPicker();
10499
10500
  return;
10500
10501
  }
10501
-
10502
+
10502
10503
  // Google Cast for non-iOS devices
10503
10504
  try {
10504
10505
  const castNs = (window as any).cast;
10505
10506
  if (this.isCasting && castNs && castNs.framework) {
10506
10507
  const ctx = castNs.framework.CastContext.getInstance();
10507
- ctx.requestSession().catch(() => {});
10508
+ ctx.requestSession().catch(() => { });
10508
10509
  return;
10509
10510
  }
10510
- } catch (_) {}
10511
+ } catch (_) { }
10511
10512
  // Not casting yet
10512
10513
  this.initCast();
10513
10514
  }
10514
-
10515
+
10515
10516
  /**
10516
10517
  * Show AirPlay picker for iOS devices
10517
10518
  */
@@ -10520,7 +10521,7 @@ export class WebPlayer extends BasePlayer {
10520
10521
  this.showNotification('Video not ready');
10521
10522
  return;
10522
10523
  }
10523
-
10524
+
10524
10525
  // Check if AirPlay is supported
10525
10526
  const videoElement = this.video as any;
10526
10527
  if (typeof videoElement.webkitShowPlaybackTargetPicker === 'function') {
@@ -10544,7 +10545,7 @@ export class WebPlayer extends BasePlayer {
10544
10545
  const ctx = castNs.framework.CastContext.getInstance();
10545
10546
  const sess = ctx.getCurrentSession && ctx.getCurrentSession();
10546
10547
  if (sess) {
10547
- try { sess.endSession(true); } catch (_) {}
10548
+ try { sess.endSession(true); } catch (_) { }
10548
10549
  this.disableCastRemoteControl();
10549
10550
  this.showNotification('Stopped casting');
10550
10551
  } else {
@@ -10583,9 +10584,9 @@ export class WebPlayer extends BasePlayer {
10583
10584
  const url = this.source?.url || this.video?.src || '';
10584
10585
  const u = (url || '').toLowerCase();
10585
10586
  const contentType = u.includes('.m3u8') ? 'application/x-mpegurl'
10586
- : u.includes('.mpd') ? 'application/dash+xml'
10587
- : u.includes('.webm') ? 'video/webm'
10588
- : 'video/mp4';
10587
+ : u.includes('.mpd') ? 'application/dash+xml'
10588
+ : u.includes('.webm') ? 'video/webm'
10589
+ : 'video/mp4';
10589
10590
 
10590
10591
  const chromeNs = (window as any).chrome;
10591
10592
  const mediaInfo = new chromeNs.cast.media.MediaInfo(url, contentType);
@@ -10594,7 +10595,7 @@ export class WebPlayer extends BasePlayer {
10594
10595
  const md = new chromeNs.cast.media.GenericMediaMetadata();
10595
10596
  md.title = this.source?.metadata?.title || (this.video?.currentSrc ? this.video!.currentSrc.split('/').slice(-1)[0] : 'Web Player');
10596
10597
  mediaInfo.metadata = md;
10597
- } catch (_) {}
10598
+ } catch (_) { }
10598
10599
 
10599
10600
  // Subtitle tracks -> Cast tracks mapping
10600
10601
  const castTracks: any[] = [];
@@ -10610,7 +10611,7 @@ export class WebPlayer extends BasePlayer {
10610
10611
  let nextId = 1;
10611
10612
  for (let i = 0; i < this.subtitles.length; i++) {
10612
10613
  const t = this.subtitles[i];
10613
- const key = t.label || t.language || `Track ${i+1}`;
10614
+ const key = t.label || t.language || `Track ${i + 1}`;
10614
10615
  try {
10615
10616
  const track = new chromeNs.cast.media.Track(nextId, chromeNs.cast.media.TrackType.TEXT);
10616
10617
  track.trackContentId = t.url;
@@ -10622,7 +10623,7 @@ export class WebPlayer extends BasePlayer {
10622
10623
  castTracks.push(track);
10623
10624
  this._castTrackIdByKey[key] = nextId;
10624
10625
  nextId++;
10625
- } catch (_) {}
10626
+ } catch (_) { }
10626
10627
  }
10627
10628
  }
10628
10629
  if (castTracks.length > 0) {
@@ -10635,12 +10636,12 @@ export class WebPlayer extends BasePlayer {
10635
10636
  style.edgeColor = '#000000FF';
10636
10637
  style.fontScale = 1.0;
10637
10638
  mediaInfo.textTrackStyle = style;
10638
- } catch (_) {}
10639
+ } catch (_) { }
10639
10640
  }
10640
10641
 
10641
10642
  const request = new chromeNs.cast.media.LoadRequest(mediaInfo);
10642
10643
  request.autoplay = true;
10643
- try { request.currentTime = Math.max(0, Math.floor(this.video?.currentTime || 0)); } catch (_) {}
10644
+ try { request.currentTime = Math.max(0, Math.floor(this.video?.currentTime || 0)); } catch (_) { }
10644
10645
  // Determine selected subtitle key from currentSubtitleIndex
10645
10646
  const currentIdx = this.currentSubtitleIndex;
10646
10647
  this.selectedSubtitleKey = (currentIdx >= 0 && this.subtitles[currentIdx]) ? (this.subtitles[currentIdx].label || this.subtitles[currentIdx].language) : 'off';
@@ -10657,7 +10658,7 @@ export class WebPlayer extends BasePlayer {
10657
10658
  this.showNotification('Cast failed');
10658
10659
  }
10659
10660
  }
10660
-
10661
+
10661
10662
  private async shareVideo(): Promise<void> {
10662
10663
  // Get share configuration
10663
10664
  const shareConfig = this.config.share;
@@ -10705,7 +10706,7 @@ export class WebPlayer extends BasePlayer {
10705
10706
  this.showNotification('Share failed');
10706
10707
  }
10707
10708
  }
10708
-
10709
+
10709
10710
  /**
10710
10711
  * Check if text is truncated and needs tooltip
10711
10712
  */
@@ -10727,9 +10728,9 @@ export class WebPlayer extends BasePlayer {
10727
10728
  const tooltip = document.createElement('div');
10728
10729
  tooltip.className = 'uvf-text-tooltip';
10729
10730
  tooltip.textContent = text;
10730
-
10731
+
10731
10732
  element.appendChild(tooltip);
10732
-
10733
+
10733
10734
  // Show tooltip with delay
10734
10735
  setTimeout(() => {
10735
10736
  tooltip.classList.add('show');
@@ -10765,11 +10766,11 @@ export class WebPlayer extends BasePlayer {
10765
10766
  this.showTextTooltip(titleElement, titleText);
10766
10767
  }
10767
10768
  });
10768
-
10769
+
10769
10770
  titleElement.addEventListener('mouseleave', () => {
10770
10771
  this.hideTextTooltip(titleElement);
10771
10772
  });
10772
-
10773
+
10773
10774
  // Touch support for mobile
10774
10775
  titleElement.addEventListener('touchstart', () => {
10775
10776
  const titleText = (this.source?.metadata?.title || '').toString().trim();
@@ -10790,11 +10791,11 @@ export class WebPlayer extends BasePlayer {
10790
10791
  this.showTextTooltip(descElement, descText);
10791
10792
  }
10792
10793
  });
10793
-
10794
+
10794
10795
  descElement.addEventListener('mouseleave', () => {
10795
10796
  this.hideTextTooltip(descElement);
10796
10797
  });
10797
-
10798
+
10798
10799
  // Touch support for mobile
10799
10800
  descElement.addEventListener('touchstart', () => {
10800
10801
  const descText = (this.source?.metadata?.description || '').toString().trim();
@@ -10817,7 +10818,7 @@ export class WebPlayer extends BasePlayer {
10817
10818
  if (words.length <= maxWords) {
10818
10819
  return { truncated: text, needsTooltip: false };
10819
10820
  }
10820
-
10821
+
10821
10822
  const truncated = words.slice(0, maxWords).join(' ') + '...';
10822
10823
  return { truncated, needsTooltip: true };
10823
10824
  }
@@ -10828,10 +10829,10 @@ export class WebPlayer extends BasePlayer {
10828
10829
  private applySmartTextDisplay(titleEl: HTMLElement | null, descEl: HTMLElement | null, titleText: string, descText: string): void {
10829
10830
  const isDesktop = window.innerWidth >= 1024;
10830
10831
  const isMobile = window.innerWidth < 768;
10831
-
10832
+
10832
10833
  if (titleEl && titleText) {
10833
10834
  const wordCount = titleText.split(' ').length;
10834
-
10835
+
10835
10836
  if (isDesktop && wordCount > 8 && wordCount <= 15) {
10836
10837
  // Use multiline for moderately long titles on desktop
10837
10838
  titleEl.classList.add('multiline');
@@ -10847,10 +10848,10 @@ export class WebPlayer extends BasePlayer {
10847
10848
  titleEl.classList.remove('multiline');
10848
10849
  }
10849
10850
  }
10850
-
10851
+
10851
10852
  if (descEl && descText) {
10852
10853
  const wordCount = descText.split(' ').length;
10853
-
10854
+
10854
10855
  if (isDesktop && wordCount > 15 && wordCount <= 25) {
10855
10856
  // Use multiline for moderately long descriptions on desktop
10856
10857
  descEl.classList.add('multiline');
@@ -10906,7 +10907,7 @@ export class WebPlayer extends BasePlayer {
10906
10907
  setTimeout(() => {
10907
10908
  this.setupTextTooltips();
10908
10909
  }, 100); // Small delay to ensure elements are rendered
10909
-
10910
+
10910
10911
  } catch (_) { /* ignore */ }
10911
10912
  }
10912
10913
 
@@ -10921,26 +10922,26 @@ export class WebPlayer extends BasePlayer {
10921
10922
  private canPlayVideo(): boolean {
10922
10923
  const freeDuration = Number(this.config.freeDuration || 0);
10923
10924
  const currentTime = this.video?.currentTime || 0;
10924
-
10925
+
10925
10926
  // Always allow if no free duration limit is set
10926
10927
  if (freeDuration <= 0) return true;
10927
-
10928
+
10928
10929
  // Allow if payment was successful
10929
10930
  if (this.paymentSuccessful) return true;
10930
-
10931
+
10931
10932
  // Allow if within free preview duration
10932
10933
  if (currentTime < freeDuration) return true;
10933
-
10934
+
10934
10935
  // Check if paywall controller indicates user is authenticated
10935
- if (this.paywallController &&
10936
- typeof this.paywallController.isAuthenticated === 'function') {
10936
+ if (this.paywallController &&
10937
+ typeof this.paywallController.isAuthenticated === 'function') {
10937
10938
  const isAuth = this.paywallController.isAuthenticated();
10938
10939
  if (isAuth) {
10939
10940
  this.paymentSuccessful = true;
10940
10941
  return true;
10941
10942
  }
10942
10943
  }
10943
-
10944
+
10944
10945
  return false;
10945
10946
  }
10946
10947
 
@@ -10949,17 +10950,17 @@ export class WebPlayer extends BasePlayer {
10949
10950
  */
10950
10951
  private enforcePaywallSecurity(): void {
10951
10952
  this.debugLog('Enforcing paywall security');
10952
-
10953
+
10953
10954
  // Pause video immediately
10954
10955
  try {
10955
10956
  if (this.video && !this.video.paused) {
10956
10957
  this.video.pause();
10957
10958
  }
10958
- } catch (_) {}
10959
-
10959
+ } catch (_) { }
10960
+
10960
10961
  // Activate paywall state
10961
10962
  this.isPaywallActive = true;
10962
-
10963
+
10963
10964
  // Show paywall overlay
10964
10965
  if (this.paywallController) {
10965
10966
  try {
@@ -10968,7 +10969,7 @@ export class WebPlayer extends BasePlayer {
10968
10969
  this.debugError('Error showing paywall overlay:', error);
10969
10970
  }
10970
10971
  }
10971
-
10972
+
10972
10973
  // Start monitoring for overlay tampering
10973
10974
  this.startOverlayMonitoring();
10974
10975
  }
@@ -10978,15 +10979,15 @@ export class WebPlayer extends BasePlayer {
10978
10979
  */
10979
10980
  private startOverlayMonitoring(): void {
10980
10981
  if (!this.playerWrapper || this.paymentSuccessful) return;
10981
-
10982
+
10982
10983
  // Clear existing monitor
10983
10984
  if (this.authValidationInterval) {
10984
10985
  clearInterval(this.authValidationInterval);
10985
10986
  this.authValidationInterval = null;
10986
10987
  }
10987
-
10988
+
10988
10989
  this.debugLog('Starting overlay monitoring - payment successful:', this.paymentSuccessful, 'paywall active:', this.isPaywallActive);
10989
-
10990
+
10990
10991
  // Monitor every 1000ms (less aggressive than before)
10991
10992
  this.authValidationInterval = setInterval(() => {
10992
10993
  // First check: stop monitoring if payment successful or paywall inactive
@@ -10998,7 +10999,7 @@ export class WebPlayer extends BasePlayer {
10998
10999
  }
10999
11000
  return;
11000
11001
  }
11001
-
11002
+
11002
11003
  // Double-check payment success before enforcing security
11003
11004
  if (this.paymentSuccessful) {
11004
11005
  this.debugLog('Payment successful detected during monitoring, stopping');
@@ -11008,20 +11009,20 @@ export class WebPlayer extends BasePlayer {
11008
11009
  }
11009
11010
  return;
11010
11011
  }
11011
-
11012
+
11012
11013
  // Check for overlay presence
11013
11014
  const paywallOverlays = this.playerWrapper!.querySelectorAll('.uvf-paywall-overlay, .uvf-auth-overlay');
11014
11015
  const visibleOverlays = Array.from(paywallOverlays).filter(overlay => {
11015
11016
  const element = overlay as HTMLElement;
11016
- return element.style.display !== 'none' &&
11017
- element.offsetParent !== null &&
11018
- window.getComputedStyle(element).visibility !== 'hidden';
11017
+ return element.style.display !== 'none' &&
11018
+ element.offsetParent !== null &&
11019
+ window.getComputedStyle(element).visibility !== 'hidden';
11019
11020
  });
11020
-
11021
+
11021
11022
  if (visibleOverlays.length === 0) {
11022
11023
  this.overlayRemovalAttempts++;
11023
11024
  this.debugWarn(`Overlay removal attempt detected (${this.overlayRemovalAttempts}/${this.maxOverlayRemovalAttempts})`);
11024
-
11025
+
11025
11026
  // Final check before taking action - ensure payment wasn't just completed
11026
11027
  if (this.paymentSuccessful) {
11027
11028
  this.debugLog('Payment successful detected, ignoring overlay removal');
@@ -11031,7 +11032,7 @@ export class WebPlayer extends BasePlayer {
11031
11032
  }
11032
11033
  return;
11033
11034
  }
11034
-
11035
+
11035
11036
  if (this.overlayRemovalAttempts >= this.maxOverlayRemovalAttempts) {
11036
11037
  this.handleSecurityViolation();
11037
11038
  } else {
@@ -11039,7 +11040,7 @@ export class WebPlayer extends BasePlayer {
11039
11040
  this.enforcePaywallSecurity();
11040
11041
  }
11041
11042
  }
11042
-
11043
+
11043
11044
  // Additional check: ensure video is paused if not authenticated
11044
11045
  if (this.video && !this.video.paused && !this.paymentSuccessful) {
11045
11046
  this.debugWarn('Unauthorized playback detected, pausing video');
@@ -11049,7 +11050,7 @@ export class WebPlayer extends BasePlayer {
11049
11050
  if (freeDuration > 0 && isFinite(freeDuration)) {
11050
11051
  this.safeSetCurrentTime(freeDuration - 1);
11051
11052
  }
11052
- } catch (_) {}
11053
+ } catch (_) { }
11053
11054
  }
11054
11055
  }, 1000);
11055
11056
  }
@@ -11059,7 +11060,7 @@ export class WebPlayer extends BasePlayer {
11059
11060
  */
11060
11061
  private handleSecurityViolation(): void {
11061
11062
  this.debugError('Security violation detected - disabling video');
11062
-
11063
+
11063
11064
  // Disable video completely
11064
11065
  if (this.video) {
11065
11066
  this.video.pause();
@@ -11067,10 +11068,10 @@ export class WebPlayer extends BasePlayer {
11067
11068
  this.video.src = ''; // Clear video source
11068
11069
  this.video.style.display = 'none';
11069
11070
  }
11070
-
11071
+
11071
11072
  // Show security violation message
11072
11073
  this.showSecurityViolationMessage();
11073
-
11074
+
11074
11075
  // Clear monitoring interval
11075
11076
  if (this.authValidationInterval) {
11076
11077
  clearInterval(this.authValidationInterval);
@@ -11082,10 +11083,10 @@ export class WebPlayer extends BasePlayer {
11082
11083
  */
11083
11084
  private showSecurityViolationMessage(): void {
11084
11085
  if (!this.playerWrapper) return;
11085
-
11086
+
11086
11087
  // Clear existing content
11087
11088
  this.playerWrapper.innerHTML = '';
11088
-
11089
+
11089
11090
  // Create security violation overlay
11090
11091
  const securityOverlay = document.createElement('div');
11091
11092
  securityOverlay.style.cssText = `
@@ -11101,7 +11102,7 @@ export class WebPlayer extends BasePlayer {
11101
11102
  text-align: center;
11102
11103
  padding: 40px;
11103
11104
  `;
11104
-
11105
+
11105
11106
  const messageContainer = document.createElement('div');
11106
11107
  messageContainer.innerHTML = `
11107
11108
  <div style="font-size: 24px; font-weight: bold; margin-bottom: 16px; color: #ff6b6b;">
@@ -11124,7 +11125,7 @@ export class WebPlayer extends BasePlayer {
11124
11125
  ">Reload Page</button>
11125
11126
  </div>
11126
11127
  `;
11127
-
11128
+
11128
11129
  securityOverlay.appendChild(messageContainer);
11129
11130
  this.playerWrapper.appendChild(securityOverlay);
11130
11131
  }
@@ -11134,25 +11135,25 @@ export class WebPlayer extends BasePlayer {
11134
11135
  */
11135
11136
  private forceCleanupOverlays(): void {
11136
11137
  this.debugLog('Force cleanup of overlays called');
11137
-
11138
+
11138
11139
  if (!this.playerWrapper) return;
11139
-
11140
+
11140
11141
  // Find and remove all overlay elements
11141
11142
  const overlays = this.playerWrapper.querySelectorAll('.uvf-paywall-overlay, .uvf-auth-overlay');
11142
11143
  overlays.forEach((overlay: Element) => {
11143
11144
  const htmlOverlay = overlay as HTMLElement;
11144
11145
  this.debugLog('Removing overlay:', htmlOverlay.className);
11145
-
11146
+
11146
11147
  // Hide immediately
11147
11148
  htmlOverlay.style.display = 'none';
11148
11149
  htmlOverlay.classList.remove('active');
11149
-
11150
+
11150
11151
  // Remove from DOM
11151
11152
  if (htmlOverlay.parentNode) {
11152
11153
  htmlOverlay.parentNode.removeChild(htmlOverlay);
11153
11154
  }
11154
11155
  });
11155
-
11156
+
11156
11157
  // Also tell paywall controller to clean up
11157
11158
  if (this.paywallController && typeof this.paywallController.destroyOverlays === 'function') {
11158
11159
  this.debugLog('Calling paywallController.destroyOverlays()');