unified-video-framework 1.4.370 → 1.4.372

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();
@@ -3633,6 +3633,12 @@ export class WebPlayer extends BasePlayer {
3633
3633
  to { transform: rotate(360deg); }
3634
3634
  }
3635
3635
 
3636
+ /* Hide center play button when loading is active */
3637
+ .uvf-loading-container.active ~ .uvf-center-play-container {
3638
+ opacity: 0;
3639
+ pointer-events: none;
3640
+ }
3641
+
3636
3642
  /* Center Play Button Container */
3637
3643
  .uvf-center-play-container {
3638
3644
  position: absolute;
@@ -6680,17 +6686,17 @@ export class WebPlayer extends BasePlayer {
6680
6686
  * Creates framework branding logo
6681
6687
  * Only creates branding if showFrameworkBranding is not explicitly set to false
6682
6688
  */
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 = `
6689
+ private createFrameworkBranding(container: HTMLElement): void {
6690
+ // Double-check configuration (defensive programming)
6691
+ if ((this.config as any).showFrameworkBranding === false) {
6692
+ this.debugLog('Framework branding disabled by configuration');
6693
+ return;
6694
+ }
6695
+ const brandingContainer = document.createElement('div');
6696
+ brandingContainer.className = 'uvf-framework-branding';
6697
+ brandingContainer.setAttribute('title', 'Powered by flicknexs');
6698
+
6699
+ const logoSvg = `
6694
6700
  <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
6701
  <defs>
6696
6702
  <style>.cls-1{fill:#0d5ef8;}.cls-2{fill:#ff0366;opacity:0.94;}.cls-3{fill:#fff;}</style>
@@ -6710,35 +6716,35 @@ export class WebPlayer extends BasePlayer {
6710
6716
  <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
6717
  </svg>
6712
6718
  `;
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
- }
6719
+
6720
+ brandingContainer.innerHTML = logoSvg;
6721
+
6722
+ // Add click handler to redirect to flicknexs.com
6723
+ brandingContainer.addEventListener('click', (event) => {
6724
+ event.preventDefault();
6725
+ event.stopPropagation();
6726
+
6727
+ // Open flicknexs.com in a new tab/window
6728
+ const link = document.createElement('a');
6729
+ link.href = 'https://flicknexs.com/';
6730
+ link.target = '_blank';
6731
+ link.rel = 'noopener noreferrer';
6732
+ document.body.appendChild(link);
6733
+ link.click();
6734
+ document.body.removeChild(link);
6735
+
6736
+ // Emit analytics event
6737
+ this.emit('frameworkBrandingClick', {
6738
+ timestamp: Date.now(),
6739
+ url: 'https://flicknexs.com/',
6740
+ userAgent: navigator.userAgent
6741
+ });
6742
+ });
6743
+
6744
+ container.appendChild(brandingContainer);
6745
+
6746
+ this.debugLog('Framework branding added');
6747
+ }
6742
6748
 
6743
6749
  /**
6744
6750
  * Create navigation buttons (back/close) based on configuration
@@ -6756,16 +6762,16 @@ export class WebPlayer extends BasePlayer {
6756
6762
  backBtn.id = 'uvf-back-btn';
6757
6763
  backBtn.title = backButton.title || 'Back';
6758
6764
  backBtn.setAttribute('aria-label', backButton.ariaLabel || 'Go back');
6759
-
6765
+
6760
6766
  // Get icon based on config
6761
6767
  const backIcon = this.getNavigationIcon(backButton.icon || 'arrow', backButton.customIcon);
6762
6768
  backBtn.innerHTML = backIcon;
6763
-
6769
+
6764
6770
  // Add click handler
6765
6771
  backBtn.addEventListener('click', async (e) => {
6766
6772
  e.preventDefault();
6767
6773
  e.stopPropagation();
6768
-
6774
+
6769
6775
  if (backButton.onClick) {
6770
6776
  await backButton.onClick();
6771
6777
  } else if (backButton.href) {
@@ -6778,10 +6784,10 @@ export class WebPlayer extends BasePlayer {
6778
6784
  // Default: go back in history
6779
6785
  window.history.back();
6780
6786
  }
6781
-
6787
+
6782
6788
  this.emit('navigationBackClicked');
6783
6789
  });
6784
-
6790
+
6785
6791
  container.appendChild(backBtn);
6786
6792
  }
6787
6793
 
@@ -6792,16 +6798,16 @@ export class WebPlayer extends BasePlayer {
6792
6798
  closeBtn.id = 'uvf-close-btn';
6793
6799
  closeBtn.title = closeButton.title || 'Close';
6794
6800
  closeBtn.setAttribute('aria-label', closeButton.ariaLabel || 'Close player');
6795
-
6801
+
6796
6802
  // Get icon based on config
6797
6803
  const closeIcon = this.getNavigationIcon(closeButton.icon || 'x', closeButton.customIcon);
6798
6804
  closeBtn.innerHTML = closeIcon;
6799
-
6805
+
6800
6806
  // Add click handler
6801
6807
  closeBtn.addEventListener('click', async (e) => {
6802
6808
  e.preventDefault();
6803
6809
  e.stopPropagation();
6804
-
6810
+
6805
6811
  if (closeButton.onClick) {
6806
6812
  await closeButton.onClick();
6807
6813
  } else {
@@ -6809,7 +6815,7 @@ export class WebPlayer extends BasePlayer {
6809
6815
  if (closeButton.exitFullscreen && this.isFullscreen()) {
6810
6816
  await this.exitFullscreen();
6811
6817
  }
6812
-
6818
+
6813
6819
  if (closeButton.closeModal) {
6814
6820
  // Hide player or remove from DOM
6815
6821
  const playerWrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
@@ -6818,10 +6824,10 @@ export class WebPlayer extends BasePlayer {
6818
6824
  }
6819
6825
  }
6820
6826
  }
6821
-
6827
+
6822
6828
  this.emit('navigationCloseClicked');
6823
6829
  });
6824
-
6830
+
6825
6831
  container.appendChild(closeBtn);
6826
6832
  }
6827
6833
  }
@@ -6843,22 +6849,22 @@ export class WebPlayer extends BasePlayer {
6843
6849
  return `<svg viewBox="0 0 24 24">
6844
6850
  <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
6845
6851
  </svg>`;
6846
-
6852
+
6847
6853
  case 'chevron':
6848
6854
  return `<svg viewBox="0 0 24 24">
6849
6855
  <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
6850
6856
  </svg>`;
6851
-
6857
+
6852
6858
  case 'x':
6853
6859
  return `<svg viewBox="0 0 24 24">
6854
6860
  <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
6861
  </svg>`;
6856
-
6862
+
6857
6863
  case 'close':
6858
6864
  return `<svg viewBox="0 0 24 24">
6859
6865
  <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
6866
  </svg>`;
6861
-
6867
+
6862
6868
  default:
6863
6869
  return `<svg viewBox="0 0 24 24">
6864
6870
  <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
@@ -6875,21 +6881,21 @@ export class WebPlayer extends BasePlayer {
6875
6881
  const controlsGradient = document.createElement('div');
6876
6882
  controlsGradient.className = 'uvf-controls-gradient';
6877
6883
  container.appendChild(controlsGradient);
6878
-
6884
+
6879
6885
  // Combined top bar: navigation buttons → title → controls
6880
6886
  const topBar = document.createElement('div');
6881
6887
  topBar.className = 'uvf-top-bar';
6882
-
6888
+
6883
6889
  // Left side container for navigation + title
6884
6890
  const leftSide = document.createElement('div');
6885
6891
  leftSide.className = 'uvf-left-side';
6886
-
6892
+
6887
6893
  // Navigation buttons (back/close)
6888
6894
  const navigationControls = document.createElement('div');
6889
6895
  navigationControls.className = 'uvf-navigation-controls';
6890
6896
  this.createNavigationButtons(navigationControls);
6891
6897
  leftSide.appendChild(navigationControls);
6892
-
6898
+
6893
6899
  // Title bar (after navigation buttons)
6894
6900
  const titleBar = document.createElement('div');
6895
6901
  titleBar.className = 'uvf-title-bar';
@@ -6902,9 +6908,9 @@ export class WebPlayer extends BasePlayer {
6902
6908
  </div>
6903
6909
  `;
6904
6910
  leftSide.appendChild(titleBar);
6905
-
6911
+
6906
6912
  topBar.appendChild(leftSide);
6907
-
6913
+
6908
6914
  // Top controls (right side - Cast and Share buttons)
6909
6915
  const topControls = document.createElement('div');
6910
6916
  topControls.className = 'uvf-top-controls';
@@ -6927,12 +6933,12 @@ export class WebPlayer extends BasePlayer {
6927
6933
  </button>
6928
6934
  `;
6929
6935
  topBar.appendChild(topControls);
6930
-
6936
+
6931
6937
  container.appendChild(topBar);
6932
6938
 
6933
- // Add loading spinner
6939
+ // Add loading spinner (show by default until video is ready)
6934
6940
  const loadingContainer = document.createElement('div');
6935
- loadingContainer.className = 'uvf-loading-container';
6941
+ loadingContainer.className = 'uvf-loading-container active'; // Start with active class
6936
6942
  loadingContainer.id = 'uvf-loading';
6937
6943
  loadingContainer.innerHTML = '<div class="uvf-loading-spinner"></div>';
6938
6944
  container.appendChild(loadingContainer);
@@ -6940,15 +6946,15 @@ export class WebPlayer extends BasePlayer {
6940
6946
  // Add center play button container for proper responsive centering
6941
6947
  const centerPlayContainer = document.createElement('div');
6942
6948
  centerPlayContainer.className = 'uvf-center-play-container';
6943
-
6949
+
6944
6950
  const centerPlayBtn = document.createElement('div');
6945
6951
  centerPlayBtn.className = 'uvf-center-play-btn uvf-pulse';
6946
6952
  centerPlayBtn.id = 'uvf-center-play';
6947
6953
  centerPlayBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
6948
-
6954
+
6949
6955
  centerPlayContainer.appendChild(centerPlayBtn);
6950
6956
  container.appendChild(centerPlayContainer);
6951
-
6957
+
6952
6958
  // Add shortcut indicator
6953
6959
  const shortcutIndicator = document.createElement('div');
6954
6960
  shortcutIndicator.className = 'uvf-shortcut-indicator';
@@ -6959,11 +6965,11 @@ export class WebPlayer extends BasePlayer {
6959
6965
  const controlsBar = document.createElement('div');
6960
6966
  controlsBar.className = 'uvf-controls-bar';
6961
6967
  controlsBar.id = 'uvf-controls';
6962
-
6968
+
6963
6969
  // Time and branding section above seekbar
6964
6970
  const aboveSeekbarSection = document.createElement('div');
6965
6971
  aboveSeekbarSection.className = 'uvf-above-seekbar-section';
6966
-
6972
+
6967
6973
  // Time display (moved to above seekbar)
6968
6974
  const timeDisplay = document.createElement('div');
6969
6975
  timeDisplay.className = 'uvf-time-display uvf-above-seekbar';
@@ -6975,11 +6981,11 @@ export class WebPlayer extends BasePlayer {
6975
6981
  if ((this.config as any).showFrameworkBranding !== false) {
6976
6982
  this.createFrameworkBranding(aboveSeekbarSection);
6977
6983
  }
6978
-
6984
+
6979
6985
  // Progress section
6980
6986
  const progressSection = document.createElement('div');
6981
6987
  progressSection.className = 'uvf-progress-section';
6982
-
6988
+
6983
6989
  const progressBar = document.createElement('div');
6984
6990
  progressBar.className = 'uvf-progress-bar-wrapper';
6985
6991
  progressBar.id = 'uvf-progress-bar';
@@ -6992,11 +6998,11 @@ export class WebPlayer extends BasePlayer {
6992
6998
  <div class="uvf-time-tooltip" id="uvf-time-tooltip">00:00</div>
6993
6999
  `;
6994
7000
  progressSection.appendChild(progressBar);
6995
-
7001
+
6996
7002
  // Controls row
6997
7003
  const controlsRow = document.createElement('div');
6998
7004
  controlsRow.className = 'uvf-controls-row';
6999
-
7005
+
7000
7006
  // Play/Pause button
7001
7007
  const playPauseBtn = document.createElement('button');
7002
7008
  playPauseBtn.className = 'uvf-control-btn play-pause';
@@ -7010,7 +7016,7 @@ export class WebPlayer extends BasePlayer {
7010
7016
  </svg>
7011
7017
  `;
7012
7018
  controlsRow.appendChild(playPauseBtn);
7013
-
7019
+
7014
7020
  // Skip buttons with consistent icons
7015
7021
  const skipBackBtn = document.createElement('button');
7016
7022
  skipBackBtn.className = 'uvf-control-btn';
@@ -7019,7 +7025,7 @@ export class WebPlayer extends BasePlayer {
7019
7025
  skipBackBtn.setAttribute('aria-label', 'Skip backward 10 seconds');
7020
7026
  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
7027
  controlsRow.appendChild(skipBackBtn);
7022
-
7028
+
7023
7029
  const skipForwardBtn = document.createElement('button');
7024
7030
  skipForwardBtn.className = 'uvf-control-btn';
7025
7031
  skipForwardBtn.id = 'uvf-skip-forward';
@@ -7027,7 +7033,7 @@ export class WebPlayer extends BasePlayer {
7027
7033
  skipForwardBtn.setAttribute('aria-label', 'Skip forward 10 seconds');
7028
7034
  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
7035
  controlsRow.appendChild(skipForwardBtn);
7030
-
7036
+
7031
7037
  // Volume control
7032
7038
  const volumeControl = document.createElement('div');
7033
7039
  volumeControl.className = 'uvf-volume-control';
@@ -7048,23 +7054,23 @@ export class WebPlayer extends BasePlayer {
7048
7054
  </div>
7049
7055
  `;
7050
7056
  controlsRow.appendChild(volumeControl);
7051
-
7057
+
7052
7058
  // Right controls
7053
7059
  const rightControls = document.createElement('div');
7054
7060
  rightControls.className = 'uvf-right-controls';
7055
-
7061
+
7056
7062
  // Quality badge
7057
7063
  const qualityBadge = document.createElement('div');
7058
7064
  qualityBadge.className = 'uvf-quality-badge';
7059
7065
  qualityBadge.id = 'uvf-quality-badge';
7060
7066
  qualityBadge.textContent = 'HD';
7061
7067
  rightControls.appendChild(qualityBadge);
7062
-
7068
+
7063
7069
  // Settings button with menu (show only if enabled)
7064
7070
  this.debugLog('Settings config check:', this.settingsConfig);
7065
7071
  this.debugLog('Settings enabled:', this.settingsConfig.enabled);
7066
7072
  this.debugLog('Custom controls enabled:', this.useCustomControls);
7067
-
7073
+
7068
7074
  if (this.settingsConfig.enabled) {
7069
7075
  this.debugLog('Creating settings button...');
7070
7076
  const settingsContainer = document.createElement('div');
@@ -7073,7 +7079,7 @@ export class WebPlayer extends BasePlayer {
7073
7079
  settingsContainer.style.display = 'flex';
7074
7080
  settingsContainer.style.alignItems = 'center';
7075
7081
  settingsContainer.style.justifyContent = 'center';
7076
-
7082
+
7077
7083
  const settingsBtn = document.createElement('button');
7078
7084
  settingsBtn.className = 'uvf-control-btn';
7079
7085
  settingsBtn.id = 'uvf-settings-btn';
@@ -7081,7 +7087,7 @@ export class WebPlayer extends BasePlayer {
7081
7087
  settingsBtn.setAttribute('aria-label', 'Settings');
7082
7088
  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
7089
  settingsContainer.appendChild(settingsBtn);
7084
-
7090
+
7085
7091
  // Settings menu - will be populated dynamically based on video capabilities and configuration
7086
7092
  const settingsMenu = document.createElement('div');
7087
7093
  settingsMenu.className = 'uvf-settings-menu';
@@ -7091,9 +7097,9 @@ export class WebPlayer extends BasePlayer {
7091
7097
  // CSS handles initial hidden state
7092
7098
  settingsContainer.appendChild(settingsMenu);
7093
7099
  rightControls.appendChild(settingsContainer);
7094
-
7100
+
7095
7101
  this.debugLog('Settings button created and added to controls');
7096
-
7102
+
7097
7103
  // Add debugging for settings button visibility
7098
7104
  setTimeout(() => {
7099
7105
  const createdBtn = document.getElementById('uvf-settings-btn');
@@ -7107,10 +7113,10 @@ export class WebPlayer extends BasePlayer {
7107
7113
  this.debugLog('- Height:', computedStyle.height);
7108
7114
  this.debugLog('- Position:', computedStyle.position);
7109
7115
  this.debugLog('- Z-index:', computedStyle.zIndex);
7110
-
7116
+
7111
7117
  const rect = createdBtn.getBoundingClientRect();
7112
7118
  this.debugLog('- Bounding rect:', { width: rect.width, height: rect.height, top: rect.top, left: rect.left });
7113
-
7119
+
7114
7120
  // Check parent containers
7115
7121
  const settingsContainer = createdBtn.closest('.uvf-settings-container');
7116
7122
  if (settingsContainer) {
@@ -7120,7 +7126,7 @@ export class WebPlayer extends BasePlayer {
7120
7126
  this.debugLog('Settings container visibility:', containerStyle.visibility);
7121
7127
  this.debugLog('Settings container rect:', { width: containerRect.width, height: containerRect.height, top: containerRect.top, left: containerRect.left });
7122
7128
  }
7123
-
7129
+
7124
7130
  const rightControls = createdBtn.closest('.uvf-right-controls');
7125
7131
  if (rightControls) {
7126
7132
  const parentStyle = window.getComputedStyle(rightControls);
@@ -7132,7 +7138,7 @@ export class WebPlayer extends BasePlayer {
7132
7138
  } else {
7133
7139
  this.debugError('Settings button NOT found after creation!');
7134
7140
  }
7135
-
7141
+
7136
7142
  // Fallback: Force settings button visibility if it's not rendering properly
7137
7143
  setTimeout(() => {
7138
7144
  const btn = document.getElementById('uvf-settings-btn');
@@ -7152,7 +7158,7 @@ export class WebPlayer extends BasePlayer {
7152
7158
  btn.style.border = '1px solid rgba(255,255,255,0.1)';
7153
7159
  btn.style.position = 'relative';
7154
7160
  btn.style.zIndex = '10';
7155
-
7161
+
7156
7162
  // Also fix the container
7157
7163
  const container = btn.parentElement;
7158
7164
  if (container) {
@@ -7162,7 +7168,7 @@ export class WebPlayer extends BasePlayer {
7162
7168
  container.style.minWidth = '44px';
7163
7169
  container.style.minHeight = '44px';
7164
7170
  }
7165
-
7171
+
7166
7172
  this.debugLog('Fallback styles applied to settings button');
7167
7173
  }
7168
7174
  }
@@ -7171,7 +7177,7 @@ export class WebPlayer extends BasePlayer {
7171
7177
  } else {
7172
7178
  this.debugLog('Settings button NOT created - settings disabled');
7173
7179
  }
7174
-
7180
+
7175
7181
  // EPG button (Electronic Program Guide)
7176
7182
  const epgBtn = document.createElement('button');
7177
7183
  epgBtn.className = 'uvf-control-btn';
@@ -7185,42 +7191,42 @@ export class WebPlayer extends BasePlayer {
7185
7191
  </svg>`;
7186
7192
  epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
7187
7193
  rightControls.appendChild(epgBtn);
7188
-
7194
+
7189
7195
  // PiP button - only show on desktop/supported browsers
7190
7196
  const pipBtn = document.createElement('button');
7191
7197
  pipBtn.className = 'uvf-control-btn';
7192
7198
  pipBtn.id = 'uvf-pip-btn';
7193
7199
  pipBtn.title = 'Picture-in-Picture';
7194
7200
  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
-
7201
+
7196
7202
  // Hide PiP button on mobile devices and browsers that don't support it
7197
7203
  if (this.isMobileDevice() || !this.isPipSupported()) {
7198
7204
  pipBtn.style.display = 'none';
7199
7205
  }
7200
-
7206
+
7201
7207
  rightControls.appendChild(pipBtn);
7202
-
7208
+
7203
7209
  // Fullscreen button
7204
7210
  const fullscreenBtn = document.createElement('button');
7205
7211
  fullscreenBtn.className = 'uvf-control-btn';
7206
7212
  fullscreenBtn.id = 'uvf-fullscreen-btn';
7207
7213
  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
7214
  rightControls.appendChild(fullscreenBtn);
7209
-
7215
+
7210
7216
  controlsRow.appendChild(rightControls);
7211
-
7217
+
7212
7218
  // Assemble controls bar
7213
7219
  controlsBar.appendChild(aboveSeekbarSection);
7214
7220
  controlsBar.appendChild(progressSection);
7215
7221
  controlsBar.appendChild(controlsRow);
7216
7222
  container.appendChild(controlsBar);
7217
-
7223
+
7218
7224
  this.controlsContainer = controlsBar;
7219
7225
  }
7220
7226
 
7221
7227
  private setupControlsEventListeners(): void {
7222
7228
  if (!this.useCustomControls || !this.video) return;
7223
-
7229
+
7224
7230
  const wrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
7225
7231
  const centerPlay = document.getElementById('uvf-center-play');
7226
7232
  const playPauseBtn = document.getElementById('uvf-play-pause');
@@ -7232,25 +7238,25 @@ export class WebPlayer extends BasePlayer {
7232
7238
  const progressBar = document.getElementById('uvf-progress-bar');
7233
7239
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
7234
7240
  const settingsBtn = document.getElementById('uvf-settings-btn');
7235
-
7241
+
7236
7242
  // Get the event target (video element or YouTube player)
7237
7243
  const getEventTarget = () => this.youtubePlayer && this.youtubePlayerReady ? this.youtubePlayer : this.video;
7238
-
7244
+
7239
7245
  // Disable right-click context menu
7240
7246
  this.video.addEventListener('contextmenu', (e) => {
7241
7247
  e.preventDefault();
7242
7248
  return false;
7243
7249
  });
7244
-
7250
+
7245
7251
  wrapper?.addEventListener('contextmenu', (e) => {
7246
7252
  e.preventDefault();
7247
7253
  return false;
7248
7254
  });
7249
-
7255
+
7250
7256
  // Play/Pause
7251
7257
  centerPlay?.addEventListener('click', () => this.togglePlayPause());
7252
7258
  playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
7253
-
7259
+
7254
7260
  // Video click behavior will be handled by the comprehensive tap system below
7255
7261
  // Desktop click for play/pause
7256
7262
  if (!this.isMobileDevice()) {
@@ -7258,20 +7264,20 @@ export class WebPlayer extends BasePlayer {
7258
7264
  this.togglePlayPause();
7259
7265
  });
7260
7266
  }
7261
-
7267
+
7262
7268
  // Update play/pause icons
7263
7269
  this.video.addEventListener('play', () => {
7264
7270
  const playIcon = document.getElementById('uvf-play-icon');
7265
7271
  const pauseIcon = document.getElementById('uvf-pause-icon');
7266
7272
  if (playIcon) playIcon.style.display = 'none';
7267
7273
  if (pauseIcon) pauseIcon.style.display = 'block';
7268
-
7274
+
7269
7275
  // Hide center play button when playing
7270
7276
  if (centerPlay) {
7271
7277
  centerPlay.classList.add('hidden');
7272
7278
  this.debugLog('Center play button hidden - video playing');
7273
7279
  }
7274
-
7280
+
7275
7281
  // Schedule hide controls
7276
7282
  setTimeout(() => {
7277
7283
  if (this.state.isPlaying) {
@@ -7279,13 +7285,13 @@ export class WebPlayer extends BasePlayer {
7279
7285
  }
7280
7286
  }, 1000);
7281
7287
  });
7282
-
7288
+
7283
7289
  this.video.addEventListener('pause', () => {
7284
7290
  const playIcon = document.getElementById('uvf-play-icon');
7285
7291
  const pauseIcon = document.getElementById('uvf-pause-icon');
7286
7292
  if (playIcon) playIcon.style.display = 'block';
7287
7293
  if (pauseIcon) pauseIcon.style.display = 'none';
7288
-
7294
+
7289
7295
  // Show center play button when paused
7290
7296
  if (centerPlay) {
7291
7297
  centerPlay.classList.remove('hidden');
@@ -7293,7 +7299,7 @@ export class WebPlayer extends BasePlayer {
7293
7299
  }
7294
7300
  this.showControls();
7295
7301
  });
7296
-
7302
+
7297
7303
  // Ensure center play button is visible initially when video is paused/stopped
7298
7304
  this.video.addEventListener('loadeddata', () => {
7299
7305
  if (centerPlay && (this.video?.paused || this.video?.ended)) {
@@ -7301,14 +7307,14 @@ export class WebPlayer extends BasePlayer {
7301
7307
  this.debugLog('Center play button shown - video loaded and paused');
7302
7308
  }
7303
7309
  });
7304
-
7310
+
7305
7311
  this.video.addEventListener('ended', () => {
7306
7312
  if (centerPlay) {
7307
7313
  centerPlay.classList.remove('hidden');
7308
7314
  this.debugLog('Center play button shown - video ended');
7309
7315
  }
7310
7316
  });
7311
-
7317
+
7312
7318
  // Skip buttons with null safety - works with both video and YouTube
7313
7319
  skipBackBtn?.addEventListener('click', () => {
7314
7320
  const currentTime = this.getCurrentTime();
@@ -7326,19 +7332,19 @@ export class WebPlayer extends BasePlayer {
7326
7332
  this.showShortcutIndicator('+10s');
7327
7333
  }
7328
7334
  });
7329
-
7335
+
7330
7336
  // Volume control
7331
7337
  volumeBtn?.addEventListener('click', (e) => {
7332
7338
  e.stopPropagation();
7333
7339
  this.toggleMuteAction();
7334
7340
  });
7335
-
7341
+
7336
7342
  // Volume panel interactions
7337
7343
  volumeBtn?.addEventListener('mouseenter', () => {
7338
7344
  if (this.volumeHideTimeout) clearTimeout(this.volumeHideTimeout);
7339
7345
  volumePanel?.classList.add('active');
7340
7346
  });
7341
-
7347
+
7342
7348
  volumeBtn?.addEventListener('mouseleave', () => {
7343
7349
  this.volumeHideTimeout = setTimeout(() => {
7344
7350
  if (!volumePanel?.matches(':hover')) {
@@ -7346,12 +7352,12 @@ export class WebPlayer extends BasePlayer {
7346
7352
  }
7347
7353
  }, 800);
7348
7354
  });
7349
-
7355
+
7350
7356
  volumePanel?.addEventListener('mouseenter', () => {
7351
7357
  if (this.volumeHideTimeout) clearTimeout(this.volumeHideTimeout);
7352
7358
  volumePanel.classList.add('active');
7353
7359
  });
7354
-
7360
+
7355
7361
  volumePanel?.addEventListener('mouseleave', () => {
7356
7362
  if (!this.isVolumeSliding) {
7357
7363
  setTimeout(() => {
@@ -7361,7 +7367,7 @@ export class WebPlayer extends BasePlayer {
7361
7367
  }, 1500);
7362
7368
  }
7363
7369
  });
7364
-
7370
+
7365
7371
  // Volume slider
7366
7372
  volumeSlider?.addEventListener('mousedown', (e) => {
7367
7373
  e.stopPropagation();
@@ -7369,43 +7375,43 @@ export class WebPlayer extends BasePlayer {
7369
7375
  volumePanel?.classList.add('active');
7370
7376
  this.handleVolumeChange(e as MouseEvent);
7371
7377
  });
7372
-
7378
+
7373
7379
  volumeSlider?.addEventListener('click', (e) => {
7374
7380
  e.stopPropagation();
7375
7381
  this.handleVolumeChange(e as MouseEvent);
7376
7382
  });
7377
-
7378
-
7383
+
7384
+
7379
7385
  // Progress bar interactions
7380
7386
  progressBar?.addEventListener('click', (e) => {
7381
7387
  this.seekToPosition(e as MouseEvent);
7382
7388
  });
7383
-
7389
+
7384
7390
  progressBar?.addEventListener('mousedown', (e) => {
7385
7391
  this.isDragging = true;
7386
7392
  this.showTimeTooltip = true;
7387
7393
  this.seekToPosition(e as MouseEvent);
7388
7394
  this.updateTimeTooltip(e as MouseEvent);
7389
7395
  });
7390
-
7396
+
7391
7397
  // Hover tooltip functionality
7392
7398
  progressBar?.addEventListener('mouseenter', () => {
7393
7399
  this.showTimeTooltip = true;
7394
7400
  });
7395
-
7401
+
7396
7402
  progressBar?.addEventListener('mouseleave', () => {
7397
7403
  if (!this.isDragging) {
7398
7404
  this.showTimeTooltip = false;
7399
7405
  this.hideTimeTooltip();
7400
7406
  }
7401
7407
  });
7402
-
7408
+
7403
7409
  progressBar?.addEventListener('mousemove', (e) => {
7404
7410
  if (this.showTimeTooltip) {
7405
7411
  this.updateTimeTooltip(e as MouseEvent);
7406
7412
  }
7407
7413
  });
7408
-
7414
+
7409
7415
  // Touch support for mobile devices
7410
7416
  progressBar?.addEventListener('touchstart', (e) => {
7411
7417
  e.preventDefault(); // Prevent scrolling
@@ -7417,7 +7423,7 @@ export class WebPlayer extends BasePlayer {
7417
7423
  });
7418
7424
  this.seekToPosition(mouseEvent);
7419
7425
  }, { passive: false });
7420
-
7426
+
7421
7427
  // Global mouse and touch events for enhanced dragging
7422
7428
  document.addEventListener('mousemove', (e) => {
7423
7429
  if (this.isVolumeSliding) {
@@ -7429,7 +7435,7 @@ export class WebPlayer extends BasePlayer {
7429
7435
  this.updateTimeTooltip(e);
7430
7436
  }
7431
7437
  });
7432
-
7438
+
7433
7439
  document.addEventListener('touchmove', (e) => {
7434
7440
  if (this.isDragging && progressBar) {
7435
7441
  e.preventDefault(); // Prevent scrolling
@@ -7441,7 +7447,7 @@ export class WebPlayer extends BasePlayer {
7441
7447
  this.seekToPosition(mouseEvent);
7442
7448
  }
7443
7449
  }, { passive: false });
7444
-
7450
+
7445
7451
  document.addEventListener('mouseup', () => {
7446
7452
  if (this.isVolumeSliding) {
7447
7453
  this.isVolumeSliding = false;
@@ -7451,7 +7457,7 @@ export class WebPlayer extends BasePlayer {
7451
7457
  }
7452
7458
  }, 2000);
7453
7459
  }
7454
-
7460
+
7455
7461
  if (this.isDragging) {
7456
7462
  this.isDragging = false;
7457
7463
  // Remove dragging class from handle
@@ -7464,7 +7470,7 @@ export class WebPlayer extends BasePlayer {
7464
7470
  }
7465
7471
  }
7466
7472
  });
7467
-
7473
+
7468
7474
  document.addEventListener('touchend', () => {
7469
7475
  if (this.isDragging) {
7470
7476
  this.isDragging = false;
@@ -7476,26 +7482,26 @@ export class WebPlayer extends BasePlayer {
7476
7482
  this.hideTimeTooltip();
7477
7483
  }
7478
7484
  });
7479
-
7485
+
7480
7486
  // Update progress bar
7481
7487
  this.video.addEventListener('timeupdate', () => {
7482
7488
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
7483
7489
  const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
7484
-
7490
+
7485
7491
  if (this.video && progressFilled) {
7486
7492
  const percent = (this.video.currentTime / this.video.duration) * 100;
7487
7493
  progressFilled.style.width = percent + '%';
7488
-
7494
+
7489
7495
  // Update handle position (only when not dragging)
7490
7496
  if (progressHandle && !this.isDragging) {
7491
7497
  progressHandle.style.left = percent + '%';
7492
7498
  }
7493
7499
  }
7494
-
7500
+
7495
7501
  // Update time display using the dedicated method
7496
7502
  this.updateTimeDisplay();
7497
7503
  });
7498
-
7504
+
7499
7505
  // Update buffered progress
7500
7506
  this.video.addEventListener('progress', () => {
7501
7507
  const progressBuffered = document.getElementById('uvf-progress-buffered') as HTMLElement;
@@ -7504,7 +7510,7 @@ export class WebPlayer extends BasePlayer {
7504
7510
  progressBuffered.style.width = buffered + '%';
7505
7511
  }
7506
7512
  });
7507
-
7513
+
7508
7514
  // Update volume display
7509
7515
  this.video.addEventListener('volumechange', () => {
7510
7516
  const volumeFill = document.getElementById('uvf-volume-fill') as HTMLElement;
@@ -7533,7 +7539,7 @@ export class WebPlayer extends BasePlayer {
7533
7539
  }
7534
7540
  }
7535
7541
  });
7536
-
7542
+
7537
7543
  // Fullscreen button with enhanced cross-platform support
7538
7544
  fullscreenBtn?.addEventListener('click', (event) => {
7539
7545
  // Enhanced debugging for all platforms
@@ -7542,7 +7548,7 @@ export class WebPlayer extends BasePlayer {
7542
7548
  const isIOS = this.isIOSDevice();
7543
7549
  const isAndroid = this.isAndroidDevice();
7544
7550
  const isMobile = this.isMobileDevice();
7545
-
7551
+
7546
7552
  this.debugLog('Fullscreen button clicked:', {
7547
7553
  isBrave,
7548
7554
  isPrivate,
@@ -7555,13 +7561,13 @@ export class WebPlayer extends BasePlayer {
7555
7561
  timestamp: Date.now(),
7556
7562
  fullscreenSupported: this.isFullscreenSupported()
7557
7563
  });
7558
-
7564
+
7559
7565
  // Update user interaction timestamp
7560
7566
  this.lastUserInteraction = Date.now();
7561
-
7567
+
7562
7568
  // Run permissions check before attempting fullscreen
7563
7569
  this.checkFullscreenPermissions();
7564
-
7570
+
7565
7571
  if (this.isFullscreen()) {
7566
7572
  this.debugLog('Exiting fullscreen via button');
7567
7573
  this.exitFullscreen().catch(err => {
@@ -7569,18 +7575,18 @@ export class WebPlayer extends BasePlayer {
7569
7575
  });
7570
7576
  } else {
7571
7577
  this.debugLog('Entering fullscreen via button');
7572
-
7578
+
7573
7579
  // iOS Safari special message
7574
7580
  if (isIOS) {
7575
7581
  this.showShortcutIndicator('Using iOS video fullscreen');
7576
7582
  } else if (isAndroid) {
7577
7583
  this.showShortcutIndicator('Entering fullscreen - rotate to landscape');
7578
7584
  }
7579
-
7585
+
7580
7586
  // Use enhanced cross-platform fullscreen method
7581
7587
  this.enterFullscreen().catch(err => {
7582
7588
  this.debugWarn('Fullscreen button failed:', err.message);
7583
-
7589
+
7584
7590
  // Platform-specific error messages
7585
7591
  if (isIOS) {
7586
7592
  this.showTemporaryMessage('iOS: Use device rotation or video controls for fullscreen');
@@ -7594,69 +7600,70 @@ export class WebPlayer extends BasePlayer {
7594
7600
  });
7595
7601
  }
7596
7602
  });
7597
-
7603
+
7598
7604
  // Update fullscreen button icon based on state
7599
7605
  const updateFullscreenIcon = () => {
7600
7606
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
7601
7607
  if (fullscreenBtn) {
7602
- fullscreenBtn.innerHTML = this.isFullscreen()
7608
+ fullscreenBtn.innerHTML = this.isFullscreen()
7603
7609
  ? '<svg viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>'
7604
7610
  : '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
7605
7611
  }
7606
7612
  };
7607
-
7613
+
7608
7614
  // Listen for fullscreen state changes to update icon
7609
7615
  this.on('onFullscreenChanged', updateFullscreenIcon);
7610
-
7616
+
7611
7617
  // Loading states
7612
7618
  this.video.addEventListener('waiting', () => {
7613
7619
  const loading = document.getElementById('uvf-loading');
7614
7620
  if (loading) loading.classList.add('active');
7615
7621
  });
7616
-
7622
+
7617
7623
  this.video.addEventListener('canplay', () => {
7618
7624
  const loading = document.getElementById('uvf-loading');
7619
7625
  if (loading) loading.classList.remove('active');
7620
7626
  // Update settings menu when video is ready
7621
7627
  this.updateSettingsMenu();
7628
+ this.debugLog('📡 canplay event fired - video ready to play');
7622
7629
  });
7623
7630
 
7624
7631
  this.video.addEventListener('loadedmetadata', () => {
7625
7632
  // Update settings menu when metadata is loaded
7626
7633
  this.updateSettingsMenu();
7627
7634
  });
7628
-
7635
+
7629
7636
  // Note: Enhanced mouse movement and control visibility handled in setupFullscreenListeners()
7630
-
7637
+
7631
7638
  this.controlsContainer?.addEventListener('mouseenter', () => {
7632
7639
  if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
7633
7640
  });
7634
-
7641
+
7635
7642
  this.controlsContainer?.addEventListener('mouseleave', () => {
7636
7643
  if (this.state.isPlaying) {
7637
7644
  this.scheduleHideControls();
7638
7645
  }
7639
7646
  });
7640
-
7641
-
7647
+
7648
+
7642
7649
  // Settings menu - dynamically populated
7643
7650
  const settingsMenu = document.getElementById('uvf-settings-menu');
7644
7651
  this.debugLog('Settings menu element found:', !!settingsMenu);
7645
7652
  this.debugLog('Settings button found:', !!settingsBtn);
7646
-
7653
+
7647
7654
  settingsBtn?.addEventListener('click', (e) => {
7648
7655
  e.stopPropagation();
7649
7656
  this.debugLog('Settings button clicked!');
7650
7657
  this.debugLog('Settings menu before update:', settingsMenu?.innerHTML?.length || 0, 'characters');
7651
-
7658
+
7652
7659
  // Update the menu content before showing it
7653
7660
  this.updateSettingsMenu();
7654
-
7661
+
7655
7662
  this.debugLog('Settings menu after update:', settingsMenu?.innerHTML?.length || 0, 'characters');
7656
7663
  this.debugLog('Settings menu classes before toggle:', Array.from(settingsMenu?.classList || []).join(' '));
7657
-
7664
+
7658
7665
  settingsMenu?.classList.toggle('active');
7659
-
7666
+
7660
7667
  // Force visibility if menu is active, hide if not active
7661
7668
  if (settingsMenu) {
7662
7669
  if (settingsMenu.classList.contains('active')) {
@@ -7681,13 +7688,13 @@ export class WebPlayer extends BasePlayer {
7681
7688
  this.debugLog('Applied fallback styles to hide menu');
7682
7689
  }
7683
7690
  }
7684
-
7691
+
7685
7692
  this.debugLog('Settings menu classes after toggle:', Array.from(settingsMenu?.classList || []).join(' '));
7686
7693
  this.debugLog('Settings menu computed display:', window.getComputedStyle(settingsMenu || document.body).display);
7687
7694
  this.debugLog('Settings menu computed visibility:', window.getComputedStyle(settingsMenu || document.body).visibility);
7688
7695
  this.debugLog('Settings menu computed opacity:', window.getComputedStyle(settingsMenu || document.body).opacity);
7689
7696
  });
7690
-
7697
+
7691
7698
  // EPG button
7692
7699
  const epgBtn = document.getElementById('uvf-epg-btn');
7693
7700
  epgBtn?.addEventListener('click', (e) => {
@@ -7696,16 +7703,16 @@ export class WebPlayer extends BasePlayer {
7696
7703
  // Trigger custom event for EPG toggle
7697
7704
  this.emit('epgToggle', {});
7698
7705
  });
7699
-
7706
+
7700
7707
  // PiP button
7701
7708
  const pipBtn = document.getElementById('uvf-pip-btn');
7702
7709
  pipBtn?.addEventListener('click', () => this.togglePiP());
7703
-
7710
+
7704
7711
  // Top control buttons
7705
7712
  const castBtn = document.getElementById('uvf-cast-btn');
7706
7713
  const stopCastBtn = document.getElementById('uvf-stop-cast-btn');
7707
7714
  const shareBtn = document.getElementById('uvf-share-btn');
7708
-
7715
+
7709
7716
  // Update cast button icon and functionality for iOS (AirPlay)
7710
7717
  if (this.isIOSDevice() && castBtn) {
7711
7718
  castBtn.innerHTML = `
@@ -7716,19 +7723,19 @@ export class WebPlayer extends BasePlayer {
7716
7723
  castBtn.setAttribute('title', 'AirPlay');
7717
7724
  castBtn.setAttribute('aria-label', 'AirPlay');
7718
7725
  }
7719
-
7726
+
7720
7727
  castBtn?.addEventListener('click', () => this.onCastButtonClick());
7721
7728
  stopCastBtn?.addEventListener('click', () => this.stopCasting());
7722
7729
  shareBtn?.addEventListener('click', () => this.shareVideo());
7723
-
7730
+
7724
7731
  // Hide settings menu when clicking outside or pressing Escape
7725
7732
  document.addEventListener('click', (e) => {
7726
- if (!(e.target as HTMLElement).closest('#uvf-settings-btn') &&
7727
- !(e.target as HTMLElement).closest('#uvf-settings-menu')) {
7733
+ if (!(e.target as HTMLElement).closest('#uvf-settings-btn') &&
7734
+ !(e.target as HTMLElement).closest('#uvf-settings-menu')) {
7728
7735
  this.hideSettingsMenu();
7729
7736
  }
7730
7737
  });
7731
-
7738
+
7732
7739
  // Add Escape key handler for settings menu
7733
7740
  document.addEventListener('keydown', (e) => {
7734
7741
  if (e.key === 'Escape' && settingsMenu?.classList.contains('active')) {
@@ -7736,7 +7743,7 @@ export class WebPlayer extends BasePlayer {
7736
7743
  }
7737
7744
  });
7738
7745
  }
7739
-
7746
+
7740
7747
  protected setupKeyboardShortcuts(): void {
7741
7748
  // Add keyboard event listener to both document and player wrapper for better coverage
7742
7749
  const handleKeydown = (e: KeyboardEvent) => {
@@ -7745,23 +7752,23 @@ export class WebPlayer extends BasePlayer {
7745
7752
  if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
7746
7753
  return;
7747
7754
  }
7748
-
7755
+
7749
7756
  // Block all keyboard controls during ads (Google IMA handles ad controls)
7750
7757
  if (this.isAdPlaying) {
7751
7758
  this.debugLog('Keyboard blocked: Ad is playing');
7752
7759
  e.preventDefault();
7753
7760
  return;
7754
7761
  }
7755
-
7762
+
7756
7763
  // Debug logging
7757
7764
  this.debugLog('Keyboard event:', e.key, 'target:', target.tagName);
7758
-
7765
+
7759
7766
  let shortcutText = '';
7760
-
7767
+
7761
7768
  // Update interaction timestamp
7762
7769
  this.lastUserInteraction = Date.now();
7763
-
7764
- switch(e.key) {
7770
+
7771
+ switch (e.key) {
7765
7772
  case ' ':
7766
7773
  case 'Spacebar': // For older browsers
7767
7774
  case 'k':
@@ -7772,13 +7779,13 @@ export class WebPlayer extends BasePlayer {
7772
7779
  videoPaused: this.video?.paused,
7773
7780
  videoExists: !!this.video
7774
7781
  });
7775
-
7782
+
7776
7783
  // Determine what action we're about to take based on current video state
7777
7784
  const willPlay = this.video?.paused || false;
7778
7785
  this.debugLog('Will perform action:', willPlay ? 'PLAY' : 'PAUSE');
7779
-
7786
+
7780
7787
  this.togglePlayPause();
7781
-
7788
+
7782
7789
  // Show the action we're taking, not the current state
7783
7790
  shortcutText = willPlay ? 'Play' : 'Pause';
7784
7791
  this.debugLog('Showing icon:', shortcutText);
@@ -7844,7 +7851,7 @@ export class WebPlayer extends BasePlayer {
7844
7851
  break;
7845
7852
  case 'f':
7846
7853
  e.preventDefault();
7847
-
7854
+
7848
7855
  if (!document.fullscreenElement) {
7849
7856
  // Always use the fullscreen button for maximum reliability
7850
7857
  this.triggerFullscreenButton();
@@ -7888,26 +7895,26 @@ export class WebPlayer extends BasePlayer {
7888
7895
  }
7889
7896
  break;
7890
7897
  }
7891
-
7898
+
7892
7899
  if (shortcutText) {
7893
7900
  this.debugLog('Showing shortcut indicator:', shortcutText);
7894
7901
  this.showShortcutIndicator(shortcutText);
7895
7902
  }
7896
7903
  };
7897
-
7904
+
7898
7905
  // Add event listeners to multiple targets for better coverage
7899
7906
  document.addEventListener('keydown', handleKeydown, { capture: true });
7900
-
7907
+
7901
7908
  // Also add to the player wrapper if it exists
7902
7909
  if (this.playerWrapper) {
7903
7910
  this.playerWrapper.addEventListener('keydown', handleKeydown);
7904
7911
  this.playerWrapper.setAttribute('tabindex', '0'); // Make it focusable
7905
-
7912
+
7906
7913
  // Add visual feedback when player is focused for better UX
7907
7914
  this.playerWrapper.addEventListener('focus', () => {
7908
7915
  this.debugLog('Player focused - keyboard shortcuts available');
7909
7916
  });
7910
-
7917
+
7911
7918
  // Auto-focus the player when clicked to enable keyboard shortcuts
7912
7919
  this.playerWrapper.addEventListener('click', (e) => {
7913
7920
  // Don't focus if clicking on a control button
@@ -7918,17 +7925,17 @@ export class WebPlayer extends BasePlayer {
7918
7925
  this.lastUserInteraction = Date.now();
7919
7926
  }
7920
7927
  });
7921
-
7928
+
7922
7929
  // Also focus on any interaction with the video area
7923
7930
  this.playerWrapper.addEventListener('mousedown', () => {
7924
7931
  this.playerWrapper?.focus();
7925
7932
  this.lastUserInteraction = Date.now();
7926
7933
  });
7927
-
7934
+
7928
7935
  // Advanced tap handling system for mobile
7929
7936
  this.setupAdvancedTapHandling();
7930
7937
  }
7931
-
7938
+
7932
7939
  // Add to the video element
7933
7940
  if (this.video) {
7934
7941
  this.video.addEventListener('keydown', handleKeydown);
@@ -7937,25 +7944,25 @@ export class WebPlayer extends BasePlayer {
7937
7944
 
7938
7945
  protected setupWatermark(): void {
7939
7946
  if (!this.watermarkCanvas) return;
7940
-
7947
+
7941
7948
  // Get watermark configuration
7942
7949
  const watermarkConfig = (this.config as any).watermark;
7943
-
7950
+
7944
7951
  // Check if watermark is disabled or not configured
7945
7952
  if (!watermarkConfig || watermarkConfig.enabled === false) {
7946
7953
  this.debugLog('Watermark disabled or not configured');
7947
7954
  return;
7948
7955
  }
7949
-
7956
+
7950
7957
  // If watermark config exists but enabled is not explicitly set, default to disabled
7951
7958
  if (watermarkConfig.enabled !== true) {
7952
7959
  this.debugLog('Watermark not explicitly enabled');
7953
7960
  return;
7954
7961
  }
7955
-
7962
+
7956
7963
  const ctx = this.watermarkCanvas.getContext('2d');
7957
7964
  if (!ctx) return;
7958
-
7965
+
7959
7966
  // Default configuration values
7960
7967
  const config = {
7961
7968
  text: watermarkConfig.text || 'PREMIUM',
@@ -7971,31 +7978,31 @@ export class WebPlayer extends BasePlayer {
7971
7978
  gradientColors: watermarkConfig.style?.gradientColors || ['#ff0000', '#ff4d4f']
7972
7979
  }
7973
7980
  };
7974
-
7981
+
7975
7982
  this.debugLog('Watermark configuration:', config);
7976
-
7983
+
7977
7984
  const renderWatermark = () => {
7978
7985
  const container = this.watermarkCanvas!.parentElement;
7979
7986
  if (!container) return;
7980
-
7987
+
7981
7988
  this.watermarkCanvas!.width = container.offsetWidth;
7982
7989
  this.watermarkCanvas!.height = container.offsetHeight;
7983
-
7990
+
7984
7991
  ctx.clearRect(0, 0, this.watermarkCanvas!.width, this.watermarkCanvas!.height);
7985
-
7992
+
7986
7993
  // Build watermark text
7987
7994
  let text = config.text;
7988
7995
  if (config.showTime) {
7989
7996
  const timeStr = new Date().toLocaleTimeString();
7990
7997
  text += ` • ${timeStr}`;
7991
7998
  }
7992
-
7999
+
7993
8000
  // Set up styling
7994
8001
  ctx.save();
7995
8002
  ctx.globalAlpha = config.style.opacity;
7996
8003
  ctx.font = `${config.style.fontSize}px ${config.style.fontFamily}`;
7997
8004
  ctx.textAlign = 'left';
7998
-
8005
+
7999
8006
  // Set fill style
8000
8007
  if (config.style.color) {
8001
8008
  // Use solid color
@@ -8005,7 +8012,7 @@ export class WebPlayer extends BasePlayer {
8005
8012
  const wrapper = this.playerWrapper as HTMLElement | null;
8006
8013
  let c1 = config.style.gradientColors[0];
8007
8014
  let c2 = config.style.gradientColors[1];
8008
-
8015
+
8009
8016
  // Try to get theme colors if using defaults
8010
8017
  if (!watermarkConfig.style?.gradientColors) {
8011
8018
  try {
@@ -8016,18 +8023,18 @@ export class WebPlayer extends BasePlayer {
8016
8023
  if (v1) c1 = v1;
8017
8024
  if (v2) c2 = v2;
8018
8025
  }
8019
- } catch (_) {}
8026
+ } catch (_) { }
8020
8027
  }
8021
-
8028
+
8022
8029
  const gradient = ctx.createLinearGradient(0, 0, 200, 0);
8023
8030
  gradient.addColorStop(0, c1);
8024
8031
  gradient.addColorStop(1, c2);
8025
8032
  ctx.fillStyle = gradient;
8026
8033
  }
8027
-
8034
+
8028
8035
  // Calculate position
8029
8036
  let x: number, y: number;
8030
-
8037
+
8031
8038
  if (config.randomPosition) {
8032
8039
  // Random position (default behavior)
8033
8040
  x = 20 + Math.random() * Math.max(0, this.watermarkCanvas!.width - 200);
@@ -8036,7 +8043,7 @@ export class WebPlayer extends BasePlayer {
8036
8043
  // Fixed or calculated position
8037
8044
  const posX = config.position.x;
8038
8045
  const posY = config.position.y;
8039
-
8046
+
8040
8047
  // Calculate X position
8041
8048
  if (typeof posX === 'number') {
8042
8049
  x = posX;
@@ -8060,7 +8067,7 @@ export class WebPlayer extends BasePlayer {
8060
8067
  x = 20; // default left
8061
8068
  }
8062
8069
  }
8063
-
8070
+
8064
8071
  // Calculate Y position
8065
8072
  if (typeof posY === 'number') {
8066
8073
  y = posY;
@@ -8083,18 +8090,18 @@ export class WebPlayer extends BasePlayer {
8083
8090
  }
8084
8091
  }
8085
8092
  }
8086
-
8093
+
8087
8094
  // Render the watermark
8088
8095
  ctx.fillText(text, x, y);
8089
8096
  ctx.restore();
8090
-
8097
+
8091
8098
  this.debugLog('Watermark rendered:', { text, x, y });
8092
8099
  };
8093
-
8100
+
8094
8101
  // Set up interval with configured frequency
8095
8102
  setInterval(renderWatermark, config.updateInterval);
8096
8103
  renderWatermark(); // Render immediately
8097
-
8104
+
8098
8105
  this.debugLog('Watermark setup complete with update interval:', config.updateInterval + 'ms');
8099
8106
  }
8100
8107
 
@@ -8109,12 +8116,12 @@ export class WebPlayer extends BasePlayer {
8109
8116
  import('./paywall/PaywallController').then((m: any) => {
8110
8117
  this.paywallController = new m.PaywallController(config, {
8111
8118
  getOverlayContainer: () => this.playerWrapper,
8112
- onResume: (accessInfo?: any) => {
8113
- try {
8119
+ onResume: (accessInfo?: any) => {
8120
+ try {
8114
8121
  // Reset preview gate after successful auth/payment
8115
8122
  this.previewGateHit = false;
8116
8123
  this.paymentSuccessTime = Date.now();
8117
-
8124
+
8118
8125
  // Check if access was granted via email auth
8119
8126
  if (accessInfo && (accessInfo.accessGranted || accessInfo.paymentSuccessful)) {
8120
8127
  this.paymentSuccessful = true;
@@ -8123,22 +8130,22 @@ export class WebPlayer extends BasePlayer {
8123
8130
  this.paymentSuccessful = true;
8124
8131
  this.debugLog('Payment successful (via setPaywallConfig) - preview gate permanently disabled, resuming playback');
8125
8132
  }
8126
-
8127
- this.play();
8128
- } catch(_) {}
8133
+
8134
+ this.play();
8135
+ } catch (_) { }
8129
8136
  },
8130
8137
  onShow: () => {
8131
8138
  // Use safe pause method to avoid race conditions
8132
- try { this.requestPause(); } catch(_) {}
8139
+ try { this.requestPause(); } catch (_) { }
8133
8140
  },
8134
8141
  onClose: () => {
8135
8142
  // Resume video if auth was successful
8136
8143
  }
8137
8144
  });
8138
- }).catch(() => {});
8145
+ }).catch(() => { });
8139
8146
  }
8140
8147
  }
8141
- } catch (_) {}
8148
+ } catch (_) { }
8142
8149
  }
8143
8150
 
8144
8151
  private togglePlayPause(): void {
@@ -8148,12 +8155,12 @@ export class WebPlayer extends BasePlayer {
8148
8155
  youtubePlayerReady: this.youtubePlayerReady,
8149
8156
  playerState: this.state
8150
8157
  });
8151
-
8158
+
8152
8159
  // Handle YouTube player
8153
8160
  if (this.youtubePlayer && this.youtubePlayerReady) {
8154
8161
  const playerState = this.youtubePlayer.getPlayerState();
8155
8162
  this.debugLog('YouTube player state:', playerState);
8156
-
8163
+
8157
8164
  if (playerState === window.YT.PlayerState.PLAYING) {
8158
8165
  this.debugLog('YouTube video is playing, calling pause()');
8159
8166
  this.pause();
@@ -8163,13 +8170,13 @@ export class WebPlayer extends BasePlayer {
8163
8170
  }
8164
8171
  return;
8165
8172
  }
8166
-
8173
+
8167
8174
  // Handle regular video element
8168
8175
  if (!this.video) {
8169
8176
  this.debugError('No video element or YouTube player available for toggle');
8170
8177
  return;
8171
8178
  }
8172
-
8179
+
8173
8180
  if (this.video.paused) {
8174
8181
  this.debugLog('Video is paused, calling play()');
8175
8182
  this.play();
@@ -8185,25 +8192,25 @@ export class WebPlayer extends BasePlayer {
8185
8192
  const lim = Number(this.config.freeDuration || 0);
8186
8193
  if (!(lim > 0)) return;
8187
8194
  if (this.previewGateHit && !fromSeek) return;
8188
-
8195
+
8189
8196
  // Don't trigger gate if payment was successful for this session
8190
8197
  if (this.paymentSuccessful) {
8191
8198
  this.debugLog('Skipping preview gate - payment was successful, access granted permanently for this session');
8192
8199
  return;
8193
8200
  }
8194
-
8201
+
8195
8202
  // Don't trigger gate if payment was successful recently (within 5 seconds)
8196
8203
  const timeSincePayment = Date.now() - this.paymentSuccessTime;
8197
8204
  if (this.paymentSuccessTime > 0 && timeSincePayment < 5000) {
8198
8205
  this.debugLog('Skipping preview gate - recent payment success:', timeSincePayment + 'ms ago');
8199
8206
  return;
8200
8207
  }
8201
-
8208
+
8202
8209
  if (current >= lim - 0.01 && !this.previewGateHit) {
8203
8210
  this.previewGateHit = true;
8204
8211
  this.showNotification('Free preview ended.');
8205
8212
  this.emit('onFreePreviewEnded');
8206
-
8213
+
8207
8214
  // Trigger paywall controller which will handle auth/payment flow
8208
8215
  this.debugLog('Free preview gate hit, paywallController exists:', !!this.paywallController);
8209
8216
  if (this.paywallController) {
@@ -8219,18 +8226,18 @@ export class WebPlayer extends BasePlayer {
8219
8226
  if (this.remotePlayer && this.remotePlayer.isPaused === false) {
8220
8227
  this.remoteController.playOrPause();
8221
8228
  }
8222
- } catch (_) {}
8229
+ } catch (_) { }
8223
8230
  } else if (this.video) {
8224
- try {
8231
+ try {
8225
8232
  // Use deferred pause to avoid race conditions
8226
- this.requestPause();
8233
+ this.requestPause();
8227
8234
  if (fromSeek || ((this.video.currentTime || 0) > lim)) {
8228
8235
  this.safeSetCurrentTime(lim - 0.1);
8229
8236
  }
8230
- } catch (_) {}
8237
+ } catch (_) { }
8231
8238
  }
8232
8239
  }
8233
- } catch (_) {}
8240
+ } catch (_) { }
8234
8241
  }
8235
8242
 
8236
8243
  // Public runtime controls for free preview
@@ -8247,7 +8254,7 @@ export class WebPlayer extends BasePlayer {
8247
8254
  const cur = this.video ? (this.video.currentTime || 0) : 0;
8248
8255
  this.enforceFreePreviewGate(cur, true);
8249
8256
  }
8250
- } catch (_) {}
8257
+ } catch (_) { }
8251
8258
  }
8252
8259
  public resetFreePreviewGate(): void {
8253
8260
  // Only reset if payment hasn't been successful
@@ -8255,7 +8262,7 @@ export class WebPlayer extends BasePlayer {
8255
8262
  this.previewGateHit = false;
8256
8263
  }
8257
8264
  }
8258
-
8265
+
8259
8266
  public resetPaymentStatus(): void {
8260
8267
  this.paymentSuccessful = false;
8261
8268
  this.paymentSuccessTime = 0;
@@ -8265,7 +8272,7 @@ export class WebPlayer extends BasePlayer {
8265
8272
 
8266
8273
  private toggleMuteAction(): void {
8267
8274
  if (this.isCasting && this.remoteController) {
8268
- try { this.remoteController.muteOrUnmute(); } catch (_) {}
8275
+ try { this.remoteController.muteOrUnmute(); } catch (_) { }
8269
8276
  return;
8270
8277
  }
8271
8278
  if (this.video?.muted) {
@@ -8284,7 +8291,7 @@ export class WebPlayer extends BasePlayer {
8284
8291
  const isMobileUserAgent = mobileKeywords.some(keyword => userAgent.includes(keyword));
8285
8292
  const isSmallScreen = window.innerWidth <= 768;
8286
8293
  const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
8287
-
8294
+
8288
8295
  return isMobileUserAgent || (isSmallScreen && hasTouchScreen);
8289
8296
  }
8290
8297
 
@@ -8391,21 +8398,21 @@ export class WebPlayer extends BasePlayer {
8391
8398
  private handleVolumeChange(e: MouseEvent): void {
8392
8399
  const slider = document.getElementById('uvf-volume-slider');
8393
8400
  if (!slider) return;
8394
-
8401
+
8395
8402
  const rect = slider.getBoundingClientRect();
8396
8403
  const x = e.clientX - rect.left;
8397
8404
  const width = rect.width;
8398
8405
  const percent = Math.max(0, Math.min(1, x / width));
8399
-
8406
+
8400
8407
  if (this.isCasting && this.remoteController && this.remotePlayer) {
8401
8408
  try {
8402
8409
  if (this.remotePlayer.isMuted) {
8403
- try { this.remoteController.muteOrUnmute(); } catch (_) {}
8410
+ try { this.remoteController.muteOrUnmute(); } catch (_) { }
8404
8411
  this.remotePlayer.isMuted = false;
8405
8412
  }
8406
8413
  this.remotePlayer.volumeLevel = percent;
8407
8414
  this.remoteController.setVolumeLevel();
8408
- } catch (_) {}
8415
+ } catch (_) { }
8409
8416
  this.updateVolumeUIFromRemote();
8410
8417
  } else if (this.video) {
8411
8418
  this.setVolume(percent);
@@ -8418,25 +8425,25 @@ export class WebPlayer extends BasePlayer {
8418
8425
  const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
8419
8426
  const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
8420
8427
  if (!progressBar || !this.video) return;
8421
-
8428
+
8422
8429
  const duration = this.video.duration;
8423
8430
  // Validate duration before calculating seek time
8424
8431
  if (!isFinite(duration) || isNaN(duration) || duration <= 0) {
8425
8432
  this.debugWarn('Invalid video duration, cannot seek via progress bar');
8426
8433
  return;
8427
8434
  }
8428
-
8435
+
8429
8436
  const rect = progressBar.getBoundingClientRect();
8430
8437
  const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
8431
8438
  const percent = (x / rect.width) * 100;
8432
8439
  const time = (percent / 100) * duration;
8433
-
8440
+
8434
8441
  // Validate calculated time
8435
8442
  if (!isFinite(time) || isNaN(time)) {
8436
8443
  this.debugWarn('Calculated seek time is invalid:', time);
8437
8444
  return;
8438
8445
  }
8439
-
8446
+
8440
8447
  // Update UI immediately for responsive feedback
8441
8448
  if (progressFilled) {
8442
8449
  progressFilled.style.width = percent + '%';
@@ -8450,17 +8457,17 @@ export class WebPlayer extends BasePlayer {
8450
8457
  progressHandle.classList.remove('dragging');
8451
8458
  }
8452
8459
  }
8453
-
8460
+
8454
8461
  this.seek(time);
8455
8462
  }
8456
8463
 
8457
8464
  private formatTime(seconds: number): string {
8458
8465
  if (!seconds || isNaN(seconds)) return '00:00';
8459
-
8466
+
8460
8467
  const hours = Math.floor(seconds / 3600);
8461
8468
  const minutes = Math.floor((seconds % 3600) / 60);
8462
8469
  const secs = Math.floor(seconds % 60);
8463
-
8470
+
8464
8471
  if (hours > 0) {
8465
8472
  return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
8466
8473
  } else {
@@ -8471,10 +8478,10 @@ export class WebPlayer extends BasePlayer {
8471
8478
  private updateTimeDisplay(): void {
8472
8479
  const timeDisplay = document.getElementById('uvf-time-display');
8473
8480
  if (!timeDisplay) return;
8474
-
8481
+
8475
8482
  let current = 0;
8476
8483
  let duration = 0;
8477
-
8484
+
8478
8485
  // Get time from YouTube player if available
8479
8486
  if (this.youtubePlayer && this.youtubePlayerReady) {
8480
8487
  try {
@@ -8487,7 +8494,7 @@ export class WebPlayer extends BasePlayer {
8487
8494
  current = this.video.currentTime || 0;
8488
8495
  duration = this.video.duration || 0;
8489
8496
  }
8490
-
8497
+
8491
8498
  const currentFormatted = this.formatTime(current);
8492
8499
  const durationFormatted = this.formatTime(duration);
8493
8500
  timeDisplay.textContent = `${currentFormatted} / ${durationFormatted}`;
@@ -8505,7 +8512,7 @@ export class WebPlayer extends BasePlayer {
8505
8512
 
8506
8513
  private hideControls(): void {
8507
8514
  if (!this.state.isPlaying) return;
8508
-
8515
+
8509
8516
  const wrapper = this.container?.querySelector('.uvf-player-wrapper');
8510
8517
  if (wrapper) {
8511
8518
  wrapper.classList.remove('controls-visible');
@@ -8515,7 +8522,7 @@ export class WebPlayer extends BasePlayer {
8515
8522
 
8516
8523
  private scheduleHideControls(): void {
8517
8524
  if (!this.state.isPlaying) return;
8518
-
8525
+
8519
8526
  if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
8520
8527
  // Use longer timeout in fullscreen for better UX
8521
8528
  const timeout = this.isFullscreen() ? 4000 : 3000;
@@ -8542,7 +8549,7 @@ export class WebPlayer extends BasePlayer {
8542
8549
  const TAP_MOVEMENT_THRESHOLD = 10; // pixels
8543
8550
  const SKIP_SECONDS = 10;
8544
8551
  const FAST_PLAYBACK_RATE = 2;
8545
-
8552
+
8546
8553
  // Track if we're currently in a double-tap window
8547
8554
  let inDoubleTapWindow = false;
8548
8555
 
@@ -8663,7 +8670,7 @@ export class WebPlayer extends BasePlayer {
8663
8670
  this.debugLog('Single tap detected - toggling controls');
8664
8671
  const wrapper = this.container?.querySelector('.uvf-player-wrapper');
8665
8672
  const areControlsVisible = wrapper?.classList.contains('controls-visible');
8666
-
8673
+
8667
8674
  if (areControlsVisible) {
8668
8675
  // Hide controls and top UI elements
8669
8676
  this.hideControls();
@@ -8672,7 +8679,7 @@ export class WebPlayer extends BasePlayer {
8672
8679
  // Show controls and top UI elements
8673
8680
  this.showControls();
8674
8681
  this.debugLog('Single tap: showing controls');
8675
-
8682
+
8676
8683
  // Schedule auto-hide if video is playing
8677
8684
  if (this.state.isPlaying) {
8678
8685
  this.scheduleHideControls();
@@ -8692,7 +8699,7 @@ export class WebPlayer extends BasePlayer {
8692
8699
 
8693
8700
  const currentTime = this.video.currentTime;
8694
8701
  const duration = this.video.duration;
8695
-
8702
+
8696
8703
  // Validate current time and duration before calculating new time
8697
8704
  if (!isFinite(currentTime) || isNaN(currentTime) || !isFinite(duration) || isNaN(duration)) {
8698
8705
  this.debugWarn('Invalid video time values, skipping double-tap action');
@@ -8788,19 +8795,19 @@ export class WebPlayer extends BasePlayer {
8788
8795
  this.fastBackwardInterval = null;
8789
8796
  }
8790
8797
  }
8791
-
8798
+
8792
8799
  private isFullscreen(): boolean {
8793
8800
  return !!(document.fullscreenElement ||
8794
- (document as any).webkitFullscreenElement ||
8795
- (document as any).mozFullScreenElement ||
8796
- (document as any).msFullscreenElement);
8801
+ (document as any).webkitFullscreenElement ||
8802
+ (document as any).mozFullScreenElement ||
8803
+ (document as any).msFullscreenElement);
8797
8804
  }
8798
-
8805
+
8799
8806
  private setupFullscreenListeners(): void {
8800
8807
  // Handle fullscreen changes from browser/keyboard shortcuts
8801
8808
  const handleFullscreenChange = () => {
8802
8809
  const isFullscreen = this.isFullscreen();
8803
-
8810
+
8804
8811
  if (this.playerWrapper) {
8805
8812
  if (isFullscreen) {
8806
8813
  this.playerWrapper.classList.add('uvf-fullscreen');
@@ -8808,36 +8815,36 @@ export class WebPlayer extends BasePlayer {
8808
8815
  this.playerWrapper.classList.remove('uvf-fullscreen');
8809
8816
  }
8810
8817
  }
8811
-
8818
+
8812
8819
  // Show controls when entering/exiting fullscreen
8813
8820
  this.showControls();
8814
8821
  if (isFullscreen && this.state.isPlaying) {
8815
8822
  this.scheduleHideControls();
8816
8823
  }
8817
-
8824
+
8818
8825
  this.emit('onFullscreenChanged', isFullscreen);
8819
8826
  };
8820
-
8827
+
8821
8828
  // Listen for fullscreen change events (all browser prefixes)
8822
8829
  document.addEventListener('fullscreenchange', handleFullscreenChange);
8823
8830
  document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
8824
8831
  document.addEventListener('mozfullscreenchange', handleFullscreenChange);
8825
8832
  document.addEventListener('MSFullscreenChange', handleFullscreenChange);
8826
-
8833
+
8827
8834
  // Enhanced mouse/touch movement detection for control visibility
8828
8835
  let lastMouseMoveTime = 0;
8829
8836
  let mouseInactivityTimeout: any = null;
8830
-
8837
+
8831
8838
  const handleMouseMovement = () => {
8832
8839
  const now = Date.now();
8833
8840
  lastMouseMoveTime = now;
8834
-
8841
+
8835
8842
  // Show controls immediately on mouse movement
8836
8843
  this.showControls();
8837
-
8844
+
8838
8845
  // Clear existing inactivity timeout
8839
8846
  clearTimeout(mouseInactivityTimeout);
8840
-
8847
+
8841
8848
  // Set new inactivity timeout
8842
8849
  if (this.state.isPlaying) {
8843
8850
  const timeout = this.isFullscreen() ? 4000 : 3000;
@@ -8849,7 +8856,7 @@ export class WebPlayer extends BasePlayer {
8849
8856
  }, timeout);
8850
8857
  }
8851
8858
  };
8852
-
8859
+
8853
8860
  // Touch movement detection for mobile - only for actual dragging/scrolling
8854
8861
  // Note: Don't handle touchstart here as it conflicts with advanced tap handling
8855
8862
  const handleTouchMovement = () => {
@@ -8859,7 +8866,7 @@ export class WebPlayer extends BasePlayer {
8859
8866
  this.scheduleHideControls();
8860
8867
  }
8861
8868
  };
8862
-
8869
+
8863
8870
  // Add event listeners to the player wrapper
8864
8871
  if (this.playerWrapper) {
8865
8872
  this.playerWrapper.addEventListener('mousemove', handleMouseMovement, { passive: true });
@@ -8869,9 +8876,9 @@ export class WebPlayer extends BasePlayer {
8869
8876
  this.playerWrapper.addEventListener('touchmove', handleTouchMovement, { passive: true });
8870
8877
  }
8871
8878
  }
8872
-
8873
8879
 
8874
-
8880
+
8881
+
8875
8882
  private showShortcutIndicator(text: string): void {
8876
8883
  const el = document.getElementById('uvf-shortcut-indicator');
8877
8884
  this.debugLog('showShortcutIndicator called with:', text, 'element found:', !!el);
@@ -8891,7 +8898,7 @@ export class WebPlayer extends BasePlayer {
8891
8898
  el.innerHTML = `<div class="uvf-ki uvf-ki-icon">${svg}</div>`;
8892
8899
  resetAnim();
8893
8900
  };
8894
- const setSkip = (dir: 'fwd'|'back', num: number) => {
8901
+ const setSkip = (dir: 'fwd' | 'back', num: number) => {
8895
8902
  el.classList.add('uvf-ki-icon');
8896
8903
  const svg = dir === 'fwd'
8897
8904
  ? `<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 +8962,14 @@ export class WebPlayer extends BasePlayer {
8955
8962
  // auto-hide after animation
8956
8963
  clearTimeout(this._kiTo);
8957
8964
  this._kiTo = setTimeout(() => {
8958
- try { el.classList.remove('active'); } catch (_) {}
8965
+ try { el.classList.remove('active'); } catch (_) { }
8959
8966
  }, 1000);
8960
8967
  } catch (err) {
8961
8968
  try {
8962
8969
  (el as HTMLElement).textContent = String(text || '');
8963
8970
  el.classList.add('active');
8964
8971
  setTimeout(() => el.classList.remove('active'), 1000);
8965
- } catch(_) {}
8972
+ } catch (_) { }
8966
8973
  }
8967
8974
  }
8968
8975
 
@@ -9070,7 +9077,7 @@ export class WebPlayer extends BasePlayer {
9070
9077
  };
9071
9078
 
9072
9079
  this.coreChapterManager = new CoreChapterManager(coreChapterConfig);
9073
-
9080
+
9074
9081
  // Initialize the core chapter manager
9075
9082
  this.coreChapterManager.initialize();
9076
9083
 
@@ -9177,7 +9184,7 @@ export class WebPlayer extends BasePlayer {
9177
9184
  if (!this.chapterManager) {
9178
9185
  throw new Error('Chapter manager not initialized. Enable chapters in config first.');
9179
9186
  }
9180
-
9187
+
9181
9188
  try {
9182
9189
  await this.chapterManager.loadChapters(chapters);
9183
9190
  this.debugLog('Chapters loaded successfully');
@@ -9194,7 +9201,7 @@ export class WebPlayer extends BasePlayer {
9194
9201
  if (!this.chapterManager) {
9195
9202
  throw new Error('Chapter manager not initialized. Enable chapters in config first.');
9196
9203
  }
9197
-
9204
+
9198
9205
  try {
9199
9206
  await this.chapterManager.loadChaptersFromUrl(url);
9200
9207
  this.debugLog('Chapters loaded from URL successfully');
@@ -9211,7 +9218,7 @@ export class WebPlayer extends BasePlayer {
9211
9218
  if (!this.chapterManager || !this.video) {
9212
9219
  return null;
9213
9220
  }
9214
-
9221
+
9215
9222
  return this.chapterManager.getCurrentSegment(this.video.currentTime);
9216
9223
  }
9217
9224
 
@@ -9223,7 +9230,7 @@ export class WebPlayer extends BasePlayer {
9223
9230
  this.debugWarn('Cannot skip segment: chapter manager not initialized');
9224
9231
  return;
9225
9232
  }
9226
-
9233
+
9227
9234
  this.chapterManager.skipToSegment(segmentId);
9228
9235
  }
9229
9236
 
@@ -9234,7 +9241,7 @@ export class WebPlayer extends BasePlayer {
9234
9241
  if (!this.chapterManager) {
9235
9242
  return [];
9236
9243
  }
9237
-
9244
+
9238
9245
  return this.chapterManager.getSegments();
9239
9246
  }
9240
9247
 
@@ -9243,7 +9250,7 @@ export class WebPlayer extends BasePlayer {
9243
9250
  */
9244
9251
  public updateChapterConfig(newConfig: Partial<ChapterConfig>): void {
9245
9252
  this.chapterConfig = { ...this.chapterConfig, ...newConfig };
9246
-
9253
+
9247
9254
  if (this.chapterManager) {
9248
9255
  this.chapterManager.updateConfig(this.chapterConfig);
9249
9256
  }
@@ -9370,7 +9377,7 @@ export class WebPlayer extends BasePlayer {
9370
9377
  if (iconColor) wrapper.style.setProperty('--uvf-icon-color', iconColor);
9371
9378
  if (textPrimary) wrapper.style.setProperty('--uvf-text-primary', textPrimary);
9372
9379
  if (textSecondary) wrapper.style.setProperty('--uvf-text-secondary', textSecondary);
9373
-
9380
+
9374
9381
  // Set overlay colors for gradient backgrounds
9375
9382
  if (overlayStrong) wrapper.style.setProperty('--uvf-overlay-strong', overlayStrong);
9376
9383
  if (overlayMedium) wrapper.style.setProperty('--uvf-overlay-medium', overlayMedium);
@@ -9406,7 +9413,7 @@ export class WebPlayer extends BasePlayer {
9406
9413
  return { r: Math.round(nums[0]), g: Math.round(nums[1]), b: Math.round(nums[2]) };
9407
9414
  }
9408
9415
  }
9409
- } catch (_) {}
9416
+ } catch (_) { }
9410
9417
  return null;
9411
9418
  }
9412
9419
 
@@ -9431,26 +9438,26 @@ export class WebPlayer extends BasePlayer {
9431
9438
  const a = Math.max(0, Math.min(1, alpha));
9432
9439
  return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
9433
9440
  }
9434
-
9441
+
9435
9442
  private changeVolume(delta: number): void {
9436
9443
  if (this.isCasting && this.remoteController && this.remotePlayer) {
9437
9444
  const cur = this.remotePlayer.volumeLevel || 0;
9438
9445
  const next = Math.max(0, Math.min(1, cur + delta));
9439
9446
  try {
9440
9447
  if (this.remotePlayer.isMuted) {
9441
- try { this.remoteController.muteOrUnmute(); } catch (_) {}
9448
+ try { this.remoteController.muteOrUnmute(); } catch (_) { }
9442
9449
  this.remotePlayer.isMuted = false;
9443
9450
  }
9444
9451
  this.remotePlayer.volumeLevel = next;
9445
9452
  this.remoteController.setVolumeLevel();
9446
- } catch (_) {}
9453
+ } catch (_) { }
9447
9454
  this.updateVolumeUIFromRemote();
9448
9455
  return;
9449
9456
  }
9450
9457
  if (!this.video) return;
9451
9458
  this.video.volume = Math.max(0, Math.min(1, this.video.volume + delta));
9452
9459
  }
9453
-
9460
+
9454
9461
  private setSpeed(speed: number): void {
9455
9462
  // Handle YouTube player
9456
9463
  if (this.youtubePlayer && this.youtubePlayerReady) {
@@ -9462,7 +9469,7 @@ export class WebPlayer extends BasePlayer {
9462
9469
  } else if (this.video) {
9463
9470
  this.video.playbackRate = speed;
9464
9471
  }
9465
-
9472
+
9466
9473
  // Update UI
9467
9474
  document.querySelectorAll('.speed-option').forEach(option => {
9468
9475
  option.classList.remove('active');
@@ -9471,7 +9478,7 @@ export class WebPlayer extends BasePlayer {
9471
9478
  }
9472
9479
  });
9473
9480
  }
9474
-
9481
+
9475
9482
  private setQualityByLabel(quality: string): void {
9476
9483
  // Handle YouTube player
9477
9484
  if (this.youtubePlayer && this.youtubePlayerReady) {
@@ -9486,9 +9493,9 @@ export class WebPlayer extends BasePlayer {
9486
9493
  });
9487
9494
  return;
9488
9495
  }
9489
-
9496
+
9490
9497
  const qualityBadge = document.getElementById('uvf-quality-badge');
9491
-
9498
+
9492
9499
  // Update UI
9493
9500
  document.querySelectorAll('.quality-option').forEach(option => {
9494
9501
  option.classList.remove('active');
@@ -9496,7 +9503,7 @@ export class WebPlayer extends BasePlayer {
9496
9503
  option.classList.add('active');
9497
9504
  }
9498
9505
  });
9499
-
9506
+
9500
9507
  // Update badge
9501
9508
  if (qualityBadge) {
9502
9509
  if (quality === 'auto') {
@@ -9505,7 +9512,7 @@ export class WebPlayer extends BasePlayer {
9505
9512
  qualityBadge.textContent = quality + 'p';
9506
9513
  }
9507
9514
  }
9508
-
9515
+
9509
9516
  // If we have actual quality levels from HLS/DASH, apply them
9510
9517
  if (quality !== 'auto' && this.qualities.length > 0) {
9511
9518
  const qualityLevel = this.qualities.find(q => q.label === quality + 'p');
@@ -9517,7 +9524,7 @@ export class WebPlayer extends BasePlayer {
9517
9524
  this.setAutoQuality(true);
9518
9525
  }
9519
9526
  }
9520
-
9527
+
9521
9528
  private async togglePiP(): Promise<void> {
9522
9529
  try {
9523
9530
  if ((document as any).pictureInPictureElement) {
@@ -9529,14 +9536,14 @@ export class WebPlayer extends BasePlayer {
9529
9536
  console.error('PiP toggle failed:', error);
9530
9537
  }
9531
9538
  }
9532
-
9539
+
9533
9540
  private setupCastContextSafe(): void {
9534
9541
  try {
9535
9542
  const castNs = (window as any).cast;
9536
9543
  if (castNs && castNs.framework) {
9537
9544
  this.setupCastContext();
9538
9545
  }
9539
- } catch (_) {}
9546
+ } catch (_) { }
9540
9547
  }
9541
9548
 
9542
9549
  private setupCastContext(): void {
@@ -9549,14 +9556,14 @@ export class WebPlayer extends BasePlayer {
9549
9556
  try {
9550
9557
  const autoJoin = chromeNs?.cast?.AutoJoinPolicy?.ORIGIN_SCOPED;
9551
9558
  if (autoJoin) options.autoJoinPolicy = autoJoin;
9552
- } catch (_) {}
9559
+ } catch (_) { }
9553
9560
  this.castContext.setOptions(options);
9554
9561
  this.castContext.addEventListener(
9555
9562
  castNs.framework.CastContextEventType.SESSION_STATE_CHANGED,
9556
9563
  (ev: any) => {
9557
9564
  const state = ev.sessionState;
9558
9565
  if (state === castNs.framework.SessionState.SESSION_STARTED ||
9559
- state === castNs.framework.SessionState.SESSION_RESUMED) {
9566
+ state === castNs.framework.SessionState.SESSION_RESUMED) {
9560
9567
  this.enableCastRemoteControl();
9561
9568
  } else if (state === castNs.framework.SessionState.SESSION_ENDED) {
9562
9569
  this.disableCastRemoteControl();
@@ -9580,7 +9587,7 @@ export class WebPlayer extends BasePlayer {
9580
9587
  this._bindRemotePlayerEvents();
9581
9588
  }
9582
9589
  this.isCasting = true;
9583
- try { this.video?.pause(); } catch (_) {}
9590
+ try { this.video?.pause(); } catch (_) { }
9584
9591
  this._syncUIFromRemote();
9585
9592
  this._syncCastButtons();
9586
9593
  } catch (err) {
@@ -9703,7 +9710,7 @@ export class WebPlayer extends BasePlayer {
9703
9710
  const sess = castNs?.framework?.CastContext?.getInstance()?.getCurrentSession?.();
9704
9711
  const dev = sess && sess.getCastDevice ? sess.getCastDevice() : null;
9705
9712
  if (dev && dev.friendlyName) title += ` (${dev.friendlyName})`;
9706
- } catch (_) {}
9713
+ } catch (_) { }
9707
9714
  castBtn.setAttribute('title', title);
9708
9715
  castBtn.setAttribute('aria-label', title);
9709
9716
  } else {
@@ -9742,7 +9749,7 @@ export class WebPlayer extends BasePlayer {
9742
9749
  // Generate accordion-style menu
9743
9750
  this.generateAccordionMenu();
9744
9751
  }
9745
-
9752
+
9746
9753
  /**
9747
9754
  * Generate accordion-style settings menu
9748
9755
  */
@@ -9786,7 +9793,7 @@ export class WebPlayer extends BasePlayer {
9786
9793
  if (this.settingsConfig.quality && this.availableQualities.length > 0) {
9787
9794
  const currentQuality = this.availableQualities.find(q => q.value === this.currentQuality);
9788
9795
  const currentQualityLabel = currentQuality ? currentQuality.label : 'Auto';
9789
-
9796
+
9790
9797
  menuHTML += `
9791
9798
  <div class="uvf-accordion-item">
9792
9799
  <div class="uvf-accordion-header" data-section="quality">
@@ -9802,13 +9809,13 @@ export class WebPlayer extends BasePlayer {
9802
9809
  <div class="uvf-accordion-arrow">▼</div>
9803
9810
  </div>
9804
9811
  <div class="uvf-accordion-content" data-section="quality">`;
9805
-
9812
+
9806
9813
  this.availableQualities.forEach(quality => {
9807
9814
  const isActive = quality.value === this.currentQuality ? 'active' : '';
9808
9815
  const isPremium = this.isQualityPremium(quality);
9809
9816
  const isLocked = isPremium && !this.isPremiumUser();
9810
9817
  const qualityHeight = (quality as any).height || 0;
9811
-
9818
+
9812
9819
  if (isLocked) {
9813
9820
  const premiumLabel = this.premiumQualities?.premiumLabel || '🔒 Premium';
9814
9821
  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 +9823,7 @@ export class WebPlayer extends BasePlayer {
9816
9823
  menuHTML += `<div class="uvf-settings-option quality-option ${isActive}" data-quality="${quality.value}">${quality.label}</div>`;
9817
9824
  }
9818
9825
  });
9819
-
9826
+
9820
9827
  menuHTML += `</div></div>`;
9821
9828
  }
9822
9829
 
@@ -9824,7 +9831,7 @@ export class WebPlayer extends BasePlayer {
9824
9831
  if (this.settingsConfig.subtitles && this.availableSubtitles.length > 0) {
9825
9832
  const currentSubtitle = this.availableSubtitles.find(s => s.value === this.currentSubtitle);
9826
9833
  const currentSubtitleLabel = currentSubtitle ? currentSubtitle.label : 'Off';
9827
-
9834
+
9828
9835
  menuHTML += `
9829
9836
  <div class="uvf-accordion-item">
9830
9837
  <div class="uvf-accordion-header" data-section="subtitles">
@@ -9840,18 +9847,18 @@ export class WebPlayer extends BasePlayer {
9840
9847
  <div class="uvf-accordion-arrow">▼</div>
9841
9848
  </div>
9842
9849
  <div class="uvf-accordion-content" data-section="subtitles">`;
9843
-
9850
+
9844
9851
  this.availableSubtitles.forEach(subtitle => {
9845
9852
  const isActive = subtitle.value === this.currentSubtitle ? 'active' : '';
9846
9853
  menuHTML += `<div class="uvf-settings-option subtitle-option ${isActive}" data-subtitle="${subtitle.value}">${subtitle.label}</div>`;
9847
9854
  });
9848
-
9855
+
9849
9856
  menuHTML += `</div></div>`;
9850
9857
  }
9851
-
9858
+
9852
9859
  // Close accordion container
9853
9860
  menuHTML += '</div>';
9854
-
9861
+
9855
9862
  // If no sections are enabled or available, show a message
9856
9863
  if (menuHTML === '<div class="uvf-settings-accordion"></div>') {
9857
9864
  menuHTML = '<div class="uvf-settings-accordion"><div class="uvf-settings-empty">No settings available</div></div>';
@@ -9859,7 +9866,7 @@ export class WebPlayer extends BasePlayer {
9859
9866
 
9860
9867
  this.debugLog('Generated menu HTML length:', menuHTML.length);
9861
9868
  this.debugLog('Generated menu HTML content:', menuHTML.substring(0, 200) + (menuHTML.length > 200 ? '...' : ''));
9862
-
9869
+
9863
9870
  settingsMenu.innerHTML = menuHTML;
9864
9871
  this.debugLog('Settings menu HTML set successfully');
9865
9872
 
@@ -9906,7 +9913,7 @@ export class WebPlayer extends BasePlayer {
9906
9913
  // Native video - add common resolutions based on current resolution
9907
9914
  const height = this.video.videoHeight;
9908
9915
  const commonQualities = [2160, 1440, 1080, 720, 480, 360];
9909
-
9916
+
9910
9917
  commonQualities.forEach(quality => {
9911
9918
  if (quality <= height) {
9912
9919
  detectedQualities.push({
@@ -9917,46 +9924,46 @@ export class WebPlayer extends BasePlayer {
9917
9924
  }
9918
9925
  });
9919
9926
  }
9920
-
9927
+
9921
9928
  // Apply quality filter if configured
9922
9929
  if (this.qualityFilter) {
9923
9930
  detectedQualities = this.applyQualityFilter(detectedQualities);
9924
9931
  }
9925
-
9932
+
9926
9933
  // Add filtered qualities to available list
9927
9934
  this.availableQualities.push(...detectedQualities);
9928
9935
  }
9929
-
9936
+
9930
9937
  /**
9931
9938
  * Apply quality filter to detected qualities
9932
9939
  */
9933
9940
  private applyQualityFilter(qualities: Array<{ value: string; label: string; height?: number }>): Array<{ value: string; label: string; height?: number }> {
9934
9941
  let filtered = [...qualities];
9935
9942
  const filter = this.qualityFilter;
9936
-
9943
+
9937
9944
  // Filter by allowed heights
9938
9945
  if (filter.allowedHeights && filter.allowedHeights.length > 0) {
9939
9946
  filtered = filtered.filter(q => q.height && filter.allowedHeights.includes(q.height));
9940
9947
  }
9941
-
9948
+
9942
9949
  // Filter by allowed labels
9943
9950
  if (filter.allowedLabels && filter.allowedLabels.length > 0) {
9944
9951
  filtered = filtered.filter(q => filter.allowedLabels.includes(q.label));
9945
9952
  }
9946
-
9953
+
9947
9954
  // Filter by minimum height
9948
9955
  if (filter.minHeight !== undefined) {
9949
9956
  filtered = filtered.filter(q => q.height && q.height >= filter.minHeight);
9950
9957
  }
9951
-
9958
+
9952
9959
  // Filter by maximum height
9953
9960
  if (filter.maxHeight !== undefined) {
9954
9961
  filtered = filtered.filter(q => q.height && q.height <= filter.maxHeight);
9955
9962
  }
9956
-
9963
+
9957
9964
  return filtered;
9958
9965
  }
9959
-
9966
+
9960
9967
  /**
9961
9968
  * Set quality filter (can be called at runtime)
9962
9969
  */
@@ -9967,7 +9974,7 @@ export class WebPlayer extends BasePlayer {
9967
9974
  this.updateSettingsMenu();
9968
9975
  }
9969
9976
  }
9970
-
9977
+
9971
9978
  /**
9972
9979
  * Set ad playing state (called by Google Ads Manager)
9973
9980
  */
@@ -9975,14 +9982,14 @@ export class WebPlayer extends BasePlayer {
9975
9982
  this.isAdPlaying = isPlaying;
9976
9983
  this.debugLog('Ad playing state:', isPlaying);
9977
9984
  }
9978
-
9985
+
9979
9986
  /**
9980
9987
  * Check if ad is currently playing
9981
9988
  */
9982
9989
  public isAdCurrentlyPlaying(): boolean {
9983
9990
  return this.isAdPlaying;
9984
9991
  }
9985
-
9992
+
9986
9993
  /**
9987
9994
  * Check if a quality level is premium
9988
9995
  */
@@ -9990,61 +9997,61 @@ export class WebPlayer extends BasePlayer {
9990
9997
  if (!this.premiumQualities || !this.premiumQualities.enabled) {
9991
9998
  return false;
9992
9999
  }
9993
-
10000
+
9994
10001
  const config = this.premiumQualities;
9995
10002
  const height = quality.height || 0;
9996
10003
  const label = quality.label || '';
9997
-
10004
+
9998
10005
  // Check by specific heights
9999
10006
  if (config.requiredHeights && config.requiredHeights.length > 0) {
10000
10007
  if (config.requiredHeights.includes(height)) return true;
10001
10008
  }
10002
-
10009
+
10003
10010
  // Check by specific labels
10004
10011
  if (config.requiredLabels && config.requiredLabels.length > 0) {
10005
10012
  if (config.requiredLabels.includes(label)) return true;
10006
10013
  }
10007
-
10014
+
10008
10015
  // Check by minimum height threshold
10009
10016
  if (config.minPremiumHeight !== undefined) {
10010
10017
  if (height >= config.minPremiumHeight) return true;
10011
10018
  }
10012
-
10019
+
10013
10020
  return false;
10014
10021
  }
10015
-
10022
+
10016
10023
  /**
10017
10024
  * Check if current user is premium
10018
10025
  */
10019
10026
  private isPremiumUser(): boolean {
10020
10027
  return this.premiumQualities?.isPremiumUser === true;
10021
10028
  }
10022
-
10029
+
10023
10030
  /**
10024
10031
  * Handle click on locked premium quality
10025
10032
  */
10026
10033
  private handlePremiumQualityClick(element: HTMLElement): void {
10027
10034
  const height = parseInt(element.dataset.qualityHeight || '0');
10028
10035
  const label = element.dataset.qualityLabel || '';
10029
-
10036
+
10030
10037
  this.debugLog(`Premium quality clicked: ${label} (${height}p)`);
10031
-
10038
+
10032
10039
  // Call custom callback if provided
10033
10040
  if (this.premiumQualities?.onPremiumQualityClick) {
10034
10041
  this.premiumQualities.onPremiumQualityClick({ height, label });
10035
10042
  }
10036
-
10043
+
10037
10044
  // Show notification
10038
10045
  const message = this.premiumQualities?.premiumLabel || 'Premium';
10039
10046
  this.showNotification(`${label} requires ${message.replace('🔒 ', '')}`);
10040
-
10047
+
10041
10048
  // Redirect to unlock URL if provided
10042
10049
  if (this.premiumQualities?.unlockUrl) {
10043
10050
  setTimeout(() => {
10044
10051
  window.location.href = this.premiumQualities.unlockUrl;
10045
10052
  }, 1500);
10046
10053
  }
10047
-
10054
+
10048
10055
  // Close settings menu
10049
10056
  this.hideSettingsMenu();
10050
10057
  }
@@ -10101,10 +10108,10 @@ export class WebPlayer extends BasePlayer {
10101
10108
  header.addEventListener('click', (e) => {
10102
10109
  e.preventDefault();
10103
10110
  e.stopPropagation();
10104
-
10111
+
10105
10112
  const accordionItem = header.parentElement;
10106
10113
  const section = header.getAttribute('data-section');
10107
-
10114
+
10108
10115
  if (accordionItem && section) {
10109
10116
  this.toggleAccordionSection(accordionItem, section);
10110
10117
  }
@@ -10127,13 +10134,13 @@ export class WebPlayer extends BasePlayer {
10127
10134
  e.stopPropagation();
10128
10135
  const target = e.target as HTMLElement;
10129
10136
  const quality = target.dataset.quality || 'auto';
10130
-
10137
+
10131
10138
  // Check if this is a locked premium quality
10132
10139
  if (target.classList.contains('premium-locked')) {
10133
10140
  this.handlePremiumQualityClick(target);
10134
10141
  return;
10135
10142
  }
10136
-
10143
+
10137
10144
  this.setQualityFromSettings(quality);
10138
10145
  this.updateAccordionAfterSelection('quality');
10139
10146
  });
@@ -10149,19 +10156,19 @@ export class WebPlayer extends BasePlayer {
10149
10156
  });
10150
10157
  });
10151
10158
  }
10152
-
10159
+
10153
10160
  /**
10154
10161
  * Toggle accordion section
10155
10162
  */
10156
10163
  private toggleAccordionSection(accordionItem: Element, section: string): void {
10157
10164
  const isExpanded = accordionItem.classList.contains('expanded');
10158
-
10165
+
10159
10166
  // If clicking the same section that's already expanded, just close it
10160
10167
  if (isExpanded) {
10161
10168
  accordionItem.classList.remove('expanded');
10162
10169
  return;
10163
10170
  }
10164
-
10171
+
10165
10172
  // Otherwise, close all sections and open the clicked one
10166
10173
  const settingsMenu = document.getElementById('uvf-settings-menu');
10167
10174
  if (settingsMenu) {
@@ -10169,30 +10176,30 @@ export class WebPlayer extends BasePlayer {
10169
10176
  item.classList.remove('expanded');
10170
10177
  });
10171
10178
  }
10172
-
10179
+
10173
10180
  // Open the clicked section
10174
10181
  accordionItem.classList.add('expanded');
10175
10182
  }
10176
-
10183
+
10177
10184
  /**
10178
10185
  * Hide settings menu with proper styling
10179
10186
  */
10180
10187
  private hideSettingsMenu(): void {
10181
10188
  const settingsMenu = document.getElementById('uvf-settings-menu');
10182
10189
  if (!settingsMenu) return;
10183
-
10190
+
10184
10191
  settingsMenu.classList.remove('active');
10185
-
10192
+
10186
10193
  // Apply fallback styles to ensure menu is hidden
10187
10194
  settingsMenu.style.display = 'none';
10188
10195
  settingsMenu.style.visibility = 'hidden';
10189
10196
  settingsMenu.style.opacity = '0';
10190
-
10197
+
10191
10198
  // Also close any expanded accordions
10192
10199
  settingsMenu.querySelectorAll('.uvf-accordion-item.expanded').forEach(item => {
10193
10200
  item.classList.remove('expanded');
10194
10201
  });
10195
-
10202
+
10196
10203
  this.debugLog('Settings menu hidden via hideSettingsMenu()');
10197
10204
  }
10198
10205
 
@@ -10207,7 +10214,7 @@ export class WebPlayer extends BasePlayer {
10207
10214
  item.classList.remove('expanded');
10208
10215
  });
10209
10216
  }
10210
-
10217
+
10211
10218
  // Close settings menu after selection on all devices
10212
10219
  setTimeout(() => {
10213
10220
  this.hideSettingsMenu();
@@ -10246,7 +10253,7 @@ export class WebPlayer extends BasePlayer {
10246
10253
  */
10247
10254
  private setQualityFromSettings(quality: string): void {
10248
10255
  this.currentQuality = quality;
10249
-
10256
+
10250
10257
  if (quality === 'auto') {
10251
10258
  // Enable auto quality with filter consideration
10252
10259
  if (this.hls) {
@@ -10267,7 +10274,7 @@ export class WebPlayer extends BasePlayer {
10267
10274
  } else {
10268
10275
  // Set specific quality
10269
10276
  const qualityIndex = parseInt(quality);
10270
-
10277
+
10271
10278
  if (this.hls && !isNaN(qualityIndex) && this.hls.levels[qualityIndex]) {
10272
10279
  this.hls.currentLevel = qualityIndex;
10273
10280
  } else if (this.dash && !isNaN(qualityIndex)) {
@@ -10275,38 +10282,38 @@ export class WebPlayer extends BasePlayer {
10275
10282
  this.dash.setQualityFor('video', qualityIndex);
10276
10283
  }
10277
10284
  }
10278
-
10285
+
10279
10286
  this.debugLog(`Quality set to ${quality}`);
10280
10287
  }
10281
-
10288
+
10282
10289
  /**
10283
10290
  * Apply quality filter to HLS auto quality selection
10284
10291
  */
10285
10292
  private applyHLSQualityFilter(): void {
10286
10293
  if (!this.hls || !this.hls.levels) return;
10287
-
10294
+
10288
10295
  const allowedLevels: number[] = [];
10289
-
10296
+
10290
10297
  this.hls.levels.forEach((level: any, index: number) => {
10291
10298
  let allowed = true;
10292
-
10299
+
10293
10300
  // Apply quality filter if exists
10294
10301
  if (this.qualityFilter) {
10295
10302
  const filter = this.qualityFilter;
10296
-
10303
+
10297
10304
  if (filter.allowedHeights && filter.allowedHeights.length > 0) {
10298
10305
  allowed = allowed && filter.allowedHeights.includes(level.height);
10299
10306
  }
10300
-
10307
+
10301
10308
  if (filter.minHeight !== undefined) {
10302
10309
  allowed = allowed && level.height >= filter.minHeight;
10303
10310
  }
10304
-
10311
+
10305
10312
  if (filter.maxHeight !== undefined) {
10306
10313
  allowed = allowed && level.height <= filter.maxHeight;
10307
10314
  }
10308
10315
  }
10309
-
10316
+
10310
10317
  // Apply premium quality restrictions for non-premium users
10311
10318
  if (this.premiumQualities && this.premiumQualities.enabled && !this.isPremiumUser()) {
10312
10319
  const isPremium = this.isQualityPremium({ height: level.height, label: `${level.height}p` });
@@ -10314,24 +10321,24 @@ export class WebPlayer extends BasePlayer {
10314
10321
  allowed = false; // Exclude premium qualities for non-premium users
10315
10322
  }
10316
10323
  }
10317
-
10324
+
10318
10325
  if (allowed) {
10319
10326
  allowedLevels.push(index);
10320
10327
  }
10321
10328
  });
10322
-
10329
+
10323
10330
  // Set max and min auto level based on filter and premium restrictions
10324
10331
  if (allowedLevels.length > 0) {
10325
10332
  // Set the maximum level cap
10326
10333
  this.hls.autoLevelCapping = Math.max(...allowedLevels);
10327
-
10334
+
10328
10335
  // Enable auto quality
10329
10336
  this.hls.currentLevel = -1;
10330
-
10337
+
10331
10338
  // For minimum level, we need to use a different approach
10332
10339
  // Monitor level changes and prevent switching below minimum
10333
10340
  const minLevel = Math.min(...allowedLevels);
10334
-
10341
+
10335
10342
  // Set up level switching listener to enforce minimum
10336
10343
  const enforceLevelConstraints = () => {
10337
10344
  if (this.hls && this.hls.currentLevel !== -1 && this.hls.currentLevel < minLevel) {
@@ -10339,52 +10346,52 @@ export class WebPlayer extends BasePlayer {
10339
10346
  this.hls.currentLevel = minLevel;
10340
10347
  }
10341
10348
  };
10342
-
10349
+
10343
10350
  // Remove old listener if exists
10344
10351
  if (this.hls.listeners('hlsLevelSwitching').length > 0) {
10345
10352
  this.hls.off('hlsLevelSwitching', enforceLevelConstraints);
10346
10353
  }
10347
-
10354
+
10348
10355
  // Add listener to enforce constraints
10349
10356
  this.hls.on('hlsLevelSwitching', enforceLevelConstraints);
10350
-
10357
+
10351
10358
  this.debugLog(`Auto quality limited to levels: ${allowedLevels.join(', ')} (min: ${minLevel}, max: ${Math.max(...allowedLevels)})`);
10352
10359
  } else {
10353
10360
  // No allowed levels - just use auto
10354
10361
  this.hls.currentLevel = -1;
10355
10362
  }
10356
10363
  }
10357
-
10364
+
10358
10365
  /**
10359
10366
  * Apply quality filter to DASH auto quality selection
10360
10367
  */
10361
10368
  private applyDASHQualityFilter(): void {
10362
10369
  if (!this.dash) return;
10363
-
10370
+
10364
10371
  try {
10365
10372
  const videoQualities = this.dash.getBitrateInfoListFor('video');
10366
10373
  const allowedIndices: number[] = [];
10367
-
10374
+
10368
10375
  videoQualities.forEach((quality: any, index: number) => {
10369
10376
  let allowed = true;
10370
-
10377
+
10371
10378
  // Apply quality filter if exists
10372
10379
  if (this.qualityFilter) {
10373
10380
  const filter = this.qualityFilter;
10374
-
10381
+
10375
10382
  if (filter.allowedHeights && filter.allowedHeights.length > 0) {
10376
10383
  allowed = allowed && filter.allowedHeights.includes(quality.height);
10377
10384
  }
10378
-
10385
+
10379
10386
  if (filter.minHeight !== undefined) {
10380
10387
  allowed = allowed && quality.height >= filter.minHeight;
10381
10388
  }
10382
-
10389
+
10383
10390
  if (filter.maxHeight !== undefined) {
10384
10391
  allowed = allowed && quality.height <= filter.maxHeight;
10385
10392
  }
10386
10393
  }
10387
-
10394
+
10388
10395
  // Apply premium quality restrictions for non-premium users
10389
10396
  if (this.premiumQualities && this.premiumQualities.enabled && !this.isPremiumUser()) {
10390
10397
  const isPremium = this.isQualityPremium({ height: quality.height, label: `${quality.height}p` });
@@ -10392,12 +10399,12 @@ export class WebPlayer extends BasePlayer {
10392
10399
  allowed = false; // Exclude premium qualities for non-premium users
10393
10400
  }
10394
10401
  }
10395
-
10402
+
10396
10403
  if (allowed) {
10397
10404
  allowedIndices.push(index);
10398
10405
  }
10399
10406
  });
10400
-
10407
+
10401
10408
  // Set quality bounds for DASH
10402
10409
  if (allowedIndices.length > 0) {
10403
10410
  const settings = {
@@ -10422,7 +10429,7 @@ export class WebPlayer extends BasePlayer {
10422
10429
  */
10423
10430
  private setSubtitle(subtitle: string): void {
10424
10431
  this.currentSubtitle = subtitle;
10425
-
10432
+
10426
10433
  if (this.youtubePlayer && this.youtubePlayerReady) {
10427
10434
  // YouTube subtitles - limited control
10428
10435
  if (subtitle === 'off') {
@@ -10441,7 +10448,7 @@ export class WebPlayer extends BasePlayer {
10441
10448
  this.showShortcutIndicator('Subtitle language selection not available for YouTube');
10442
10449
  return;
10443
10450
  }
10444
-
10451
+
10445
10452
  if (subtitle === 'off') {
10446
10453
  // Disable all subtitles
10447
10454
  if (this.video?.textTracks) {
@@ -10467,7 +10474,7 @@ export class WebPlayer extends BasePlayer {
10467
10474
  });
10468
10475
  }
10469
10476
  }
10470
-
10477
+
10471
10478
  this.debugLog(`Subtitle set to ${subtitle}`);
10472
10479
  }
10473
10480
 
@@ -10485,11 +10492,11 @@ export class WebPlayer extends BasePlayer {
10485
10492
  if (tid) ids = [tid];
10486
10493
  }
10487
10494
  if (typeof media.setActiveTracks === 'function') {
10488
- media.setActiveTracks(ids, () => {}, () => {});
10495
+ media.setActiveTracks(ids, () => { }, () => { });
10489
10496
  } else if (typeof media.setActiveTrackIds === 'function') {
10490
10497
  media.setActiveTrackIds(ids);
10491
10498
  }
10492
- } catch (_) {}
10499
+ } catch (_) { }
10493
10500
  }
10494
10501
 
10495
10502
  private onCastButtonClick(): void {
@@ -10498,20 +10505,20 @@ export class WebPlayer extends BasePlayer {
10498
10505
  this.showAirPlayPicker();
10499
10506
  return;
10500
10507
  }
10501
-
10508
+
10502
10509
  // Google Cast for non-iOS devices
10503
10510
  try {
10504
10511
  const castNs = (window as any).cast;
10505
10512
  if (this.isCasting && castNs && castNs.framework) {
10506
10513
  const ctx = castNs.framework.CastContext.getInstance();
10507
- ctx.requestSession().catch(() => {});
10514
+ ctx.requestSession().catch(() => { });
10508
10515
  return;
10509
10516
  }
10510
- } catch (_) {}
10517
+ } catch (_) { }
10511
10518
  // Not casting yet
10512
10519
  this.initCast();
10513
10520
  }
10514
-
10521
+
10515
10522
  /**
10516
10523
  * Show AirPlay picker for iOS devices
10517
10524
  */
@@ -10520,7 +10527,7 @@ export class WebPlayer extends BasePlayer {
10520
10527
  this.showNotification('Video not ready');
10521
10528
  return;
10522
10529
  }
10523
-
10530
+
10524
10531
  // Check if AirPlay is supported
10525
10532
  const videoElement = this.video as any;
10526
10533
  if (typeof videoElement.webkitShowPlaybackTargetPicker === 'function') {
@@ -10544,7 +10551,7 @@ export class WebPlayer extends BasePlayer {
10544
10551
  const ctx = castNs.framework.CastContext.getInstance();
10545
10552
  const sess = ctx.getCurrentSession && ctx.getCurrentSession();
10546
10553
  if (sess) {
10547
- try { sess.endSession(true); } catch (_) {}
10554
+ try { sess.endSession(true); } catch (_) { }
10548
10555
  this.disableCastRemoteControl();
10549
10556
  this.showNotification('Stopped casting');
10550
10557
  } else {
@@ -10583,9 +10590,9 @@ export class WebPlayer extends BasePlayer {
10583
10590
  const url = this.source?.url || this.video?.src || '';
10584
10591
  const u = (url || '').toLowerCase();
10585
10592
  const contentType = u.includes('.m3u8') ? 'application/x-mpegurl'
10586
- : u.includes('.mpd') ? 'application/dash+xml'
10587
- : u.includes('.webm') ? 'video/webm'
10588
- : 'video/mp4';
10593
+ : u.includes('.mpd') ? 'application/dash+xml'
10594
+ : u.includes('.webm') ? 'video/webm'
10595
+ : 'video/mp4';
10589
10596
 
10590
10597
  const chromeNs = (window as any).chrome;
10591
10598
  const mediaInfo = new chromeNs.cast.media.MediaInfo(url, contentType);
@@ -10594,7 +10601,7 @@ export class WebPlayer extends BasePlayer {
10594
10601
  const md = new chromeNs.cast.media.GenericMediaMetadata();
10595
10602
  md.title = this.source?.metadata?.title || (this.video?.currentSrc ? this.video!.currentSrc.split('/').slice(-1)[0] : 'Web Player');
10596
10603
  mediaInfo.metadata = md;
10597
- } catch (_) {}
10604
+ } catch (_) { }
10598
10605
 
10599
10606
  // Subtitle tracks -> Cast tracks mapping
10600
10607
  const castTracks: any[] = [];
@@ -10610,7 +10617,7 @@ export class WebPlayer extends BasePlayer {
10610
10617
  let nextId = 1;
10611
10618
  for (let i = 0; i < this.subtitles.length; i++) {
10612
10619
  const t = this.subtitles[i];
10613
- const key = t.label || t.language || `Track ${i+1}`;
10620
+ const key = t.label || t.language || `Track ${i + 1}`;
10614
10621
  try {
10615
10622
  const track = new chromeNs.cast.media.Track(nextId, chromeNs.cast.media.TrackType.TEXT);
10616
10623
  track.trackContentId = t.url;
@@ -10622,7 +10629,7 @@ export class WebPlayer extends BasePlayer {
10622
10629
  castTracks.push(track);
10623
10630
  this._castTrackIdByKey[key] = nextId;
10624
10631
  nextId++;
10625
- } catch (_) {}
10632
+ } catch (_) { }
10626
10633
  }
10627
10634
  }
10628
10635
  if (castTracks.length > 0) {
@@ -10635,12 +10642,12 @@ export class WebPlayer extends BasePlayer {
10635
10642
  style.edgeColor = '#000000FF';
10636
10643
  style.fontScale = 1.0;
10637
10644
  mediaInfo.textTrackStyle = style;
10638
- } catch (_) {}
10645
+ } catch (_) { }
10639
10646
  }
10640
10647
 
10641
10648
  const request = new chromeNs.cast.media.LoadRequest(mediaInfo);
10642
10649
  request.autoplay = true;
10643
- try { request.currentTime = Math.max(0, Math.floor(this.video?.currentTime || 0)); } catch (_) {}
10650
+ try { request.currentTime = Math.max(0, Math.floor(this.video?.currentTime || 0)); } catch (_) { }
10644
10651
  // Determine selected subtitle key from currentSubtitleIndex
10645
10652
  const currentIdx = this.currentSubtitleIndex;
10646
10653
  this.selectedSubtitleKey = (currentIdx >= 0 && this.subtitles[currentIdx]) ? (this.subtitles[currentIdx].label || this.subtitles[currentIdx].language) : 'off';
@@ -10657,7 +10664,7 @@ export class WebPlayer extends BasePlayer {
10657
10664
  this.showNotification('Cast failed');
10658
10665
  }
10659
10666
  }
10660
-
10667
+
10661
10668
  private async shareVideo(): Promise<void> {
10662
10669
  // Get share configuration
10663
10670
  const shareConfig = this.config.share;
@@ -10705,7 +10712,7 @@ export class WebPlayer extends BasePlayer {
10705
10712
  this.showNotification('Share failed');
10706
10713
  }
10707
10714
  }
10708
-
10715
+
10709
10716
  /**
10710
10717
  * Check if text is truncated and needs tooltip
10711
10718
  */
@@ -10727,9 +10734,9 @@ export class WebPlayer extends BasePlayer {
10727
10734
  const tooltip = document.createElement('div');
10728
10735
  tooltip.className = 'uvf-text-tooltip';
10729
10736
  tooltip.textContent = text;
10730
-
10737
+
10731
10738
  element.appendChild(tooltip);
10732
-
10739
+
10733
10740
  // Show tooltip with delay
10734
10741
  setTimeout(() => {
10735
10742
  tooltip.classList.add('show');
@@ -10765,11 +10772,11 @@ export class WebPlayer extends BasePlayer {
10765
10772
  this.showTextTooltip(titleElement, titleText);
10766
10773
  }
10767
10774
  });
10768
-
10775
+
10769
10776
  titleElement.addEventListener('mouseleave', () => {
10770
10777
  this.hideTextTooltip(titleElement);
10771
10778
  });
10772
-
10779
+
10773
10780
  // Touch support for mobile
10774
10781
  titleElement.addEventListener('touchstart', () => {
10775
10782
  const titleText = (this.source?.metadata?.title || '').toString().trim();
@@ -10790,11 +10797,11 @@ export class WebPlayer extends BasePlayer {
10790
10797
  this.showTextTooltip(descElement, descText);
10791
10798
  }
10792
10799
  });
10793
-
10800
+
10794
10801
  descElement.addEventListener('mouseleave', () => {
10795
10802
  this.hideTextTooltip(descElement);
10796
10803
  });
10797
-
10804
+
10798
10805
  // Touch support for mobile
10799
10806
  descElement.addEventListener('touchstart', () => {
10800
10807
  const descText = (this.source?.metadata?.description || '').toString().trim();
@@ -10817,7 +10824,7 @@ export class WebPlayer extends BasePlayer {
10817
10824
  if (words.length <= maxWords) {
10818
10825
  return { truncated: text, needsTooltip: false };
10819
10826
  }
10820
-
10827
+
10821
10828
  const truncated = words.slice(0, maxWords).join(' ') + '...';
10822
10829
  return { truncated, needsTooltip: true };
10823
10830
  }
@@ -10828,10 +10835,10 @@ export class WebPlayer extends BasePlayer {
10828
10835
  private applySmartTextDisplay(titleEl: HTMLElement | null, descEl: HTMLElement | null, titleText: string, descText: string): void {
10829
10836
  const isDesktop = window.innerWidth >= 1024;
10830
10837
  const isMobile = window.innerWidth < 768;
10831
-
10838
+
10832
10839
  if (titleEl && titleText) {
10833
10840
  const wordCount = titleText.split(' ').length;
10834
-
10841
+
10835
10842
  if (isDesktop && wordCount > 8 && wordCount <= 15) {
10836
10843
  // Use multiline for moderately long titles on desktop
10837
10844
  titleEl.classList.add('multiline');
@@ -10847,10 +10854,10 @@ export class WebPlayer extends BasePlayer {
10847
10854
  titleEl.classList.remove('multiline');
10848
10855
  }
10849
10856
  }
10850
-
10857
+
10851
10858
  if (descEl && descText) {
10852
10859
  const wordCount = descText.split(' ').length;
10853
-
10860
+
10854
10861
  if (isDesktop && wordCount > 15 && wordCount <= 25) {
10855
10862
  // Use multiline for moderately long descriptions on desktop
10856
10863
  descEl.classList.add('multiline');
@@ -10906,7 +10913,7 @@ export class WebPlayer extends BasePlayer {
10906
10913
  setTimeout(() => {
10907
10914
  this.setupTextTooltips();
10908
10915
  }, 100); // Small delay to ensure elements are rendered
10909
-
10916
+
10910
10917
  } catch (_) { /* ignore */ }
10911
10918
  }
10912
10919
 
@@ -10921,26 +10928,26 @@ export class WebPlayer extends BasePlayer {
10921
10928
  private canPlayVideo(): boolean {
10922
10929
  const freeDuration = Number(this.config.freeDuration || 0);
10923
10930
  const currentTime = this.video?.currentTime || 0;
10924
-
10931
+
10925
10932
  // Always allow if no free duration limit is set
10926
10933
  if (freeDuration <= 0) return true;
10927
-
10934
+
10928
10935
  // Allow if payment was successful
10929
10936
  if (this.paymentSuccessful) return true;
10930
-
10937
+
10931
10938
  // Allow if within free preview duration
10932
10939
  if (currentTime < freeDuration) return true;
10933
-
10940
+
10934
10941
  // Check if paywall controller indicates user is authenticated
10935
- if (this.paywallController &&
10936
- typeof this.paywallController.isAuthenticated === 'function') {
10942
+ if (this.paywallController &&
10943
+ typeof this.paywallController.isAuthenticated === 'function') {
10937
10944
  const isAuth = this.paywallController.isAuthenticated();
10938
10945
  if (isAuth) {
10939
10946
  this.paymentSuccessful = true;
10940
10947
  return true;
10941
10948
  }
10942
10949
  }
10943
-
10950
+
10944
10951
  return false;
10945
10952
  }
10946
10953
 
@@ -10949,17 +10956,17 @@ export class WebPlayer extends BasePlayer {
10949
10956
  */
10950
10957
  private enforcePaywallSecurity(): void {
10951
10958
  this.debugLog('Enforcing paywall security');
10952
-
10959
+
10953
10960
  // Pause video immediately
10954
10961
  try {
10955
10962
  if (this.video && !this.video.paused) {
10956
10963
  this.video.pause();
10957
10964
  }
10958
- } catch (_) {}
10959
-
10965
+ } catch (_) { }
10966
+
10960
10967
  // Activate paywall state
10961
10968
  this.isPaywallActive = true;
10962
-
10969
+
10963
10970
  // Show paywall overlay
10964
10971
  if (this.paywallController) {
10965
10972
  try {
@@ -10968,7 +10975,7 @@ export class WebPlayer extends BasePlayer {
10968
10975
  this.debugError('Error showing paywall overlay:', error);
10969
10976
  }
10970
10977
  }
10971
-
10978
+
10972
10979
  // Start monitoring for overlay tampering
10973
10980
  this.startOverlayMonitoring();
10974
10981
  }
@@ -10978,15 +10985,15 @@ export class WebPlayer extends BasePlayer {
10978
10985
  */
10979
10986
  private startOverlayMonitoring(): void {
10980
10987
  if (!this.playerWrapper || this.paymentSuccessful) return;
10981
-
10988
+
10982
10989
  // Clear existing monitor
10983
10990
  if (this.authValidationInterval) {
10984
10991
  clearInterval(this.authValidationInterval);
10985
10992
  this.authValidationInterval = null;
10986
10993
  }
10987
-
10994
+
10988
10995
  this.debugLog('Starting overlay monitoring - payment successful:', this.paymentSuccessful, 'paywall active:', this.isPaywallActive);
10989
-
10996
+
10990
10997
  // Monitor every 1000ms (less aggressive than before)
10991
10998
  this.authValidationInterval = setInterval(() => {
10992
10999
  // First check: stop monitoring if payment successful or paywall inactive
@@ -10998,7 +11005,7 @@ export class WebPlayer extends BasePlayer {
10998
11005
  }
10999
11006
  return;
11000
11007
  }
11001
-
11008
+
11002
11009
  // Double-check payment success before enforcing security
11003
11010
  if (this.paymentSuccessful) {
11004
11011
  this.debugLog('Payment successful detected during monitoring, stopping');
@@ -11008,20 +11015,20 @@ export class WebPlayer extends BasePlayer {
11008
11015
  }
11009
11016
  return;
11010
11017
  }
11011
-
11018
+
11012
11019
  // Check for overlay presence
11013
11020
  const paywallOverlays = this.playerWrapper!.querySelectorAll('.uvf-paywall-overlay, .uvf-auth-overlay');
11014
11021
  const visibleOverlays = Array.from(paywallOverlays).filter(overlay => {
11015
11022
  const element = overlay as HTMLElement;
11016
- return element.style.display !== 'none' &&
11017
- element.offsetParent !== null &&
11018
- window.getComputedStyle(element).visibility !== 'hidden';
11023
+ return element.style.display !== 'none' &&
11024
+ element.offsetParent !== null &&
11025
+ window.getComputedStyle(element).visibility !== 'hidden';
11019
11026
  });
11020
-
11027
+
11021
11028
  if (visibleOverlays.length === 0) {
11022
11029
  this.overlayRemovalAttempts++;
11023
11030
  this.debugWarn(`Overlay removal attempt detected (${this.overlayRemovalAttempts}/${this.maxOverlayRemovalAttempts})`);
11024
-
11031
+
11025
11032
  // Final check before taking action - ensure payment wasn't just completed
11026
11033
  if (this.paymentSuccessful) {
11027
11034
  this.debugLog('Payment successful detected, ignoring overlay removal');
@@ -11031,7 +11038,7 @@ export class WebPlayer extends BasePlayer {
11031
11038
  }
11032
11039
  return;
11033
11040
  }
11034
-
11041
+
11035
11042
  if (this.overlayRemovalAttempts >= this.maxOverlayRemovalAttempts) {
11036
11043
  this.handleSecurityViolation();
11037
11044
  } else {
@@ -11039,7 +11046,7 @@ export class WebPlayer extends BasePlayer {
11039
11046
  this.enforcePaywallSecurity();
11040
11047
  }
11041
11048
  }
11042
-
11049
+
11043
11050
  // Additional check: ensure video is paused if not authenticated
11044
11051
  if (this.video && !this.video.paused && !this.paymentSuccessful) {
11045
11052
  this.debugWarn('Unauthorized playback detected, pausing video');
@@ -11049,7 +11056,7 @@ export class WebPlayer extends BasePlayer {
11049
11056
  if (freeDuration > 0 && isFinite(freeDuration)) {
11050
11057
  this.safeSetCurrentTime(freeDuration - 1);
11051
11058
  }
11052
- } catch (_) {}
11059
+ } catch (_) { }
11053
11060
  }
11054
11061
  }, 1000);
11055
11062
  }
@@ -11059,7 +11066,7 @@ export class WebPlayer extends BasePlayer {
11059
11066
  */
11060
11067
  private handleSecurityViolation(): void {
11061
11068
  this.debugError('Security violation detected - disabling video');
11062
-
11069
+
11063
11070
  // Disable video completely
11064
11071
  if (this.video) {
11065
11072
  this.video.pause();
@@ -11067,10 +11074,10 @@ export class WebPlayer extends BasePlayer {
11067
11074
  this.video.src = ''; // Clear video source
11068
11075
  this.video.style.display = 'none';
11069
11076
  }
11070
-
11077
+
11071
11078
  // Show security violation message
11072
11079
  this.showSecurityViolationMessage();
11073
-
11080
+
11074
11081
  // Clear monitoring interval
11075
11082
  if (this.authValidationInterval) {
11076
11083
  clearInterval(this.authValidationInterval);
@@ -11082,10 +11089,10 @@ export class WebPlayer extends BasePlayer {
11082
11089
  */
11083
11090
  private showSecurityViolationMessage(): void {
11084
11091
  if (!this.playerWrapper) return;
11085
-
11092
+
11086
11093
  // Clear existing content
11087
11094
  this.playerWrapper.innerHTML = '';
11088
-
11095
+
11089
11096
  // Create security violation overlay
11090
11097
  const securityOverlay = document.createElement('div');
11091
11098
  securityOverlay.style.cssText = `
@@ -11101,7 +11108,7 @@ export class WebPlayer extends BasePlayer {
11101
11108
  text-align: center;
11102
11109
  padding: 40px;
11103
11110
  `;
11104
-
11111
+
11105
11112
  const messageContainer = document.createElement('div');
11106
11113
  messageContainer.innerHTML = `
11107
11114
  <div style="font-size: 24px; font-weight: bold; margin-bottom: 16px; color: #ff6b6b;">
@@ -11124,7 +11131,7 @@ export class WebPlayer extends BasePlayer {
11124
11131
  ">Reload Page</button>
11125
11132
  </div>
11126
11133
  `;
11127
-
11134
+
11128
11135
  securityOverlay.appendChild(messageContainer);
11129
11136
  this.playerWrapper.appendChild(securityOverlay);
11130
11137
  }
@@ -11134,25 +11141,25 @@ export class WebPlayer extends BasePlayer {
11134
11141
  */
11135
11142
  private forceCleanupOverlays(): void {
11136
11143
  this.debugLog('Force cleanup of overlays called');
11137
-
11144
+
11138
11145
  if (!this.playerWrapper) return;
11139
-
11146
+
11140
11147
  // Find and remove all overlay elements
11141
11148
  const overlays = this.playerWrapper.querySelectorAll('.uvf-paywall-overlay, .uvf-auth-overlay');
11142
11149
  overlays.forEach((overlay: Element) => {
11143
11150
  const htmlOverlay = overlay as HTMLElement;
11144
11151
  this.debugLog('Removing overlay:', htmlOverlay.className);
11145
-
11152
+
11146
11153
  // Hide immediately
11147
11154
  htmlOverlay.style.display = 'none';
11148
11155
  htmlOverlay.classList.remove('active');
11149
-
11156
+
11150
11157
  // Remove from DOM
11151
11158
  if (htmlOverlay.parentNode) {
11152
11159
  htmlOverlay.parentNode.removeChild(htmlOverlay);
11153
11160
  }
11154
11161
  });
11155
-
11162
+
11156
11163
  // Also tell paywall controller to clean up
11157
11164
  if (this.paywallController && typeof this.paywallController.destroyOverlays === 'function') {
11158
11165
  this.debugLog('Calling paywallController.destroyOverlays()');