unified-video-framework 1.4.370 → 1.4.371
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +2 -1
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.d.ts.map +1 -1
- package/packages/web/dist/chapters/ChapterManager.js +0 -6
- package/packages/web/dist/chapters/ChapterManager.js.map +1 -1
- package/packages/web/dist/chapters/CreditsButtonController.d.ts +2 -0
- package/packages/web/dist/chapters/CreditsButtonController.d.ts.map +1 -1
- package/packages/web/dist/chapters/CreditsButtonController.js +58 -20
- package/packages/web/dist/chapters/CreditsButtonController.js.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts +8 -0
- package/packages/web/dist/chapters/types/ChapterTypes.d.ts.map +1 -1
- package/packages/web/dist/chapters/types/ChapterTypes.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +715 -714
- package/packages/web/src/chapters/ChapterManager.ts +0 -8
- package/packages/web/src/chapters/CreditsButtonController.ts +72 -22
- package/packages/web/src/chapters/types/ChapterTypes.ts +21 -0
|
@@ -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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1924
|
-
|
|
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
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
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
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
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
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
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
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
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
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
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
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
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
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3396
|
+
|
|
3397
|
+
if (errorMessage.includes('user gesture') ||
|
|
3398
|
+
errorMessage.includes('user activation') ||
|
|
3399
|
+
errorMessage.includes('Permissions check failed')) {
|
|
3400
|
+
|
|
3401
3401
|
// Try using the fullscreen button as a last resort
|
|
3402
3402
|
const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
|
|
3403
3403
|
if (fullscreenBtn && !this.hasTriedButtonFallback) {
|
|
3404
3404
|
this.hasTriedButtonFallback = true;
|
|
3405
3405
|
this.debugLog('Attempting fullscreen via button as fallback');
|
|
3406
|
-
|
|
3406
|
+
|
|
3407
3407
|
// Reset flag after a short delay
|
|
3408
3408
|
setTimeout(() => {
|
|
3409
3409
|
this.hasTriedButtonFallback = false;
|
|
3410
3410
|
}, 1000);
|
|
3411
|
-
|
|
3411
|
+
|
|
3412
3412
|
// Try clicking the button programmatically
|
|
3413
3413
|
const clickEvent = new MouseEvent('click', {
|
|
3414
3414
|
bubbles: true,
|
|
@@ -3433,7 +3433,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
3433
3433
|
|
|
3434
3434
|
protected applySubtitleTrack(track: any): void {
|
|
3435
3435
|
if (!this.video) return;
|
|
3436
|
-
|
|
3436
|
+
|
|
3437
3437
|
const tracks = this.video.textTracks;
|
|
3438
3438
|
for (let i = 0; i < tracks.length; i++) {
|
|
3439
3439
|
const textTrack = tracks[i];
|
|
@@ -3447,7 +3447,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
3447
3447
|
|
|
3448
3448
|
protected removeSubtitles(): void {
|
|
3449
3449
|
if (!this.video) return;
|
|
3450
|
-
|
|
3450
|
+
|
|
3451
3451
|
const tracks = this.video.textTracks;
|
|
3452
3452
|
for (let i = 0; i < tracks.length; i++) {
|
|
3453
3453
|
tracks[i].mode = 'hidden';
|
|
@@ -3456,7 +3456,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
3456
3456
|
|
|
3457
3457
|
private injectStyles(): void {
|
|
3458
3458
|
if (document.getElementById('uvf-player-styles')) return;
|
|
3459
|
-
|
|
3459
|
+
|
|
3460
3460
|
const style = document.createElement('style');
|
|
3461
3461
|
style.id = 'uvf-player-styles';
|
|
3462
3462
|
style.textContent = this.getPlayerStyles();
|
|
@@ -6680,17 +6680,17 @@ export class WebPlayer extends BasePlayer {
|
|
|
6680
6680
|
* Creates framework branding logo
|
|
6681
6681
|
* Only creates branding if showFrameworkBranding is not explicitly set to false
|
|
6682
6682
|
*/
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6683
|
+
private createFrameworkBranding(container: HTMLElement): void {
|
|
6684
|
+
// Double-check configuration (defensive programming)
|
|
6685
|
+
if ((this.config as any).showFrameworkBranding === false) {
|
|
6686
|
+
this.debugLog('Framework branding disabled by configuration');
|
|
6687
|
+
return;
|
|
6688
|
+
}
|
|
6689
|
+
const brandingContainer = document.createElement('div');
|
|
6690
|
+
brandingContainer.className = 'uvf-framework-branding';
|
|
6691
|
+
brandingContainer.setAttribute('title', 'Powered by flicknexs');
|
|
6692
|
+
|
|
6693
|
+
const logoSvg = `
|
|
6694
6694
|
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" id="Layer_1" viewBox="0 0 180 29" class="uvf-logo-svg">
|
|
6695
6695
|
<defs>
|
|
6696
6696
|
<style>.cls-1{fill:#0d5ef8;}.cls-2{fill:#ff0366;opacity:0.94;}.cls-3{fill:#fff;}</style>
|
|
@@ -6710,35 +6710,35 @@ export class WebPlayer extends BasePlayer {
|
|
|
6710
6710
|
<path class="cls-3" d="M17.5,8.84l-2.33.74c-1.12.36-2,.63-2.63.82-.92.28-1.78.52-2.58.74s-1.61.41-2.41.59C8.22,13.68,9,16.3,9.72,19.6s1.37,6.23,1.79,8.8a18.88,18.88,0,0,0,2-.44,14.36,14.36,0,0,0,1.8-.64c-.08-1-.2-2-.34-3.2s-.31-2.38-.5-3.69c.55-.16,1.14-.3,1.76-.43s1.31-.26,2.07-.38q-.2-1.15-.39-1.92a8.8,8.8,0,0,0-.5-1.42,16.83,16.83,0,0,0-1.74.38,14.8,14.8,0,0,0-1.73.6c-.1-.59-.2-1.18-.32-1.78s-.24-1.18-.37-1.76L15,13.26l3-.82a8.59,8.59,0,0,0,0-1,6.88,6.88,0,0,0-.08-.83q-.09-.54-.21-1a6.18,6.18,0,0,0-.29-.78Z"/>
|
|
6711
6711
|
</svg>
|
|
6712
6712
|
`;
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6713
|
+
|
|
6714
|
+
brandingContainer.innerHTML = logoSvg;
|
|
6715
|
+
|
|
6716
|
+
// Add click handler to redirect to flicknexs.com
|
|
6717
|
+
brandingContainer.addEventListener('click', (event) => {
|
|
6718
|
+
event.preventDefault();
|
|
6719
|
+
event.stopPropagation();
|
|
6720
|
+
|
|
6721
|
+
// Open flicknexs.com in a new tab/window
|
|
6722
|
+
const link = document.createElement('a');
|
|
6723
|
+
link.href = 'https://flicknexs.com/';
|
|
6724
|
+
link.target = '_blank';
|
|
6725
|
+
link.rel = 'noopener noreferrer';
|
|
6726
|
+
document.body.appendChild(link);
|
|
6727
|
+
link.click();
|
|
6728
|
+
document.body.removeChild(link);
|
|
6729
|
+
|
|
6730
|
+
// Emit analytics event
|
|
6731
|
+
this.emit('frameworkBrandingClick', {
|
|
6732
|
+
timestamp: Date.now(),
|
|
6733
|
+
url: 'https://flicknexs.com/',
|
|
6734
|
+
userAgent: navigator.userAgent
|
|
6735
|
+
});
|
|
6736
|
+
});
|
|
6737
|
+
|
|
6738
|
+
container.appendChild(brandingContainer);
|
|
6739
|
+
|
|
6740
|
+
this.debugLog('Framework branding added');
|
|
6741
|
+
}
|
|
6742
6742
|
|
|
6743
6743
|
/**
|
|
6744
6744
|
* Create navigation buttons (back/close) based on configuration
|
|
@@ -6756,16 +6756,16 @@ export class WebPlayer extends BasePlayer {
|
|
|
6756
6756
|
backBtn.id = 'uvf-back-btn';
|
|
6757
6757
|
backBtn.title = backButton.title || 'Back';
|
|
6758
6758
|
backBtn.setAttribute('aria-label', backButton.ariaLabel || 'Go back');
|
|
6759
|
-
|
|
6759
|
+
|
|
6760
6760
|
// Get icon based on config
|
|
6761
6761
|
const backIcon = this.getNavigationIcon(backButton.icon || 'arrow', backButton.customIcon);
|
|
6762
6762
|
backBtn.innerHTML = backIcon;
|
|
6763
|
-
|
|
6763
|
+
|
|
6764
6764
|
// Add click handler
|
|
6765
6765
|
backBtn.addEventListener('click', async (e) => {
|
|
6766
6766
|
e.preventDefault();
|
|
6767
6767
|
e.stopPropagation();
|
|
6768
|
-
|
|
6768
|
+
|
|
6769
6769
|
if (backButton.onClick) {
|
|
6770
6770
|
await backButton.onClick();
|
|
6771
6771
|
} else if (backButton.href) {
|
|
@@ -6778,10 +6778,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
6778
6778
|
// Default: go back in history
|
|
6779
6779
|
window.history.back();
|
|
6780
6780
|
}
|
|
6781
|
-
|
|
6781
|
+
|
|
6782
6782
|
this.emit('navigationBackClicked');
|
|
6783
6783
|
});
|
|
6784
|
-
|
|
6784
|
+
|
|
6785
6785
|
container.appendChild(backBtn);
|
|
6786
6786
|
}
|
|
6787
6787
|
|
|
@@ -6792,16 +6792,16 @@ export class WebPlayer extends BasePlayer {
|
|
|
6792
6792
|
closeBtn.id = 'uvf-close-btn';
|
|
6793
6793
|
closeBtn.title = closeButton.title || 'Close';
|
|
6794
6794
|
closeBtn.setAttribute('aria-label', closeButton.ariaLabel || 'Close player');
|
|
6795
|
-
|
|
6795
|
+
|
|
6796
6796
|
// Get icon based on config
|
|
6797
6797
|
const closeIcon = this.getNavigationIcon(closeButton.icon || 'x', closeButton.customIcon);
|
|
6798
6798
|
closeBtn.innerHTML = closeIcon;
|
|
6799
|
-
|
|
6799
|
+
|
|
6800
6800
|
// Add click handler
|
|
6801
6801
|
closeBtn.addEventListener('click', async (e) => {
|
|
6802
6802
|
e.preventDefault();
|
|
6803
6803
|
e.stopPropagation();
|
|
6804
|
-
|
|
6804
|
+
|
|
6805
6805
|
if (closeButton.onClick) {
|
|
6806
6806
|
await closeButton.onClick();
|
|
6807
6807
|
} else {
|
|
@@ -6809,7 +6809,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
6809
6809
|
if (closeButton.exitFullscreen && this.isFullscreen()) {
|
|
6810
6810
|
await this.exitFullscreen();
|
|
6811
6811
|
}
|
|
6812
|
-
|
|
6812
|
+
|
|
6813
6813
|
if (closeButton.closeModal) {
|
|
6814
6814
|
// Hide player or remove from DOM
|
|
6815
6815
|
const playerWrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
|
|
@@ -6818,10 +6818,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
6818
6818
|
}
|
|
6819
6819
|
}
|
|
6820
6820
|
}
|
|
6821
|
-
|
|
6821
|
+
|
|
6822
6822
|
this.emit('navigationCloseClicked');
|
|
6823
6823
|
});
|
|
6824
|
-
|
|
6824
|
+
|
|
6825
6825
|
container.appendChild(closeBtn);
|
|
6826
6826
|
}
|
|
6827
6827
|
}
|
|
@@ -6843,22 +6843,22 @@ export class WebPlayer extends BasePlayer {
|
|
|
6843
6843
|
return `<svg viewBox="0 0 24 24">
|
|
6844
6844
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
|
|
6845
6845
|
</svg>`;
|
|
6846
|
-
|
|
6846
|
+
|
|
6847
6847
|
case 'chevron':
|
|
6848
6848
|
return `<svg viewBox="0 0 24 24">
|
|
6849
6849
|
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
|
|
6850
6850
|
</svg>`;
|
|
6851
|
-
|
|
6851
|
+
|
|
6852
6852
|
case 'x':
|
|
6853
6853
|
return `<svg viewBox="0 0 24 24">
|
|
6854
6854
|
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
|
|
6855
6855
|
</svg>`;
|
|
6856
|
-
|
|
6856
|
+
|
|
6857
6857
|
case 'close':
|
|
6858
6858
|
return `<svg viewBox="0 0 24 24">
|
|
6859
6859
|
<path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z" fill="currentColor"/>
|
|
6860
6860
|
</svg>`;
|
|
6861
|
-
|
|
6861
|
+
|
|
6862
6862
|
default:
|
|
6863
6863
|
return `<svg viewBox="0 0 24 24">
|
|
6864
6864
|
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z" fill="currentColor"/>
|
|
@@ -6875,21 +6875,21 @@ export class WebPlayer extends BasePlayer {
|
|
|
6875
6875
|
const controlsGradient = document.createElement('div');
|
|
6876
6876
|
controlsGradient.className = 'uvf-controls-gradient';
|
|
6877
6877
|
container.appendChild(controlsGradient);
|
|
6878
|
-
|
|
6878
|
+
|
|
6879
6879
|
// Combined top bar: navigation buttons → title → controls
|
|
6880
6880
|
const topBar = document.createElement('div');
|
|
6881
6881
|
topBar.className = 'uvf-top-bar';
|
|
6882
|
-
|
|
6882
|
+
|
|
6883
6883
|
// Left side container for navigation + title
|
|
6884
6884
|
const leftSide = document.createElement('div');
|
|
6885
6885
|
leftSide.className = 'uvf-left-side';
|
|
6886
|
-
|
|
6886
|
+
|
|
6887
6887
|
// Navigation buttons (back/close)
|
|
6888
6888
|
const navigationControls = document.createElement('div');
|
|
6889
6889
|
navigationControls.className = 'uvf-navigation-controls';
|
|
6890
6890
|
this.createNavigationButtons(navigationControls);
|
|
6891
6891
|
leftSide.appendChild(navigationControls);
|
|
6892
|
-
|
|
6892
|
+
|
|
6893
6893
|
// Title bar (after navigation buttons)
|
|
6894
6894
|
const titleBar = document.createElement('div');
|
|
6895
6895
|
titleBar.className = 'uvf-title-bar';
|
|
@@ -6902,9 +6902,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
6902
6902
|
</div>
|
|
6903
6903
|
`;
|
|
6904
6904
|
leftSide.appendChild(titleBar);
|
|
6905
|
-
|
|
6905
|
+
|
|
6906
6906
|
topBar.appendChild(leftSide);
|
|
6907
|
-
|
|
6907
|
+
|
|
6908
6908
|
// Top controls (right side - Cast and Share buttons)
|
|
6909
6909
|
const topControls = document.createElement('div');
|
|
6910
6910
|
topControls.className = 'uvf-top-controls';
|
|
@@ -6927,12 +6927,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
6927
6927
|
</button>
|
|
6928
6928
|
`;
|
|
6929
6929
|
topBar.appendChild(topControls);
|
|
6930
|
-
|
|
6930
|
+
|
|
6931
6931
|
container.appendChild(topBar);
|
|
6932
6932
|
|
|
6933
|
-
// Add loading spinner
|
|
6933
|
+
// Add loading spinner (show by default until video is ready)
|
|
6934
6934
|
const loadingContainer = document.createElement('div');
|
|
6935
|
-
loadingContainer.className = 'uvf-loading-container';
|
|
6935
|
+
loadingContainer.className = 'uvf-loading-container active'; // Start with active class
|
|
6936
6936
|
loadingContainer.id = 'uvf-loading';
|
|
6937
6937
|
loadingContainer.innerHTML = '<div class="uvf-loading-spinner"></div>';
|
|
6938
6938
|
container.appendChild(loadingContainer);
|
|
@@ -6940,15 +6940,15 @@ export class WebPlayer extends BasePlayer {
|
|
|
6940
6940
|
// Add center play button container for proper responsive centering
|
|
6941
6941
|
const centerPlayContainer = document.createElement('div');
|
|
6942
6942
|
centerPlayContainer.className = 'uvf-center-play-container';
|
|
6943
|
-
|
|
6943
|
+
|
|
6944
6944
|
const centerPlayBtn = document.createElement('div');
|
|
6945
6945
|
centerPlayBtn.className = 'uvf-center-play-btn uvf-pulse';
|
|
6946
6946
|
centerPlayBtn.id = 'uvf-center-play';
|
|
6947
6947
|
centerPlayBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
|
|
6948
|
-
|
|
6948
|
+
|
|
6949
6949
|
centerPlayContainer.appendChild(centerPlayBtn);
|
|
6950
6950
|
container.appendChild(centerPlayContainer);
|
|
6951
|
-
|
|
6951
|
+
|
|
6952
6952
|
// Add shortcut indicator
|
|
6953
6953
|
const shortcutIndicator = document.createElement('div');
|
|
6954
6954
|
shortcutIndicator.className = 'uvf-shortcut-indicator';
|
|
@@ -6959,11 +6959,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
6959
6959
|
const controlsBar = document.createElement('div');
|
|
6960
6960
|
controlsBar.className = 'uvf-controls-bar';
|
|
6961
6961
|
controlsBar.id = 'uvf-controls';
|
|
6962
|
-
|
|
6962
|
+
|
|
6963
6963
|
// Time and branding section above seekbar
|
|
6964
6964
|
const aboveSeekbarSection = document.createElement('div');
|
|
6965
6965
|
aboveSeekbarSection.className = 'uvf-above-seekbar-section';
|
|
6966
|
-
|
|
6966
|
+
|
|
6967
6967
|
// Time display (moved to above seekbar)
|
|
6968
6968
|
const timeDisplay = document.createElement('div');
|
|
6969
6969
|
timeDisplay.className = 'uvf-time-display uvf-above-seekbar';
|
|
@@ -6975,11 +6975,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
6975
6975
|
if ((this.config as any).showFrameworkBranding !== false) {
|
|
6976
6976
|
this.createFrameworkBranding(aboveSeekbarSection);
|
|
6977
6977
|
}
|
|
6978
|
-
|
|
6978
|
+
|
|
6979
6979
|
// Progress section
|
|
6980
6980
|
const progressSection = document.createElement('div');
|
|
6981
6981
|
progressSection.className = 'uvf-progress-section';
|
|
6982
|
-
|
|
6982
|
+
|
|
6983
6983
|
const progressBar = document.createElement('div');
|
|
6984
6984
|
progressBar.className = 'uvf-progress-bar-wrapper';
|
|
6985
6985
|
progressBar.id = 'uvf-progress-bar';
|
|
@@ -6992,11 +6992,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
6992
6992
|
<div class="uvf-time-tooltip" id="uvf-time-tooltip">00:00</div>
|
|
6993
6993
|
`;
|
|
6994
6994
|
progressSection.appendChild(progressBar);
|
|
6995
|
-
|
|
6995
|
+
|
|
6996
6996
|
// Controls row
|
|
6997
6997
|
const controlsRow = document.createElement('div');
|
|
6998
6998
|
controlsRow.className = 'uvf-controls-row';
|
|
6999
|
-
|
|
6999
|
+
|
|
7000
7000
|
// Play/Pause button
|
|
7001
7001
|
const playPauseBtn = document.createElement('button');
|
|
7002
7002
|
playPauseBtn.className = 'uvf-control-btn play-pause';
|
|
@@ -7010,7 +7010,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7010
7010
|
</svg>
|
|
7011
7011
|
`;
|
|
7012
7012
|
controlsRow.appendChild(playPauseBtn);
|
|
7013
|
-
|
|
7013
|
+
|
|
7014
7014
|
// Skip buttons with consistent icons
|
|
7015
7015
|
const skipBackBtn = document.createElement('button');
|
|
7016
7016
|
skipBackBtn.className = 'uvf-control-btn';
|
|
@@ -7019,7 +7019,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7019
7019
|
skipBackBtn.setAttribute('aria-label', 'Skip backward 10 seconds');
|
|
7020
7020
|
skipBackBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
|
|
7021
7021
|
controlsRow.appendChild(skipBackBtn);
|
|
7022
|
-
|
|
7022
|
+
|
|
7023
7023
|
const skipForwardBtn = document.createElement('button');
|
|
7024
7024
|
skipForwardBtn.className = 'uvf-control-btn';
|
|
7025
7025
|
skipForwardBtn.id = 'uvf-skip-forward';
|
|
@@ -7027,7 +7027,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7027
7027
|
skipForwardBtn.setAttribute('aria-label', 'Skip forward 10 seconds');
|
|
7028
7028
|
skipForwardBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M11 18V6l-8.5 6 8.5 6zm.5-6l8.5 6V6l-8.5 6z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
|
|
7029
7029
|
controlsRow.appendChild(skipForwardBtn);
|
|
7030
|
-
|
|
7030
|
+
|
|
7031
7031
|
// Volume control
|
|
7032
7032
|
const volumeControl = document.createElement('div');
|
|
7033
7033
|
volumeControl.className = 'uvf-volume-control';
|
|
@@ -7048,23 +7048,23 @@ export class WebPlayer extends BasePlayer {
|
|
|
7048
7048
|
</div>
|
|
7049
7049
|
`;
|
|
7050
7050
|
controlsRow.appendChild(volumeControl);
|
|
7051
|
-
|
|
7051
|
+
|
|
7052
7052
|
// Right controls
|
|
7053
7053
|
const rightControls = document.createElement('div');
|
|
7054
7054
|
rightControls.className = 'uvf-right-controls';
|
|
7055
|
-
|
|
7055
|
+
|
|
7056
7056
|
// Quality badge
|
|
7057
7057
|
const qualityBadge = document.createElement('div');
|
|
7058
7058
|
qualityBadge.className = 'uvf-quality-badge';
|
|
7059
7059
|
qualityBadge.id = 'uvf-quality-badge';
|
|
7060
7060
|
qualityBadge.textContent = 'HD';
|
|
7061
7061
|
rightControls.appendChild(qualityBadge);
|
|
7062
|
-
|
|
7062
|
+
|
|
7063
7063
|
// Settings button with menu (show only if enabled)
|
|
7064
7064
|
this.debugLog('Settings config check:', this.settingsConfig);
|
|
7065
7065
|
this.debugLog('Settings enabled:', this.settingsConfig.enabled);
|
|
7066
7066
|
this.debugLog('Custom controls enabled:', this.useCustomControls);
|
|
7067
|
-
|
|
7067
|
+
|
|
7068
7068
|
if (this.settingsConfig.enabled) {
|
|
7069
7069
|
this.debugLog('Creating settings button...');
|
|
7070
7070
|
const settingsContainer = document.createElement('div');
|
|
@@ -7073,7 +7073,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7073
7073
|
settingsContainer.style.display = 'flex';
|
|
7074
7074
|
settingsContainer.style.alignItems = 'center';
|
|
7075
7075
|
settingsContainer.style.justifyContent = 'center';
|
|
7076
|
-
|
|
7076
|
+
|
|
7077
7077
|
const settingsBtn = document.createElement('button');
|
|
7078
7078
|
settingsBtn.className = 'uvf-control-btn';
|
|
7079
7079
|
settingsBtn.id = 'uvf-settings-btn';
|
|
@@ -7081,7 +7081,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7081
7081
|
settingsBtn.setAttribute('aria-label', 'Settings');
|
|
7082
7082
|
settingsBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>';
|
|
7083
7083
|
settingsContainer.appendChild(settingsBtn);
|
|
7084
|
-
|
|
7084
|
+
|
|
7085
7085
|
// Settings menu - will be populated dynamically based on video capabilities and configuration
|
|
7086
7086
|
const settingsMenu = document.createElement('div');
|
|
7087
7087
|
settingsMenu.className = 'uvf-settings-menu';
|
|
@@ -7091,9 +7091,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
7091
7091
|
// CSS handles initial hidden state
|
|
7092
7092
|
settingsContainer.appendChild(settingsMenu);
|
|
7093
7093
|
rightControls.appendChild(settingsContainer);
|
|
7094
|
-
|
|
7094
|
+
|
|
7095
7095
|
this.debugLog('Settings button created and added to controls');
|
|
7096
|
-
|
|
7096
|
+
|
|
7097
7097
|
// Add debugging for settings button visibility
|
|
7098
7098
|
setTimeout(() => {
|
|
7099
7099
|
const createdBtn = document.getElementById('uvf-settings-btn');
|
|
@@ -7107,10 +7107,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
7107
7107
|
this.debugLog('- Height:', computedStyle.height);
|
|
7108
7108
|
this.debugLog('- Position:', computedStyle.position);
|
|
7109
7109
|
this.debugLog('- Z-index:', computedStyle.zIndex);
|
|
7110
|
-
|
|
7110
|
+
|
|
7111
7111
|
const rect = createdBtn.getBoundingClientRect();
|
|
7112
7112
|
this.debugLog('- Bounding rect:', { width: rect.width, height: rect.height, top: rect.top, left: rect.left });
|
|
7113
|
-
|
|
7113
|
+
|
|
7114
7114
|
// Check parent containers
|
|
7115
7115
|
const settingsContainer = createdBtn.closest('.uvf-settings-container');
|
|
7116
7116
|
if (settingsContainer) {
|
|
@@ -7120,7 +7120,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7120
7120
|
this.debugLog('Settings container visibility:', containerStyle.visibility);
|
|
7121
7121
|
this.debugLog('Settings container rect:', { width: containerRect.width, height: containerRect.height, top: containerRect.top, left: containerRect.left });
|
|
7122
7122
|
}
|
|
7123
|
-
|
|
7123
|
+
|
|
7124
7124
|
const rightControls = createdBtn.closest('.uvf-right-controls');
|
|
7125
7125
|
if (rightControls) {
|
|
7126
7126
|
const parentStyle = window.getComputedStyle(rightControls);
|
|
@@ -7132,7 +7132,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7132
7132
|
} else {
|
|
7133
7133
|
this.debugError('Settings button NOT found after creation!');
|
|
7134
7134
|
}
|
|
7135
|
-
|
|
7135
|
+
|
|
7136
7136
|
// Fallback: Force settings button visibility if it's not rendering properly
|
|
7137
7137
|
setTimeout(() => {
|
|
7138
7138
|
const btn = document.getElementById('uvf-settings-btn');
|
|
@@ -7152,7 +7152,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7152
7152
|
btn.style.border = '1px solid rgba(255,255,255,0.1)';
|
|
7153
7153
|
btn.style.position = 'relative';
|
|
7154
7154
|
btn.style.zIndex = '10';
|
|
7155
|
-
|
|
7155
|
+
|
|
7156
7156
|
// Also fix the container
|
|
7157
7157
|
const container = btn.parentElement;
|
|
7158
7158
|
if (container) {
|
|
@@ -7162,7 +7162,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7162
7162
|
container.style.minWidth = '44px';
|
|
7163
7163
|
container.style.minHeight = '44px';
|
|
7164
7164
|
}
|
|
7165
|
-
|
|
7165
|
+
|
|
7166
7166
|
this.debugLog('Fallback styles applied to settings button');
|
|
7167
7167
|
}
|
|
7168
7168
|
}
|
|
@@ -7171,7 +7171,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7171
7171
|
} else {
|
|
7172
7172
|
this.debugLog('Settings button NOT created - settings disabled');
|
|
7173
7173
|
}
|
|
7174
|
-
|
|
7174
|
+
|
|
7175
7175
|
// EPG button (Electronic Program Guide)
|
|
7176
7176
|
const epgBtn = document.createElement('button');
|
|
7177
7177
|
epgBtn.className = 'uvf-control-btn';
|
|
@@ -7185,42 +7185,42 @@ export class WebPlayer extends BasePlayer {
|
|
|
7185
7185
|
</svg>`;
|
|
7186
7186
|
epgBtn.style.display = 'none'; // Initially hidden, will be shown when EPG data is available
|
|
7187
7187
|
rightControls.appendChild(epgBtn);
|
|
7188
|
-
|
|
7188
|
+
|
|
7189
7189
|
// PiP button - only show on desktop/supported browsers
|
|
7190
7190
|
const pipBtn = document.createElement('button');
|
|
7191
7191
|
pipBtn.className = 'uvf-control-btn';
|
|
7192
7192
|
pipBtn.id = 'uvf-pip-btn';
|
|
7193
7193
|
pipBtn.title = 'Picture-in-Picture';
|
|
7194
7194
|
pipBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
|
|
7195
|
-
|
|
7195
|
+
|
|
7196
7196
|
// Hide PiP button on mobile devices and browsers that don't support it
|
|
7197
7197
|
if (this.isMobileDevice() || !this.isPipSupported()) {
|
|
7198
7198
|
pipBtn.style.display = 'none';
|
|
7199
7199
|
}
|
|
7200
|
-
|
|
7200
|
+
|
|
7201
7201
|
rightControls.appendChild(pipBtn);
|
|
7202
|
-
|
|
7202
|
+
|
|
7203
7203
|
// Fullscreen button
|
|
7204
7204
|
const fullscreenBtn = document.createElement('button');
|
|
7205
7205
|
fullscreenBtn.className = 'uvf-control-btn';
|
|
7206
7206
|
fullscreenBtn.id = 'uvf-fullscreen-btn';
|
|
7207
7207
|
fullscreenBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" stroke="currentColor" stroke-width="0.5" fill="currentColor"/></svg>';
|
|
7208
7208
|
rightControls.appendChild(fullscreenBtn);
|
|
7209
|
-
|
|
7209
|
+
|
|
7210
7210
|
controlsRow.appendChild(rightControls);
|
|
7211
|
-
|
|
7211
|
+
|
|
7212
7212
|
// Assemble controls bar
|
|
7213
7213
|
controlsBar.appendChild(aboveSeekbarSection);
|
|
7214
7214
|
controlsBar.appendChild(progressSection);
|
|
7215
7215
|
controlsBar.appendChild(controlsRow);
|
|
7216
7216
|
container.appendChild(controlsBar);
|
|
7217
|
-
|
|
7217
|
+
|
|
7218
7218
|
this.controlsContainer = controlsBar;
|
|
7219
7219
|
}
|
|
7220
7220
|
|
|
7221
7221
|
private setupControlsEventListeners(): void {
|
|
7222
7222
|
if (!this.useCustomControls || !this.video) return;
|
|
7223
|
-
|
|
7223
|
+
|
|
7224
7224
|
const wrapper = this.container?.querySelector('.uvf-player-wrapper') as HTMLElement;
|
|
7225
7225
|
const centerPlay = document.getElementById('uvf-center-play');
|
|
7226
7226
|
const playPauseBtn = document.getElementById('uvf-play-pause');
|
|
@@ -7232,25 +7232,25 @@ export class WebPlayer extends BasePlayer {
|
|
|
7232
7232
|
const progressBar = document.getElementById('uvf-progress-bar');
|
|
7233
7233
|
const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
|
|
7234
7234
|
const settingsBtn = document.getElementById('uvf-settings-btn');
|
|
7235
|
-
|
|
7235
|
+
|
|
7236
7236
|
// Get the event target (video element or YouTube player)
|
|
7237
7237
|
const getEventTarget = () => this.youtubePlayer && this.youtubePlayerReady ? this.youtubePlayer : this.video;
|
|
7238
|
-
|
|
7238
|
+
|
|
7239
7239
|
// Disable right-click context menu
|
|
7240
7240
|
this.video.addEventListener('contextmenu', (e) => {
|
|
7241
7241
|
e.preventDefault();
|
|
7242
7242
|
return false;
|
|
7243
7243
|
});
|
|
7244
|
-
|
|
7244
|
+
|
|
7245
7245
|
wrapper?.addEventListener('contextmenu', (e) => {
|
|
7246
7246
|
e.preventDefault();
|
|
7247
7247
|
return false;
|
|
7248
7248
|
});
|
|
7249
|
-
|
|
7249
|
+
|
|
7250
7250
|
// Play/Pause
|
|
7251
7251
|
centerPlay?.addEventListener('click', () => this.togglePlayPause());
|
|
7252
7252
|
playPauseBtn?.addEventListener('click', () => this.togglePlayPause());
|
|
7253
|
-
|
|
7253
|
+
|
|
7254
7254
|
// Video click behavior will be handled by the comprehensive tap system below
|
|
7255
7255
|
// Desktop click for play/pause
|
|
7256
7256
|
if (!this.isMobileDevice()) {
|
|
@@ -7258,20 +7258,20 @@ export class WebPlayer extends BasePlayer {
|
|
|
7258
7258
|
this.togglePlayPause();
|
|
7259
7259
|
});
|
|
7260
7260
|
}
|
|
7261
|
-
|
|
7261
|
+
|
|
7262
7262
|
// Update play/pause icons
|
|
7263
7263
|
this.video.addEventListener('play', () => {
|
|
7264
7264
|
const playIcon = document.getElementById('uvf-play-icon');
|
|
7265
7265
|
const pauseIcon = document.getElementById('uvf-pause-icon');
|
|
7266
7266
|
if (playIcon) playIcon.style.display = 'none';
|
|
7267
7267
|
if (pauseIcon) pauseIcon.style.display = 'block';
|
|
7268
|
-
|
|
7268
|
+
|
|
7269
7269
|
// Hide center play button when playing
|
|
7270
7270
|
if (centerPlay) {
|
|
7271
7271
|
centerPlay.classList.add('hidden');
|
|
7272
7272
|
this.debugLog('Center play button hidden - video playing');
|
|
7273
7273
|
}
|
|
7274
|
-
|
|
7274
|
+
|
|
7275
7275
|
// Schedule hide controls
|
|
7276
7276
|
setTimeout(() => {
|
|
7277
7277
|
if (this.state.isPlaying) {
|
|
@@ -7279,13 +7279,13 @@ export class WebPlayer extends BasePlayer {
|
|
|
7279
7279
|
}
|
|
7280
7280
|
}, 1000);
|
|
7281
7281
|
});
|
|
7282
|
-
|
|
7282
|
+
|
|
7283
7283
|
this.video.addEventListener('pause', () => {
|
|
7284
7284
|
const playIcon = document.getElementById('uvf-play-icon');
|
|
7285
7285
|
const pauseIcon = document.getElementById('uvf-pause-icon');
|
|
7286
7286
|
if (playIcon) playIcon.style.display = 'block';
|
|
7287
7287
|
if (pauseIcon) pauseIcon.style.display = 'none';
|
|
7288
|
-
|
|
7288
|
+
|
|
7289
7289
|
// Show center play button when paused
|
|
7290
7290
|
if (centerPlay) {
|
|
7291
7291
|
centerPlay.classList.remove('hidden');
|
|
@@ -7293,7 +7293,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7293
7293
|
}
|
|
7294
7294
|
this.showControls();
|
|
7295
7295
|
});
|
|
7296
|
-
|
|
7296
|
+
|
|
7297
7297
|
// Ensure center play button is visible initially when video is paused/stopped
|
|
7298
7298
|
this.video.addEventListener('loadeddata', () => {
|
|
7299
7299
|
if (centerPlay && (this.video?.paused || this.video?.ended)) {
|
|
@@ -7301,14 +7301,14 @@ export class WebPlayer extends BasePlayer {
|
|
|
7301
7301
|
this.debugLog('Center play button shown - video loaded and paused');
|
|
7302
7302
|
}
|
|
7303
7303
|
});
|
|
7304
|
-
|
|
7304
|
+
|
|
7305
7305
|
this.video.addEventListener('ended', () => {
|
|
7306
7306
|
if (centerPlay) {
|
|
7307
7307
|
centerPlay.classList.remove('hidden');
|
|
7308
7308
|
this.debugLog('Center play button shown - video ended');
|
|
7309
7309
|
}
|
|
7310
7310
|
});
|
|
7311
|
-
|
|
7311
|
+
|
|
7312
7312
|
// Skip buttons with null safety - works with both video and YouTube
|
|
7313
7313
|
skipBackBtn?.addEventListener('click', () => {
|
|
7314
7314
|
const currentTime = this.getCurrentTime();
|
|
@@ -7326,19 +7326,19 @@ export class WebPlayer extends BasePlayer {
|
|
|
7326
7326
|
this.showShortcutIndicator('+10s');
|
|
7327
7327
|
}
|
|
7328
7328
|
});
|
|
7329
|
-
|
|
7329
|
+
|
|
7330
7330
|
// Volume control
|
|
7331
7331
|
volumeBtn?.addEventListener('click', (e) => {
|
|
7332
7332
|
e.stopPropagation();
|
|
7333
7333
|
this.toggleMuteAction();
|
|
7334
7334
|
});
|
|
7335
|
-
|
|
7335
|
+
|
|
7336
7336
|
// Volume panel interactions
|
|
7337
7337
|
volumeBtn?.addEventListener('mouseenter', () => {
|
|
7338
7338
|
if (this.volumeHideTimeout) clearTimeout(this.volumeHideTimeout);
|
|
7339
7339
|
volumePanel?.classList.add('active');
|
|
7340
7340
|
});
|
|
7341
|
-
|
|
7341
|
+
|
|
7342
7342
|
volumeBtn?.addEventListener('mouseleave', () => {
|
|
7343
7343
|
this.volumeHideTimeout = setTimeout(() => {
|
|
7344
7344
|
if (!volumePanel?.matches(':hover')) {
|
|
@@ -7346,12 +7346,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
7346
7346
|
}
|
|
7347
7347
|
}, 800);
|
|
7348
7348
|
});
|
|
7349
|
-
|
|
7349
|
+
|
|
7350
7350
|
volumePanel?.addEventListener('mouseenter', () => {
|
|
7351
7351
|
if (this.volumeHideTimeout) clearTimeout(this.volumeHideTimeout);
|
|
7352
7352
|
volumePanel.classList.add('active');
|
|
7353
7353
|
});
|
|
7354
|
-
|
|
7354
|
+
|
|
7355
7355
|
volumePanel?.addEventListener('mouseleave', () => {
|
|
7356
7356
|
if (!this.isVolumeSliding) {
|
|
7357
7357
|
setTimeout(() => {
|
|
@@ -7361,7 +7361,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7361
7361
|
}, 1500);
|
|
7362
7362
|
}
|
|
7363
7363
|
});
|
|
7364
|
-
|
|
7364
|
+
|
|
7365
7365
|
// Volume slider
|
|
7366
7366
|
volumeSlider?.addEventListener('mousedown', (e) => {
|
|
7367
7367
|
e.stopPropagation();
|
|
@@ -7369,43 +7369,43 @@ export class WebPlayer extends BasePlayer {
|
|
|
7369
7369
|
volumePanel?.classList.add('active');
|
|
7370
7370
|
this.handleVolumeChange(e as MouseEvent);
|
|
7371
7371
|
});
|
|
7372
|
-
|
|
7372
|
+
|
|
7373
7373
|
volumeSlider?.addEventListener('click', (e) => {
|
|
7374
7374
|
e.stopPropagation();
|
|
7375
7375
|
this.handleVolumeChange(e as MouseEvent);
|
|
7376
7376
|
});
|
|
7377
|
-
|
|
7378
|
-
|
|
7377
|
+
|
|
7378
|
+
|
|
7379
7379
|
// Progress bar interactions
|
|
7380
7380
|
progressBar?.addEventListener('click', (e) => {
|
|
7381
7381
|
this.seekToPosition(e as MouseEvent);
|
|
7382
7382
|
});
|
|
7383
|
-
|
|
7383
|
+
|
|
7384
7384
|
progressBar?.addEventListener('mousedown', (e) => {
|
|
7385
7385
|
this.isDragging = true;
|
|
7386
7386
|
this.showTimeTooltip = true;
|
|
7387
7387
|
this.seekToPosition(e as MouseEvent);
|
|
7388
7388
|
this.updateTimeTooltip(e as MouseEvent);
|
|
7389
7389
|
});
|
|
7390
|
-
|
|
7390
|
+
|
|
7391
7391
|
// Hover tooltip functionality
|
|
7392
7392
|
progressBar?.addEventListener('mouseenter', () => {
|
|
7393
7393
|
this.showTimeTooltip = true;
|
|
7394
7394
|
});
|
|
7395
|
-
|
|
7395
|
+
|
|
7396
7396
|
progressBar?.addEventListener('mouseleave', () => {
|
|
7397
7397
|
if (!this.isDragging) {
|
|
7398
7398
|
this.showTimeTooltip = false;
|
|
7399
7399
|
this.hideTimeTooltip();
|
|
7400
7400
|
}
|
|
7401
7401
|
});
|
|
7402
|
-
|
|
7402
|
+
|
|
7403
7403
|
progressBar?.addEventListener('mousemove', (e) => {
|
|
7404
7404
|
if (this.showTimeTooltip) {
|
|
7405
7405
|
this.updateTimeTooltip(e as MouseEvent);
|
|
7406
7406
|
}
|
|
7407
7407
|
});
|
|
7408
|
-
|
|
7408
|
+
|
|
7409
7409
|
// Touch support for mobile devices
|
|
7410
7410
|
progressBar?.addEventListener('touchstart', (e) => {
|
|
7411
7411
|
e.preventDefault(); // Prevent scrolling
|
|
@@ -7417,7 +7417,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7417
7417
|
});
|
|
7418
7418
|
this.seekToPosition(mouseEvent);
|
|
7419
7419
|
}, { passive: false });
|
|
7420
|
-
|
|
7420
|
+
|
|
7421
7421
|
// Global mouse and touch events for enhanced dragging
|
|
7422
7422
|
document.addEventListener('mousemove', (e) => {
|
|
7423
7423
|
if (this.isVolumeSliding) {
|
|
@@ -7429,7 +7429,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7429
7429
|
this.updateTimeTooltip(e);
|
|
7430
7430
|
}
|
|
7431
7431
|
});
|
|
7432
|
-
|
|
7432
|
+
|
|
7433
7433
|
document.addEventListener('touchmove', (e) => {
|
|
7434
7434
|
if (this.isDragging && progressBar) {
|
|
7435
7435
|
e.preventDefault(); // Prevent scrolling
|
|
@@ -7441,7 +7441,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7441
7441
|
this.seekToPosition(mouseEvent);
|
|
7442
7442
|
}
|
|
7443
7443
|
}, { passive: false });
|
|
7444
|
-
|
|
7444
|
+
|
|
7445
7445
|
document.addEventListener('mouseup', () => {
|
|
7446
7446
|
if (this.isVolumeSliding) {
|
|
7447
7447
|
this.isVolumeSliding = false;
|
|
@@ -7451,7 +7451,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7451
7451
|
}
|
|
7452
7452
|
}, 2000);
|
|
7453
7453
|
}
|
|
7454
|
-
|
|
7454
|
+
|
|
7455
7455
|
if (this.isDragging) {
|
|
7456
7456
|
this.isDragging = false;
|
|
7457
7457
|
// Remove dragging class from handle
|
|
@@ -7464,7 +7464,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7464
7464
|
}
|
|
7465
7465
|
}
|
|
7466
7466
|
});
|
|
7467
|
-
|
|
7467
|
+
|
|
7468
7468
|
document.addEventListener('touchend', () => {
|
|
7469
7469
|
if (this.isDragging) {
|
|
7470
7470
|
this.isDragging = false;
|
|
@@ -7476,26 +7476,26 @@ export class WebPlayer extends BasePlayer {
|
|
|
7476
7476
|
this.hideTimeTooltip();
|
|
7477
7477
|
}
|
|
7478
7478
|
});
|
|
7479
|
-
|
|
7479
|
+
|
|
7480
7480
|
// Update progress bar
|
|
7481
7481
|
this.video.addEventListener('timeupdate', () => {
|
|
7482
7482
|
const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
|
|
7483
7483
|
const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
|
|
7484
|
-
|
|
7484
|
+
|
|
7485
7485
|
if (this.video && progressFilled) {
|
|
7486
7486
|
const percent = (this.video.currentTime / this.video.duration) * 100;
|
|
7487
7487
|
progressFilled.style.width = percent + '%';
|
|
7488
|
-
|
|
7488
|
+
|
|
7489
7489
|
// Update handle position (only when not dragging)
|
|
7490
7490
|
if (progressHandle && !this.isDragging) {
|
|
7491
7491
|
progressHandle.style.left = percent + '%';
|
|
7492
7492
|
}
|
|
7493
7493
|
}
|
|
7494
|
-
|
|
7494
|
+
|
|
7495
7495
|
// Update time display using the dedicated method
|
|
7496
7496
|
this.updateTimeDisplay();
|
|
7497
7497
|
});
|
|
7498
|
-
|
|
7498
|
+
|
|
7499
7499
|
// Update buffered progress
|
|
7500
7500
|
this.video.addEventListener('progress', () => {
|
|
7501
7501
|
const progressBuffered = document.getElementById('uvf-progress-buffered') as HTMLElement;
|
|
@@ -7504,7 +7504,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7504
7504
|
progressBuffered.style.width = buffered + '%';
|
|
7505
7505
|
}
|
|
7506
7506
|
});
|
|
7507
|
-
|
|
7507
|
+
|
|
7508
7508
|
// Update volume display
|
|
7509
7509
|
this.video.addEventListener('volumechange', () => {
|
|
7510
7510
|
const volumeFill = document.getElementById('uvf-volume-fill') as HTMLElement;
|
|
@@ -7533,7 +7533,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7533
7533
|
}
|
|
7534
7534
|
}
|
|
7535
7535
|
});
|
|
7536
|
-
|
|
7536
|
+
|
|
7537
7537
|
// Fullscreen button with enhanced cross-platform support
|
|
7538
7538
|
fullscreenBtn?.addEventListener('click', (event) => {
|
|
7539
7539
|
// Enhanced debugging for all platforms
|
|
@@ -7542,7 +7542,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7542
7542
|
const isIOS = this.isIOSDevice();
|
|
7543
7543
|
const isAndroid = this.isAndroidDevice();
|
|
7544
7544
|
const isMobile = this.isMobileDevice();
|
|
7545
|
-
|
|
7545
|
+
|
|
7546
7546
|
this.debugLog('Fullscreen button clicked:', {
|
|
7547
7547
|
isBrave,
|
|
7548
7548
|
isPrivate,
|
|
@@ -7555,13 +7555,13 @@ export class WebPlayer extends BasePlayer {
|
|
|
7555
7555
|
timestamp: Date.now(),
|
|
7556
7556
|
fullscreenSupported: this.isFullscreenSupported()
|
|
7557
7557
|
});
|
|
7558
|
-
|
|
7558
|
+
|
|
7559
7559
|
// Update user interaction timestamp
|
|
7560
7560
|
this.lastUserInteraction = Date.now();
|
|
7561
|
-
|
|
7561
|
+
|
|
7562
7562
|
// Run permissions check before attempting fullscreen
|
|
7563
7563
|
this.checkFullscreenPermissions();
|
|
7564
|
-
|
|
7564
|
+
|
|
7565
7565
|
if (this.isFullscreen()) {
|
|
7566
7566
|
this.debugLog('Exiting fullscreen via button');
|
|
7567
7567
|
this.exitFullscreen().catch(err => {
|
|
@@ -7569,18 +7569,18 @@ export class WebPlayer extends BasePlayer {
|
|
|
7569
7569
|
});
|
|
7570
7570
|
} else {
|
|
7571
7571
|
this.debugLog('Entering fullscreen via button');
|
|
7572
|
-
|
|
7572
|
+
|
|
7573
7573
|
// iOS Safari special message
|
|
7574
7574
|
if (isIOS) {
|
|
7575
7575
|
this.showShortcutIndicator('Using iOS video fullscreen');
|
|
7576
7576
|
} else if (isAndroid) {
|
|
7577
7577
|
this.showShortcutIndicator('Entering fullscreen - rotate to landscape');
|
|
7578
7578
|
}
|
|
7579
|
-
|
|
7579
|
+
|
|
7580
7580
|
// Use enhanced cross-platform fullscreen method
|
|
7581
7581
|
this.enterFullscreen().catch(err => {
|
|
7582
7582
|
this.debugWarn('Fullscreen button failed:', err.message);
|
|
7583
|
-
|
|
7583
|
+
|
|
7584
7584
|
// Platform-specific error messages
|
|
7585
7585
|
if (isIOS) {
|
|
7586
7586
|
this.showTemporaryMessage('iOS: Use device rotation or video controls for fullscreen');
|
|
@@ -7594,69 +7594,70 @@ export class WebPlayer extends BasePlayer {
|
|
|
7594
7594
|
});
|
|
7595
7595
|
}
|
|
7596
7596
|
});
|
|
7597
|
-
|
|
7597
|
+
|
|
7598
7598
|
// Update fullscreen button icon based on state
|
|
7599
7599
|
const updateFullscreenIcon = () => {
|
|
7600
7600
|
const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
|
|
7601
7601
|
if (fullscreenBtn) {
|
|
7602
|
-
fullscreenBtn.innerHTML = this.isFullscreen()
|
|
7602
|
+
fullscreenBtn.innerHTML = this.isFullscreen()
|
|
7603
7603
|
? '<svg viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>'
|
|
7604
7604
|
: '<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
|
|
7605
7605
|
}
|
|
7606
7606
|
};
|
|
7607
|
-
|
|
7607
|
+
|
|
7608
7608
|
// Listen for fullscreen state changes to update icon
|
|
7609
7609
|
this.on('onFullscreenChanged', updateFullscreenIcon);
|
|
7610
|
-
|
|
7610
|
+
|
|
7611
7611
|
// Loading states
|
|
7612
7612
|
this.video.addEventListener('waiting', () => {
|
|
7613
7613
|
const loading = document.getElementById('uvf-loading');
|
|
7614
7614
|
if (loading) loading.classList.add('active');
|
|
7615
7615
|
});
|
|
7616
|
-
|
|
7616
|
+
|
|
7617
7617
|
this.video.addEventListener('canplay', () => {
|
|
7618
7618
|
const loading = document.getElementById('uvf-loading');
|
|
7619
7619
|
if (loading) loading.classList.remove('active');
|
|
7620
7620
|
// Update settings menu when video is ready
|
|
7621
7621
|
this.updateSettingsMenu();
|
|
7622
|
+
this.debugLog('📡 canplay event fired - video ready to play');
|
|
7622
7623
|
});
|
|
7623
7624
|
|
|
7624
7625
|
this.video.addEventListener('loadedmetadata', () => {
|
|
7625
7626
|
// Update settings menu when metadata is loaded
|
|
7626
7627
|
this.updateSettingsMenu();
|
|
7627
7628
|
});
|
|
7628
|
-
|
|
7629
|
+
|
|
7629
7630
|
// Note: Enhanced mouse movement and control visibility handled in setupFullscreenListeners()
|
|
7630
|
-
|
|
7631
|
+
|
|
7631
7632
|
this.controlsContainer?.addEventListener('mouseenter', () => {
|
|
7632
7633
|
if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
|
|
7633
7634
|
});
|
|
7634
|
-
|
|
7635
|
+
|
|
7635
7636
|
this.controlsContainer?.addEventListener('mouseleave', () => {
|
|
7636
7637
|
if (this.state.isPlaying) {
|
|
7637
7638
|
this.scheduleHideControls();
|
|
7638
7639
|
}
|
|
7639
7640
|
});
|
|
7640
|
-
|
|
7641
|
-
|
|
7641
|
+
|
|
7642
|
+
|
|
7642
7643
|
// Settings menu - dynamically populated
|
|
7643
7644
|
const settingsMenu = document.getElementById('uvf-settings-menu');
|
|
7644
7645
|
this.debugLog('Settings menu element found:', !!settingsMenu);
|
|
7645
7646
|
this.debugLog('Settings button found:', !!settingsBtn);
|
|
7646
|
-
|
|
7647
|
+
|
|
7647
7648
|
settingsBtn?.addEventListener('click', (e) => {
|
|
7648
7649
|
e.stopPropagation();
|
|
7649
7650
|
this.debugLog('Settings button clicked!');
|
|
7650
7651
|
this.debugLog('Settings menu before update:', settingsMenu?.innerHTML?.length || 0, 'characters');
|
|
7651
|
-
|
|
7652
|
+
|
|
7652
7653
|
// Update the menu content before showing it
|
|
7653
7654
|
this.updateSettingsMenu();
|
|
7654
|
-
|
|
7655
|
+
|
|
7655
7656
|
this.debugLog('Settings menu after update:', settingsMenu?.innerHTML?.length || 0, 'characters');
|
|
7656
7657
|
this.debugLog('Settings menu classes before toggle:', Array.from(settingsMenu?.classList || []).join(' '));
|
|
7657
|
-
|
|
7658
|
+
|
|
7658
7659
|
settingsMenu?.classList.toggle('active');
|
|
7659
|
-
|
|
7660
|
+
|
|
7660
7661
|
// Force visibility if menu is active, hide if not active
|
|
7661
7662
|
if (settingsMenu) {
|
|
7662
7663
|
if (settingsMenu.classList.contains('active')) {
|
|
@@ -7681,13 +7682,13 @@ export class WebPlayer extends BasePlayer {
|
|
|
7681
7682
|
this.debugLog('Applied fallback styles to hide menu');
|
|
7682
7683
|
}
|
|
7683
7684
|
}
|
|
7684
|
-
|
|
7685
|
+
|
|
7685
7686
|
this.debugLog('Settings menu classes after toggle:', Array.from(settingsMenu?.classList || []).join(' '));
|
|
7686
7687
|
this.debugLog('Settings menu computed display:', window.getComputedStyle(settingsMenu || document.body).display);
|
|
7687
7688
|
this.debugLog('Settings menu computed visibility:', window.getComputedStyle(settingsMenu || document.body).visibility);
|
|
7688
7689
|
this.debugLog('Settings menu computed opacity:', window.getComputedStyle(settingsMenu || document.body).opacity);
|
|
7689
7690
|
});
|
|
7690
|
-
|
|
7691
|
+
|
|
7691
7692
|
// EPG button
|
|
7692
7693
|
const epgBtn = document.getElementById('uvf-epg-btn');
|
|
7693
7694
|
epgBtn?.addEventListener('click', (e) => {
|
|
@@ -7696,16 +7697,16 @@ export class WebPlayer extends BasePlayer {
|
|
|
7696
7697
|
// Trigger custom event for EPG toggle
|
|
7697
7698
|
this.emit('epgToggle', {});
|
|
7698
7699
|
});
|
|
7699
|
-
|
|
7700
|
+
|
|
7700
7701
|
// PiP button
|
|
7701
7702
|
const pipBtn = document.getElementById('uvf-pip-btn');
|
|
7702
7703
|
pipBtn?.addEventListener('click', () => this.togglePiP());
|
|
7703
|
-
|
|
7704
|
+
|
|
7704
7705
|
// Top control buttons
|
|
7705
7706
|
const castBtn = document.getElementById('uvf-cast-btn');
|
|
7706
7707
|
const stopCastBtn = document.getElementById('uvf-stop-cast-btn');
|
|
7707
7708
|
const shareBtn = document.getElementById('uvf-share-btn');
|
|
7708
|
-
|
|
7709
|
+
|
|
7709
7710
|
// Update cast button icon and functionality for iOS (AirPlay)
|
|
7710
7711
|
if (this.isIOSDevice() && castBtn) {
|
|
7711
7712
|
castBtn.innerHTML = `
|
|
@@ -7716,19 +7717,19 @@ export class WebPlayer extends BasePlayer {
|
|
|
7716
7717
|
castBtn.setAttribute('title', 'AirPlay');
|
|
7717
7718
|
castBtn.setAttribute('aria-label', 'AirPlay');
|
|
7718
7719
|
}
|
|
7719
|
-
|
|
7720
|
+
|
|
7720
7721
|
castBtn?.addEventListener('click', () => this.onCastButtonClick());
|
|
7721
7722
|
stopCastBtn?.addEventListener('click', () => this.stopCasting());
|
|
7722
7723
|
shareBtn?.addEventListener('click', () => this.shareVideo());
|
|
7723
|
-
|
|
7724
|
+
|
|
7724
7725
|
// Hide settings menu when clicking outside or pressing Escape
|
|
7725
7726
|
document.addEventListener('click', (e) => {
|
|
7726
|
-
if (!(e.target as HTMLElement).closest('#uvf-settings-btn') &&
|
|
7727
|
-
|
|
7727
|
+
if (!(e.target as HTMLElement).closest('#uvf-settings-btn') &&
|
|
7728
|
+
!(e.target as HTMLElement).closest('#uvf-settings-menu')) {
|
|
7728
7729
|
this.hideSettingsMenu();
|
|
7729
7730
|
}
|
|
7730
7731
|
});
|
|
7731
|
-
|
|
7732
|
+
|
|
7732
7733
|
// Add Escape key handler for settings menu
|
|
7733
7734
|
document.addEventListener('keydown', (e) => {
|
|
7734
7735
|
if (e.key === 'Escape' && settingsMenu?.classList.contains('active')) {
|
|
@@ -7736,7 +7737,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7736
7737
|
}
|
|
7737
7738
|
});
|
|
7738
7739
|
}
|
|
7739
|
-
|
|
7740
|
+
|
|
7740
7741
|
protected setupKeyboardShortcuts(): void {
|
|
7741
7742
|
// Add keyboard event listener to both document and player wrapper for better coverage
|
|
7742
7743
|
const handleKeydown = (e: KeyboardEvent) => {
|
|
@@ -7745,23 +7746,23 @@ export class WebPlayer extends BasePlayer {
|
|
|
7745
7746
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
|
7746
7747
|
return;
|
|
7747
7748
|
}
|
|
7748
|
-
|
|
7749
|
+
|
|
7749
7750
|
// Block all keyboard controls during ads (Google IMA handles ad controls)
|
|
7750
7751
|
if (this.isAdPlaying) {
|
|
7751
7752
|
this.debugLog('Keyboard blocked: Ad is playing');
|
|
7752
7753
|
e.preventDefault();
|
|
7753
7754
|
return;
|
|
7754
7755
|
}
|
|
7755
|
-
|
|
7756
|
+
|
|
7756
7757
|
// Debug logging
|
|
7757
7758
|
this.debugLog('Keyboard event:', e.key, 'target:', target.tagName);
|
|
7758
|
-
|
|
7759
|
+
|
|
7759
7760
|
let shortcutText = '';
|
|
7760
|
-
|
|
7761
|
+
|
|
7761
7762
|
// Update interaction timestamp
|
|
7762
7763
|
this.lastUserInteraction = Date.now();
|
|
7763
|
-
|
|
7764
|
-
switch(e.key) {
|
|
7764
|
+
|
|
7765
|
+
switch (e.key) {
|
|
7765
7766
|
case ' ':
|
|
7766
7767
|
case 'Spacebar': // For older browsers
|
|
7767
7768
|
case 'k':
|
|
@@ -7772,13 +7773,13 @@ export class WebPlayer extends BasePlayer {
|
|
|
7772
7773
|
videoPaused: this.video?.paused,
|
|
7773
7774
|
videoExists: !!this.video
|
|
7774
7775
|
});
|
|
7775
|
-
|
|
7776
|
+
|
|
7776
7777
|
// Determine what action we're about to take based on current video state
|
|
7777
7778
|
const willPlay = this.video?.paused || false;
|
|
7778
7779
|
this.debugLog('Will perform action:', willPlay ? 'PLAY' : 'PAUSE');
|
|
7779
|
-
|
|
7780
|
+
|
|
7780
7781
|
this.togglePlayPause();
|
|
7781
|
-
|
|
7782
|
+
|
|
7782
7783
|
// Show the action we're taking, not the current state
|
|
7783
7784
|
shortcutText = willPlay ? 'Play' : 'Pause';
|
|
7784
7785
|
this.debugLog('Showing icon:', shortcutText);
|
|
@@ -7844,7 +7845,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
7844
7845
|
break;
|
|
7845
7846
|
case 'f':
|
|
7846
7847
|
e.preventDefault();
|
|
7847
|
-
|
|
7848
|
+
|
|
7848
7849
|
if (!document.fullscreenElement) {
|
|
7849
7850
|
// Always use the fullscreen button for maximum reliability
|
|
7850
7851
|
this.triggerFullscreenButton();
|
|
@@ -7888,26 +7889,26 @@ export class WebPlayer extends BasePlayer {
|
|
|
7888
7889
|
}
|
|
7889
7890
|
break;
|
|
7890
7891
|
}
|
|
7891
|
-
|
|
7892
|
+
|
|
7892
7893
|
if (shortcutText) {
|
|
7893
7894
|
this.debugLog('Showing shortcut indicator:', shortcutText);
|
|
7894
7895
|
this.showShortcutIndicator(shortcutText);
|
|
7895
7896
|
}
|
|
7896
7897
|
};
|
|
7897
|
-
|
|
7898
|
+
|
|
7898
7899
|
// Add event listeners to multiple targets for better coverage
|
|
7899
7900
|
document.addEventListener('keydown', handleKeydown, { capture: true });
|
|
7900
|
-
|
|
7901
|
+
|
|
7901
7902
|
// Also add to the player wrapper if it exists
|
|
7902
7903
|
if (this.playerWrapper) {
|
|
7903
7904
|
this.playerWrapper.addEventListener('keydown', handleKeydown);
|
|
7904
7905
|
this.playerWrapper.setAttribute('tabindex', '0'); // Make it focusable
|
|
7905
|
-
|
|
7906
|
+
|
|
7906
7907
|
// Add visual feedback when player is focused for better UX
|
|
7907
7908
|
this.playerWrapper.addEventListener('focus', () => {
|
|
7908
7909
|
this.debugLog('Player focused - keyboard shortcuts available');
|
|
7909
7910
|
});
|
|
7910
|
-
|
|
7911
|
+
|
|
7911
7912
|
// Auto-focus the player when clicked to enable keyboard shortcuts
|
|
7912
7913
|
this.playerWrapper.addEventListener('click', (e) => {
|
|
7913
7914
|
// Don't focus if clicking on a control button
|
|
@@ -7918,17 +7919,17 @@ export class WebPlayer extends BasePlayer {
|
|
|
7918
7919
|
this.lastUserInteraction = Date.now();
|
|
7919
7920
|
}
|
|
7920
7921
|
});
|
|
7921
|
-
|
|
7922
|
+
|
|
7922
7923
|
// Also focus on any interaction with the video area
|
|
7923
7924
|
this.playerWrapper.addEventListener('mousedown', () => {
|
|
7924
7925
|
this.playerWrapper?.focus();
|
|
7925
7926
|
this.lastUserInteraction = Date.now();
|
|
7926
7927
|
});
|
|
7927
|
-
|
|
7928
|
+
|
|
7928
7929
|
// Advanced tap handling system for mobile
|
|
7929
7930
|
this.setupAdvancedTapHandling();
|
|
7930
7931
|
}
|
|
7931
|
-
|
|
7932
|
+
|
|
7932
7933
|
// Add to the video element
|
|
7933
7934
|
if (this.video) {
|
|
7934
7935
|
this.video.addEventListener('keydown', handleKeydown);
|
|
@@ -7937,25 +7938,25 @@ export class WebPlayer extends BasePlayer {
|
|
|
7937
7938
|
|
|
7938
7939
|
protected setupWatermark(): void {
|
|
7939
7940
|
if (!this.watermarkCanvas) return;
|
|
7940
|
-
|
|
7941
|
+
|
|
7941
7942
|
// Get watermark configuration
|
|
7942
7943
|
const watermarkConfig = (this.config as any).watermark;
|
|
7943
|
-
|
|
7944
|
+
|
|
7944
7945
|
// Check if watermark is disabled or not configured
|
|
7945
7946
|
if (!watermarkConfig || watermarkConfig.enabled === false) {
|
|
7946
7947
|
this.debugLog('Watermark disabled or not configured');
|
|
7947
7948
|
return;
|
|
7948
7949
|
}
|
|
7949
|
-
|
|
7950
|
+
|
|
7950
7951
|
// If watermark config exists but enabled is not explicitly set, default to disabled
|
|
7951
7952
|
if (watermarkConfig.enabled !== true) {
|
|
7952
7953
|
this.debugLog('Watermark not explicitly enabled');
|
|
7953
7954
|
return;
|
|
7954
7955
|
}
|
|
7955
|
-
|
|
7956
|
+
|
|
7956
7957
|
const ctx = this.watermarkCanvas.getContext('2d');
|
|
7957
7958
|
if (!ctx) return;
|
|
7958
|
-
|
|
7959
|
+
|
|
7959
7960
|
// Default configuration values
|
|
7960
7961
|
const config = {
|
|
7961
7962
|
text: watermarkConfig.text || 'PREMIUM',
|
|
@@ -7971,31 +7972,31 @@ export class WebPlayer extends BasePlayer {
|
|
|
7971
7972
|
gradientColors: watermarkConfig.style?.gradientColors || ['#ff0000', '#ff4d4f']
|
|
7972
7973
|
}
|
|
7973
7974
|
};
|
|
7974
|
-
|
|
7975
|
+
|
|
7975
7976
|
this.debugLog('Watermark configuration:', config);
|
|
7976
|
-
|
|
7977
|
+
|
|
7977
7978
|
const renderWatermark = () => {
|
|
7978
7979
|
const container = this.watermarkCanvas!.parentElement;
|
|
7979
7980
|
if (!container) return;
|
|
7980
|
-
|
|
7981
|
+
|
|
7981
7982
|
this.watermarkCanvas!.width = container.offsetWidth;
|
|
7982
7983
|
this.watermarkCanvas!.height = container.offsetHeight;
|
|
7983
|
-
|
|
7984
|
+
|
|
7984
7985
|
ctx.clearRect(0, 0, this.watermarkCanvas!.width, this.watermarkCanvas!.height);
|
|
7985
|
-
|
|
7986
|
+
|
|
7986
7987
|
// Build watermark text
|
|
7987
7988
|
let text = config.text;
|
|
7988
7989
|
if (config.showTime) {
|
|
7989
7990
|
const timeStr = new Date().toLocaleTimeString();
|
|
7990
7991
|
text += ` • ${timeStr}`;
|
|
7991
7992
|
}
|
|
7992
|
-
|
|
7993
|
+
|
|
7993
7994
|
// Set up styling
|
|
7994
7995
|
ctx.save();
|
|
7995
7996
|
ctx.globalAlpha = config.style.opacity;
|
|
7996
7997
|
ctx.font = `${config.style.fontSize}px ${config.style.fontFamily}`;
|
|
7997
7998
|
ctx.textAlign = 'left';
|
|
7998
|
-
|
|
7999
|
+
|
|
7999
8000
|
// Set fill style
|
|
8000
8001
|
if (config.style.color) {
|
|
8001
8002
|
// Use solid color
|
|
@@ -8005,7 +8006,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8005
8006
|
const wrapper = this.playerWrapper as HTMLElement | null;
|
|
8006
8007
|
let c1 = config.style.gradientColors[0];
|
|
8007
8008
|
let c2 = config.style.gradientColors[1];
|
|
8008
|
-
|
|
8009
|
+
|
|
8009
8010
|
// Try to get theme colors if using defaults
|
|
8010
8011
|
if (!watermarkConfig.style?.gradientColors) {
|
|
8011
8012
|
try {
|
|
@@ -8016,18 +8017,18 @@ export class WebPlayer extends BasePlayer {
|
|
|
8016
8017
|
if (v1) c1 = v1;
|
|
8017
8018
|
if (v2) c2 = v2;
|
|
8018
8019
|
}
|
|
8019
|
-
} catch (_) {}
|
|
8020
|
+
} catch (_) { }
|
|
8020
8021
|
}
|
|
8021
|
-
|
|
8022
|
+
|
|
8022
8023
|
const gradient = ctx.createLinearGradient(0, 0, 200, 0);
|
|
8023
8024
|
gradient.addColorStop(0, c1);
|
|
8024
8025
|
gradient.addColorStop(1, c2);
|
|
8025
8026
|
ctx.fillStyle = gradient;
|
|
8026
8027
|
}
|
|
8027
|
-
|
|
8028
|
+
|
|
8028
8029
|
// Calculate position
|
|
8029
8030
|
let x: number, y: number;
|
|
8030
|
-
|
|
8031
|
+
|
|
8031
8032
|
if (config.randomPosition) {
|
|
8032
8033
|
// Random position (default behavior)
|
|
8033
8034
|
x = 20 + Math.random() * Math.max(0, this.watermarkCanvas!.width - 200);
|
|
@@ -8036,7 +8037,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8036
8037
|
// Fixed or calculated position
|
|
8037
8038
|
const posX = config.position.x;
|
|
8038
8039
|
const posY = config.position.y;
|
|
8039
|
-
|
|
8040
|
+
|
|
8040
8041
|
// Calculate X position
|
|
8041
8042
|
if (typeof posX === 'number') {
|
|
8042
8043
|
x = posX;
|
|
@@ -8060,7 +8061,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8060
8061
|
x = 20; // default left
|
|
8061
8062
|
}
|
|
8062
8063
|
}
|
|
8063
|
-
|
|
8064
|
+
|
|
8064
8065
|
// Calculate Y position
|
|
8065
8066
|
if (typeof posY === 'number') {
|
|
8066
8067
|
y = posY;
|
|
@@ -8083,18 +8084,18 @@ export class WebPlayer extends BasePlayer {
|
|
|
8083
8084
|
}
|
|
8084
8085
|
}
|
|
8085
8086
|
}
|
|
8086
|
-
|
|
8087
|
+
|
|
8087
8088
|
// Render the watermark
|
|
8088
8089
|
ctx.fillText(text, x, y);
|
|
8089
8090
|
ctx.restore();
|
|
8090
|
-
|
|
8091
|
+
|
|
8091
8092
|
this.debugLog('Watermark rendered:', { text, x, y });
|
|
8092
8093
|
};
|
|
8093
|
-
|
|
8094
|
+
|
|
8094
8095
|
// Set up interval with configured frequency
|
|
8095
8096
|
setInterval(renderWatermark, config.updateInterval);
|
|
8096
8097
|
renderWatermark(); // Render immediately
|
|
8097
|
-
|
|
8098
|
+
|
|
8098
8099
|
this.debugLog('Watermark setup complete with update interval:', config.updateInterval + 'ms');
|
|
8099
8100
|
}
|
|
8100
8101
|
|
|
@@ -8109,12 +8110,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
8109
8110
|
import('./paywall/PaywallController').then((m: any) => {
|
|
8110
8111
|
this.paywallController = new m.PaywallController(config, {
|
|
8111
8112
|
getOverlayContainer: () => this.playerWrapper,
|
|
8112
|
-
onResume: (accessInfo?: any) => {
|
|
8113
|
-
try {
|
|
8113
|
+
onResume: (accessInfo?: any) => {
|
|
8114
|
+
try {
|
|
8114
8115
|
// Reset preview gate after successful auth/payment
|
|
8115
8116
|
this.previewGateHit = false;
|
|
8116
8117
|
this.paymentSuccessTime = Date.now();
|
|
8117
|
-
|
|
8118
|
+
|
|
8118
8119
|
// Check if access was granted via email auth
|
|
8119
8120
|
if (accessInfo && (accessInfo.accessGranted || accessInfo.paymentSuccessful)) {
|
|
8120
8121
|
this.paymentSuccessful = true;
|
|
@@ -8123,22 +8124,22 @@ export class WebPlayer extends BasePlayer {
|
|
|
8123
8124
|
this.paymentSuccessful = true;
|
|
8124
8125
|
this.debugLog('Payment successful (via setPaywallConfig) - preview gate permanently disabled, resuming playback');
|
|
8125
8126
|
}
|
|
8126
|
-
|
|
8127
|
-
this.play();
|
|
8128
|
-
} catch(_) {}
|
|
8127
|
+
|
|
8128
|
+
this.play();
|
|
8129
|
+
} catch (_) { }
|
|
8129
8130
|
},
|
|
8130
8131
|
onShow: () => {
|
|
8131
8132
|
// Use safe pause method to avoid race conditions
|
|
8132
|
-
try { this.requestPause(); } catch(_) {}
|
|
8133
|
+
try { this.requestPause(); } catch (_) { }
|
|
8133
8134
|
},
|
|
8134
8135
|
onClose: () => {
|
|
8135
8136
|
// Resume video if auth was successful
|
|
8136
8137
|
}
|
|
8137
8138
|
});
|
|
8138
|
-
}).catch(() => {});
|
|
8139
|
+
}).catch(() => { });
|
|
8139
8140
|
}
|
|
8140
8141
|
}
|
|
8141
|
-
} catch (_) {}
|
|
8142
|
+
} catch (_) { }
|
|
8142
8143
|
}
|
|
8143
8144
|
|
|
8144
8145
|
private togglePlayPause(): void {
|
|
@@ -8148,12 +8149,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
8148
8149
|
youtubePlayerReady: this.youtubePlayerReady,
|
|
8149
8150
|
playerState: this.state
|
|
8150
8151
|
});
|
|
8151
|
-
|
|
8152
|
+
|
|
8152
8153
|
// Handle YouTube player
|
|
8153
8154
|
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
8154
8155
|
const playerState = this.youtubePlayer.getPlayerState();
|
|
8155
8156
|
this.debugLog('YouTube player state:', playerState);
|
|
8156
|
-
|
|
8157
|
+
|
|
8157
8158
|
if (playerState === window.YT.PlayerState.PLAYING) {
|
|
8158
8159
|
this.debugLog('YouTube video is playing, calling pause()');
|
|
8159
8160
|
this.pause();
|
|
@@ -8163,13 +8164,13 @@ export class WebPlayer extends BasePlayer {
|
|
|
8163
8164
|
}
|
|
8164
8165
|
return;
|
|
8165
8166
|
}
|
|
8166
|
-
|
|
8167
|
+
|
|
8167
8168
|
// Handle regular video element
|
|
8168
8169
|
if (!this.video) {
|
|
8169
8170
|
this.debugError('No video element or YouTube player available for toggle');
|
|
8170
8171
|
return;
|
|
8171
8172
|
}
|
|
8172
|
-
|
|
8173
|
+
|
|
8173
8174
|
if (this.video.paused) {
|
|
8174
8175
|
this.debugLog('Video is paused, calling play()');
|
|
8175
8176
|
this.play();
|
|
@@ -8185,25 +8186,25 @@ export class WebPlayer extends BasePlayer {
|
|
|
8185
8186
|
const lim = Number(this.config.freeDuration || 0);
|
|
8186
8187
|
if (!(lim > 0)) return;
|
|
8187
8188
|
if (this.previewGateHit && !fromSeek) return;
|
|
8188
|
-
|
|
8189
|
+
|
|
8189
8190
|
// Don't trigger gate if payment was successful for this session
|
|
8190
8191
|
if (this.paymentSuccessful) {
|
|
8191
8192
|
this.debugLog('Skipping preview gate - payment was successful, access granted permanently for this session');
|
|
8192
8193
|
return;
|
|
8193
8194
|
}
|
|
8194
|
-
|
|
8195
|
+
|
|
8195
8196
|
// Don't trigger gate if payment was successful recently (within 5 seconds)
|
|
8196
8197
|
const timeSincePayment = Date.now() - this.paymentSuccessTime;
|
|
8197
8198
|
if (this.paymentSuccessTime > 0 && timeSincePayment < 5000) {
|
|
8198
8199
|
this.debugLog('Skipping preview gate - recent payment success:', timeSincePayment + 'ms ago');
|
|
8199
8200
|
return;
|
|
8200
8201
|
}
|
|
8201
|
-
|
|
8202
|
+
|
|
8202
8203
|
if (current >= lim - 0.01 && !this.previewGateHit) {
|
|
8203
8204
|
this.previewGateHit = true;
|
|
8204
8205
|
this.showNotification('Free preview ended.');
|
|
8205
8206
|
this.emit('onFreePreviewEnded');
|
|
8206
|
-
|
|
8207
|
+
|
|
8207
8208
|
// Trigger paywall controller which will handle auth/payment flow
|
|
8208
8209
|
this.debugLog('Free preview gate hit, paywallController exists:', !!this.paywallController);
|
|
8209
8210
|
if (this.paywallController) {
|
|
@@ -8219,18 +8220,18 @@ export class WebPlayer extends BasePlayer {
|
|
|
8219
8220
|
if (this.remotePlayer && this.remotePlayer.isPaused === false) {
|
|
8220
8221
|
this.remoteController.playOrPause();
|
|
8221
8222
|
}
|
|
8222
|
-
} catch (_) {}
|
|
8223
|
+
} catch (_) { }
|
|
8223
8224
|
} else if (this.video) {
|
|
8224
|
-
try {
|
|
8225
|
+
try {
|
|
8225
8226
|
// Use deferred pause to avoid race conditions
|
|
8226
|
-
this.requestPause();
|
|
8227
|
+
this.requestPause();
|
|
8227
8228
|
if (fromSeek || ((this.video.currentTime || 0) > lim)) {
|
|
8228
8229
|
this.safeSetCurrentTime(lim - 0.1);
|
|
8229
8230
|
}
|
|
8230
|
-
} catch (_) {}
|
|
8231
|
+
} catch (_) { }
|
|
8231
8232
|
}
|
|
8232
8233
|
}
|
|
8233
|
-
} catch (_) {}
|
|
8234
|
+
} catch (_) { }
|
|
8234
8235
|
}
|
|
8235
8236
|
|
|
8236
8237
|
// Public runtime controls for free preview
|
|
@@ -8247,7 +8248,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8247
8248
|
const cur = this.video ? (this.video.currentTime || 0) : 0;
|
|
8248
8249
|
this.enforceFreePreviewGate(cur, true);
|
|
8249
8250
|
}
|
|
8250
|
-
} catch (_) {}
|
|
8251
|
+
} catch (_) { }
|
|
8251
8252
|
}
|
|
8252
8253
|
public resetFreePreviewGate(): void {
|
|
8253
8254
|
// Only reset if payment hasn't been successful
|
|
@@ -8255,7 +8256,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8255
8256
|
this.previewGateHit = false;
|
|
8256
8257
|
}
|
|
8257
8258
|
}
|
|
8258
|
-
|
|
8259
|
+
|
|
8259
8260
|
public resetPaymentStatus(): void {
|
|
8260
8261
|
this.paymentSuccessful = false;
|
|
8261
8262
|
this.paymentSuccessTime = 0;
|
|
@@ -8265,7 +8266,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8265
8266
|
|
|
8266
8267
|
private toggleMuteAction(): void {
|
|
8267
8268
|
if (this.isCasting && this.remoteController) {
|
|
8268
|
-
try { this.remoteController.muteOrUnmute(); } catch (_) {}
|
|
8269
|
+
try { this.remoteController.muteOrUnmute(); } catch (_) { }
|
|
8269
8270
|
return;
|
|
8270
8271
|
}
|
|
8271
8272
|
if (this.video?.muted) {
|
|
@@ -8284,7 +8285,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8284
8285
|
const isMobileUserAgent = mobileKeywords.some(keyword => userAgent.includes(keyword));
|
|
8285
8286
|
const isSmallScreen = window.innerWidth <= 768;
|
|
8286
8287
|
const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
8287
|
-
|
|
8288
|
+
|
|
8288
8289
|
return isMobileUserAgent || (isSmallScreen && hasTouchScreen);
|
|
8289
8290
|
}
|
|
8290
8291
|
|
|
@@ -8391,21 +8392,21 @@ export class WebPlayer extends BasePlayer {
|
|
|
8391
8392
|
private handleVolumeChange(e: MouseEvent): void {
|
|
8392
8393
|
const slider = document.getElementById('uvf-volume-slider');
|
|
8393
8394
|
if (!slider) return;
|
|
8394
|
-
|
|
8395
|
+
|
|
8395
8396
|
const rect = slider.getBoundingClientRect();
|
|
8396
8397
|
const x = e.clientX - rect.left;
|
|
8397
8398
|
const width = rect.width;
|
|
8398
8399
|
const percent = Math.max(0, Math.min(1, x / width));
|
|
8399
|
-
|
|
8400
|
+
|
|
8400
8401
|
if (this.isCasting && this.remoteController && this.remotePlayer) {
|
|
8401
8402
|
try {
|
|
8402
8403
|
if (this.remotePlayer.isMuted) {
|
|
8403
|
-
try { this.remoteController.muteOrUnmute(); } catch (_) {}
|
|
8404
|
+
try { this.remoteController.muteOrUnmute(); } catch (_) { }
|
|
8404
8405
|
this.remotePlayer.isMuted = false;
|
|
8405
8406
|
}
|
|
8406
8407
|
this.remotePlayer.volumeLevel = percent;
|
|
8407
8408
|
this.remoteController.setVolumeLevel();
|
|
8408
|
-
} catch (_) {}
|
|
8409
|
+
} catch (_) { }
|
|
8409
8410
|
this.updateVolumeUIFromRemote();
|
|
8410
8411
|
} else if (this.video) {
|
|
8411
8412
|
this.setVolume(percent);
|
|
@@ -8418,25 +8419,25 @@ export class WebPlayer extends BasePlayer {
|
|
|
8418
8419
|
const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
|
|
8419
8420
|
const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
|
|
8420
8421
|
if (!progressBar || !this.video) return;
|
|
8421
|
-
|
|
8422
|
+
|
|
8422
8423
|
const duration = this.video.duration;
|
|
8423
8424
|
// Validate duration before calculating seek time
|
|
8424
8425
|
if (!isFinite(duration) || isNaN(duration) || duration <= 0) {
|
|
8425
8426
|
this.debugWarn('Invalid video duration, cannot seek via progress bar');
|
|
8426
8427
|
return;
|
|
8427
8428
|
}
|
|
8428
|
-
|
|
8429
|
+
|
|
8429
8430
|
const rect = progressBar.getBoundingClientRect();
|
|
8430
8431
|
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
8431
8432
|
const percent = (x / rect.width) * 100;
|
|
8432
8433
|
const time = (percent / 100) * duration;
|
|
8433
|
-
|
|
8434
|
+
|
|
8434
8435
|
// Validate calculated time
|
|
8435
8436
|
if (!isFinite(time) || isNaN(time)) {
|
|
8436
8437
|
this.debugWarn('Calculated seek time is invalid:', time);
|
|
8437
8438
|
return;
|
|
8438
8439
|
}
|
|
8439
|
-
|
|
8440
|
+
|
|
8440
8441
|
// Update UI immediately for responsive feedback
|
|
8441
8442
|
if (progressFilled) {
|
|
8442
8443
|
progressFilled.style.width = percent + '%';
|
|
@@ -8450,17 +8451,17 @@ export class WebPlayer extends BasePlayer {
|
|
|
8450
8451
|
progressHandle.classList.remove('dragging');
|
|
8451
8452
|
}
|
|
8452
8453
|
}
|
|
8453
|
-
|
|
8454
|
+
|
|
8454
8455
|
this.seek(time);
|
|
8455
8456
|
}
|
|
8456
8457
|
|
|
8457
8458
|
private formatTime(seconds: number): string {
|
|
8458
8459
|
if (!seconds || isNaN(seconds)) return '00:00';
|
|
8459
|
-
|
|
8460
|
+
|
|
8460
8461
|
const hours = Math.floor(seconds / 3600);
|
|
8461
8462
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
8462
8463
|
const secs = Math.floor(seconds % 60);
|
|
8463
|
-
|
|
8464
|
+
|
|
8464
8465
|
if (hours > 0) {
|
|
8465
8466
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
8466
8467
|
} else {
|
|
@@ -8471,10 +8472,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
8471
8472
|
private updateTimeDisplay(): void {
|
|
8472
8473
|
const timeDisplay = document.getElementById('uvf-time-display');
|
|
8473
8474
|
if (!timeDisplay) return;
|
|
8474
|
-
|
|
8475
|
+
|
|
8475
8476
|
let current = 0;
|
|
8476
8477
|
let duration = 0;
|
|
8477
|
-
|
|
8478
|
+
|
|
8478
8479
|
// Get time from YouTube player if available
|
|
8479
8480
|
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
8480
8481
|
try {
|
|
@@ -8487,7 +8488,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8487
8488
|
current = this.video.currentTime || 0;
|
|
8488
8489
|
duration = this.video.duration || 0;
|
|
8489
8490
|
}
|
|
8490
|
-
|
|
8491
|
+
|
|
8491
8492
|
const currentFormatted = this.formatTime(current);
|
|
8492
8493
|
const durationFormatted = this.formatTime(duration);
|
|
8493
8494
|
timeDisplay.textContent = `${currentFormatted} / ${durationFormatted}`;
|
|
@@ -8505,7 +8506,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8505
8506
|
|
|
8506
8507
|
private hideControls(): void {
|
|
8507
8508
|
if (!this.state.isPlaying) return;
|
|
8508
|
-
|
|
8509
|
+
|
|
8509
8510
|
const wrapper = this.container?.querySelector('.uvf-player-wrapper');
|
|
8510
8511
|
if (wrapper) {
|
|
8511
8512
|
wrapper.classList.remove('controls-visible');
|
|
@@ -8515,7 +8516,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8515
8516
|
|
|
8516
8517
|
private scheduleHideControls(): void {
|
|
8517
8518
|
if (!this.state.isPlaying) return;
|
|
8518
|
-
|
|
8519
|
+
|
|
8519
8520
|
if (this.hideControlsTimeout) clearTimeout(this.hideControlsTimeout);
|
|
8520
8521
|
// Use longer timeout in fullscreen for better UX
|
|
8521
8522
|
const timeout = this.isFullscreen() ? 4000 : 3000;
|
|
@@ -8542,7 +8543,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8542
8543
|
const TAP_MOVEMENT_THRESHOLD = 10; // pixels
|
|
8543
8544
|
const SKIP_SECONDS = 10;
|
|
8544
8545
|
const FAST_PLAYBACK_RATE = 2;
|
|
8545
|
-
|
|
8546
|
+
|
|
8546
8547
|
// Track if we're currently in a double-tap window
|
|
8547
8548
|
let inDoubleTapWindow = false;
|
|
8548
8549
|
|
|
@@ -8663,7 +8664,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8663
8664
|
this.debugLog('Single tap detected - toggling controls');
|
|
8664
8665
|
const wrapper = this.container?.querySelector('.uvf-player-wrapper');
|
|
8665
8666
|
const areControlsVisible = wrapper?.classList.contains('controls-visible');
|
|
8666
|
-
|
|
8667
|
+
|
|
8667
8668
|
if (areControlsVisible) {
|
|
8668
8669
|
// Hide controls and top UI elements
|
|
8669
8670
|
this.hideControls();
|
|
@@ -8672,7 +8673,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8672
8673
|
// Show controls and top UI elements
|
|
8673
8674
|
this.showControls();
|
|
8674
8675
|
this.debugLog('Single tap: showing controls');
|
|
8675
|
-
|
|
8676
|
+
|
|
8676
8677
|
// Schedule auto-hide if video is playing
|
|
8677
8678
|
if (this.state.isPlaying) {
|
|
8678
8679
|
this.scheduleHideControls();
|
|
@@ -8692,7 +8693,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8692
8693
|
|
|
8693
8694
|
const currentTime = this.video.currentTime;
|
|
8694
8695
|
const duration = this.video.duration;
|
|
8695
|
-
|
|
8696
|
+
|
|
8696
8697
|
// Validate current time and duration before calculating new time
|
|
8697
8698
|
if (!isFinite(currentTime) || isNaN(currentTime) || !isFinite(duration) || isNaN(duration)) {
|
|
8698
8699
|
this.debugWarn('Invalid video time values, skipping double-tap action');
|
|
@@ -8788,19 +8789,19 @@ export class WebPlayer extends BasePlayer {
|
|
|
8788
8789
|
this.fastBackwardInterval = null;
|
|
8789
8790
|
}
|
|
8790
8791
|
}
|
|
8791
|
-
|
|
8792
|
+
|
|
8792
8793
|
private isFullscreen(): boolean {
|
|
8793
8794
|
return !!(document.fullscreenElement ||
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8795
|
+
(document as any).webkitFullscreenElement ||
|
|
8796
|
+
(document as any).mozFullScreenElement ||
|
|
8797
|
+
(document as any).msFullscreenElement);
|
|
8797
8798
|
}
|
|
8798
|
-
|
|
8799
|
+
|
|
8799
8800
|
private setupFullscreenListeners(): void {
|
|
8800
8801
|
// Handle fullscreen changes from browser/keyboard shortcuts
|
|
8801
8802
|
const handleFullscreenChange = () => {
|
|
8802
8803
|
const isFullscreen = this.isFullscreen();
|
|
8803
|
-
|
|
8804
|
+
|
|
8804
8805
|
if (this.playerWrapper) {
|
|
8805
8806
|
if (isFullscreen) {
|
|
8806
8807
|
this.playerWrapper.classList.add('uvf-fullscreen');
|
|
@@ -8808,36 +8809,36 @@ export class WebPlayer extends BasePlayer {
|
|
|
8808
8809
|
this.playerWrapper.classList.remove('uvf-fullscreen');
|
|
8809
8810
|
}
|
|
8810
8811
|
}
|
|
8811
|
-
|
|
8812
|
+
|
|
8812
8813
|
// Show controls when entering/exiting fullscreen
|
|
8813
8814
|
this.showControls();
|
|
8814
8815
|
if (isFullscreen && this.state.isPlaying) {
|
|
8815
8816
|
this.scheduleHideControls();
|
|
8816
8817
|
}
|
|
8817
|
-
|
|
8818
|
+
|
|
8818
8819
|
this.emit('onFullscreenChanged', isFullscreen);
|
|
8819
8820
|
};
|
|
8820
|
-
|
|
8821
|
+
|
|
8821
8822
|
// Listen for fullscreen change events (all browser prefixes)
|
|
8822
8823
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
8823
8824
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|
8824
8825
|
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
|
8825
8826
|
document.addEventListener('MSFullscreenChange', handleFullscreenChange);
|
|
8826
|
-
|
|
8827
|
+
|
|
8827
8828
|
// Enhanced mouse/touch movement detection for control visibility
|
|
8828
8829
|
let lastMouseMoveTime = 0;
|
|
8829
8830
|
let mouseInactivityTimeout: any = null;
|
|
8830
|
-
|
|
8831
|
+
|
|
8831
8832
|
const handleMouseMovement = () => {
|
|
8832
8833
|
const now = Date.now();
|
|
8833
8834
|
lastMouseMoveTime = now;
|
|
8834
|
-
|
|
8835
|
+
|
|
8835
8836
|
// Show controls immediately on mouse movement
|
|
8836
8837
|
this.showControls();
|
|
8837
|
-
|
|
8838
|
+
|
|
8838
8839
|
// Clear existing inactivity timeout
|
|
8839
8840
|
clearTimeout(mouseInactivityTimeout);
|
|
8840
|
-
|
|
8841
|
+
|
|
8841
8842
|
// Set new inactivity timeout
|
|
8842
8843
|
if (this.state.isPlaying) {
|
|
8843
8844
|
const timeout = this.isFullscreen() ? 4000 : 3000;
|
|
@@ -8849,7 +8850,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8849
8850
|
}, timeout);
|
|
8850
8851
|
}
|
|
8851
8852
|
};
|
|
8852
|
-
|
|
8853
|
+
|
|
8853
8854
|
// Touch movement detection for mobile - only for actual dragging/scrolling
|
|
8854
8855
|
// Note: Don't handle touchstart here as it conflicts with advanced tap handling
|
|
8855
8856
|
const handleTouchMovement = () => {
|
|
@@ -8859,7 +8860,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8859
8860
|
this.scheduleHideControls();
|
|
8860
8861
|
}
|
|
8861
8862
|
};
|
|
8862
|
-
|
|
8863
|
+
|
|
8863
8864
|
// Add event listeners to the player wrapper
|
|
8864
8865
|
if (this.playerWrapper) {
|
|
8865
8866
|
this.playerWrapper.addEventListener('mousemove', handleMouseMovement, { passive: true });
|
|
@@ -8869,9 +8870,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
8869
8870
|
this.playerWrapper.addEventListener('touchmove', handleTouchMovement, { passive: true });
|
|
8870
8871
|
}
|
|
8871
8872
|
}
|
|
8872
|
-
|
|
8873
8873
|
|
|
8874
|
-
|
|
8874
|
+
|
|
8875
|
+
|
|
8875
8876
|
private showShortcutIndicator(text: string): void {
|
|
8876
8877
|
const el = document.getElementById('uvf-shortcut-indicator');
|
|
8877
8878
|
this.debugLog('showShortcutIndicator called with:', text, 'element found:', !!el);
|
|
@@ -8891,7 +8892,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
8891
8892
|
el.innerHTML = `<div class="uvf-ki uvf-ki-icon">${svg}</div>`;
|
|
8892
8893
|
resetAnim();
|
|
8893
8894
|
};
|
|
8894
|
-
const setSkip = (dir: 'fwd'|'back', num: number) => {
|
|
8895
|
+
const setSkip = (dir: 'fwd' | 'back', num: number) => {
|
|
8895
8896
|
el.classList.add('uvf-ki-icon');
|
|
8896
8897
|
const svg = dir === 'fwd'
|
|
8897
8898
|
? `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12.01 19c-3.31 0-6-2.69-6-6s2.69-6 6-6V5l5 5-5 5V9c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4h2c0 3.31-2.69 6-6 6z"/></svg>`
|
|
@@ -8955,14 +8956,14 @@ export class WebPlayer extends BasePlayer {
|
|
|
8955
8956
|
// auto-hide after animation
|
|
8956
8957
|
clearTimeout(this._kiTo);
|
|
8957
8958
|
this._kiTo = setTimeout(() => {
|
|
8958
|
-
try { el.classList.remove('active'); } catch (_) {}
|
|
8959
|
+
try { el.classList.remove('active'); } catch (_) { }
|
|
8959
8960
|
}, 1000);
|
|
8960
8961
|
} catch (err) {
|
|
8961
8962
|
try {
|
|
8962
8963
|
(el as HTMLElement).textContent = String(text || '');
|
|
8963
8964
|
el.classList.add('active');
|
|
8964
8965
|
setTimeout(() => el.classList.remove('active'), 1000);
|
|
8965
|
-
} catch(_) {}
|
|
8966
|
+
} catch (_) { }
|
|
8966
8967
|
}
|
|
8967
8968
|
}
|
|
8968
8969
|
|
|
@@ -9070,7 +9071,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9070
9071
|
};
|
|
9071
9072
|
|
|
9072
9073
|
this.coreChapterManager = new CoreChapterManager(coreChapterConfig);
|
|
9073
|
-
|
|
9074
|
+
|
|
9074
9075
|
// Initialize the core chapter manager
|
|
9075
9076
|
this.coreChapterManager.initialize();
|
|
9076
9077
|
|
|
@@ -9177,7 +9178,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9177
9178
|
if (!this.chapterManager) {
|
|
9178
9179
|
throw new Error('Chapter manager not initialized. Enable chapters in config first.');
|
|
9179
9180
|
}
|
|
9180
|
-
|
|
9181
|
+
|
|
9181
9182
|
try {
|
|
9182
9183
|
await this.chapterManager.loadChapters(chapters);
|
|
9183
9184
|
this.debugLog('Chapters loaded successfully');
|
|
@@ -9194,7 +9195,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9194
9195
|
if (!this.chapterManager) {
|
|
9195
9196
|
throw new Error('Chapter manager not initialized. Enable chapters in config first.');
|
|
9196
9197
|
}
|
|
9197
|
-
|
|
9198
|
+
|
|
9198
9199
|
try {
|
|
9199
9200
|
await this.chapterManager.loadChaptersFromUrl(url);
|
|
9200
9201
|
this.debugLog('Chapters loaded from URL successfully');
|
|
@@ -9211,7 +9212,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9211
9212
|
if (!this.chapterManager || !this.video) {
|
|
9212
9213
|
return null;
|
|
9213
9214
|
}
|
|
9214
|
-
|
|
9215
|
+
|
|
9215
9216
|
return this.chapterManager.getCurrentSegment(this.video.currentTime);
|
|
9216
9217
|
}
|
|
9217
9218
|
|
|
@@ -9223,7 +9224,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9223
9224
|
this.debugWarn('Cannot skip segment: chapter manager not initialized');
|
|
9224
9225
|
return;
|
|
9225
9226
|
}
|
|
9226
|
-
|
|
9227
|
+
|
|
9227
9228
|
this.chapterManager.skipToSegment(segmentId);
|
|
9228
9229
|
}
|
|
9229
9230
|
|
|
@@ -9234,7 +9235,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9234
9235
|
if (!this.chapterManager) {
|
|
9235
9236
|
return [];
|
|
9236
9237
|
}
|
|
9237
|
-
|
|
9238
|
+
|
|
9238
9239
|
return this.chapterManager.getSegments();
|
|
9239
9240
|
}
|
|
9240
9241
|
|
|
@@ -9243,7 +9244,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9243
9244
|
*/
|
|
9244
9245
|
public updateChapterConfig(newConfig: Partial<ChapterConfig>): void {
|
|
9245
9246
|
this.chapterConfig = { ...this.chapterConfig, ...newConfig };
|
|
9246
|
-
|
|
9247
|
+
|
|
9247
9248
|
if (this.chapterManager) {
|
|
9248
9249
|
this.chapterManager.updateConfig(this.chapterConfig);
|
|
9249
9250
|
}
|
|
@@ -9370,7 +9371,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9370
9371
|
if (iconColor) wrapper.style.setProperty('--uvf-icon-color', iconColor);
|
|
9371
9372
|
if (textPrimary) wrapper.style.setProperty('--uvf-text-primary', textPrimary);
|
|
9372
9373
|
if (textSecondary) wrapper.style.setProperty('--uvf-text-secondary', textSecondary);
|
|
9373
|
-
|
|
9374
|
+
|
|
9374
9375
|
// Set overlay colors for gradient backgrounds
|
|
9375
9376
|
if (overlayStrong) wrapper.style.setProperty('--uvf-overlay-strong', overlayStrong);
|
|
9376
9377
|
if (overlayMedium) wrapper.style.setProperty('--uvf-overlay-medium', overlayMedium);
|
|
@@ -9406,7 +9407,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9406
9407
|
return { r: Math.round(nums[0]), g: Math.round(nums[1]), b: Math.round(nums[2]) };
|
|
9407
9408
|
}
|
|
9408
9409
|
}
|
|
9409
|
-
} catch (_) {}
|
|
9410
|
+
} catch (_) { }
|
|
9410
9411
|
return null;
|
|
9411
9412
|
}
|
|
9412
9413
|
|
|
@@ -9431,26 +9432,26 @@ export class WebPlayer extends BasePlayer {
|
|
|
9431
9432
|
const a = Math.max(0, Math.min(1, alpha));
|
|
9432
9433
|
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
|
|
9433
9434
|
}
|
|
9434
|
-
|
|
9435
|
+
|
|
9435
9436
|
private changeVolume(delta: number): void {
|
|
9436
9437
|
if (this.isCasting && this.remoteController && this.remotePlayer) {
|
|
9437
9438
|
const cur = this.remotePlayer.volumeLevel || 0;
|
|
9438
9439
|
const next = Math.max(0, Math.min(1, cur + delta));
|
|
9439
9440
|
try {
|
|
9440
9441
|
if (this.remotePlayer.isMuted) {
|
|
9441
|
-
try { this.remoteController.muteOrUnmute(); } catch (_) {}
|
|
9442
|
+
try { this.remoteController.muteOrUnmute(); } catch (_) { }
|
|
9442
9443
|
this.remotePlayer.isMuted = false;
|
|
9443
9444
|
}
|
|
9444
9445
|
this.remotePlayer.volumeLevel = next;
|
|
9445
9446
|
this.remoteController.setVolumeLevel();
|
|
9446
|
-
} catch (_) {}
|
|
9447
|
+
} catch (_) { }
|
|
9447
9448
|
this.updateVolumeUIFromRemote();
|
|
9448
9449
|
return;
|
|
9449
9450
|
}
|
|
9450
9451
|
if (!this.video) return;
|
|
9451
9452
|
this.video.volume = Math.max(0, Math.min(1, this.video.volume + delta));
|
|
9452
9453
|
}
|
|
9453
|
-
|
|
9454
|
+
|
|
9454
9455
|
private setSpeed(speed: number): void {
|
|
9455
9456
|
// Handle YouTube player
|
|
9456
9457
|
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
@@ -9462,7 +9463,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9462
9463
|
} else if (this.video) {
|
|
9463
9464
|
this.video.playbackRate = speed;
|
|
9464
9465
|
}
|
|
9465
|
-
|
|
9466
|
+
|
|
9466
9467
|
// Update UI
|
|
9467
9468
|
document.querySelectorAll('.speed-option').forEach(option => {
|
|
9468
9469
|
option.classList.remove('active');
|
|
@@ -9471,7 +9472,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9471
9472
|
}
|
|
9472
9473
|
});
|
|
9473
9474
|
}
|
|
9474
|
-
|
|
9475
|
+
|
|
9475
9476
|
private setQualityByLabel(quality: string): void {
|
|
9476
9477
|
// Handle YouTube player
|
|
9477
9478
|
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
@@ -9486,9 +9487,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
9486
9487
|
});
|
|
9487
9488
|
return;
|
|
9488
9489
|
}
|
|
9489
|
-
|
|
9490
|
+
|
|
9490
9491
|
const qualityBadge = document.getElementById('uvf-quality-badge');
|
|
9491
|
-
|
|
9492
|
+
|
|
9492
9493
|
// Update UI
|
|
9493
9494
|
document.querySelectorAll('.quality-option').forEach(option => {
|
|
9494
9495
|
option.classList.remove('active');
|
|
@@ -9496,7 +9497,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9496
9497
|
option.classList.add('active');
|
|
9497
9498
|
}
|
|
9498
9499
|
});
|
|
9499
|
-
|
|
9500
|
+
|
|
9500
9501
|
// Update badge
|
|
9501
9502
|
if (qualityBadge) {
|
|
9502
9503
|
if (quality === 'auto') {
|
|
@@ -9505,7 +9506,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9505
9506
|
qualityBadge.textContent = quality + 'p';
|
|
9506
9507
|
}
|
|
9507
9508
|
}
|
|
9508
|
-
|
|
9509
|
+
|
|
9509
9510
|
// If we have actual quality levels from HLS/DASH, apply them
|
|
9510
9511
|
if (quality !== 'auto' && this.qualities.length > 0) {
|
|
9511
9512
|
const qualityLevel = this.qualities.find(q => q.label === quality + 'p');
|
|
@@ -9517,7 +9518,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9517
9518
|
this.setAutoQuality(true);
|
|
9518
9519
|
}
|
|
9519
9520
|
}
|
|
9520
|
-
|
|
9521
|
+
|
|
9521
9522
|
private async togglePiP(): Promise<void> {
|
|
9522
9523
|
try {
|
|
9523
9524
|
if ((document as any).pictureInPictureElement) {
|
|
@@ -9529,14 +9530,14 @@ export class WebPlayer extends BasePlayer {
|
|
|
9529
9530
|
console.error('PiP toggle failed:', error);
|
|
9530
9531
|
}
|
|
9531
9532
|
}
|
|
9532
|
-
|
|
9533
|
+
|
|
9533
9534
|
private setupCastContextSafe(): void {
|
|
9534
9535
|
try {
|
|
9535
9536
|
const castNs = (window as any).cast;
|
|
9536
9537
|
if (castNs && castNs.framework) {
|
|
9537
9538
|
this.setupCastContext();
|
|
9538
9539
|
}
|
|
9539
|
-
} catch (_) {}
|
|
9540
|
+
} catch (_) { }
|
|
9540
9541
|
}
|
|
9541
9542
|
|
|
9542
9543
|
private setupCastContext(): void {
|
|
@@ -9549,14 +9550,14 @@ export class WebPlayer extends BasePlayer {
|
|
|
9549
9550
|
try {
|
|
9550
9551
|
const autoJoin = chromeNs?.cast?.AutoJoinPolicy?.ORIGIN_SCOPED;
|
|
9551
9552
|
if (autoJoin) options.autoJoinPolicy = autoJoin;
|
|
9552
|
-
} catch (_) {}
|
|
9553
|
+
} catch (_) { }
|
|
9553
9554
|
this.castContext.setOptions(options);
|
|
9554
9555
|
this.castContext.addEventListener(
|
|
9555
9556
|
castNs.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
|
9556
9557
|
(ev: any) => {
|
|
9557
9558
|
const state = ev.sessionState;
|
|
9558
9559
|
if (state === castNs.framework.SessionState.SESSION_STARTED ||
|
|
9559
|
-
|
|
9560
|
+
state === castNs.framework.SessionState.SESSION_RESUMED) {
|
|
9560
9561
|
this.enableCastRemoteControl();
|
|
9561
9562
|
} else if (state === castNs.framework.SessionState.SESSION_ENDED) {
|
|
9562
9563
|
this.disableCastRemoteControl();
|
|
@@ -9580,7 +9581,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9580
9581
|
this._bindRemotePlayerEvents();
|
|
9581
9582
|
}
|
|
9582
9583
|
this.isCasting = true;
|
|
9583
|
-
try { this.video?.pause(); } catch (_) {}
|
|
9584
|
+
try { this.video?.pause(); } catch (_) { }
|
|
9584
9585
|
this._syncUIFromRemote();
|
|
9585
9586
|
this._syncCastButtons();
|
|
9586
9587
|
} catch (err) {
|
|
@@ -9703,7 +9704,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9703
9704
|
const sess = castNs?.framework?.CastContext?.getInstance()?.getCurrentSession?.();
|
|
9704
9705
|
const dev = sess && sess.getCastDevice ? sess.getCastDevice() : null;
|
|
9705
9706
|
if (dev && dev.friendlyName) title += ` (${dev.friendlyName})`;
|
|
9706
|
-
} catch (_) {}
|
|
9707
|
+
} catch (_) { }
|
|
9707
9708
|
castBtn.setAttribute('title', title);
|
|
9708
9709
|
castBtn.setAttribute('aria-label', title);
|
|
9709
9710
|
} else {
|
|
@@ -9742,7 +9743,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9742
9743
|
// Generate accordion-style menu
|
|
9743
9744
|
this.generateAccordionMenu();
|
|
9744
9745
|
}
|
|
9745
|
-
|
|
9746
|
+
|
|
9746
9747
|
/**
|
|
9747
9748
|
* Generate accordion-style settings menu
|
|
9748
9749
|
*/
|
|
@@ -9786,7 +9787,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9786
9787
|
if (this.settingsConfig.quality && this.availableQualities.length > 0) {
|
|
9787
9788
|
const currentQuality = this.availableQualities.find(q => q.value === this.currentQuality);
|
|
9788
9789
|
const currentQualityLabel = currentQuality ? currentQuality.label : 'Auto';
|
|
9789
|
-
|
|
9790
|
+
|
|
9790
9791
|
menuHTML += `
|
|
9791
9792
|
<div class="uvf-accordion-item">
|
|
9792
9793
|
<div class="uvf-accordion-header" data-section="quality">
|
|
@@ -9802,13 +9803,13 @@ export class WebPlayer extends BasePlayer {
|
|
|
9802
9803
|
<div class="uvf-accordion-arrow">▼</div>
|
|
9803
9804
|
</div>
|
|
9804
9805
|
<div class="uvf-accordion-content" data-section="quality">`;
|
|
9805
|
-
|
|
9806
|
+
|
|
9806
9807
|
this.availableQualities.forEach(quality => {
|
|
9807
9808
|
const isActive = quality.value === this.currentQuality ? 'active' : '';
|
|
9808
9809
|
const isPremium = this.isQualityPremium(quality);
|
|
9809
9810
|
const isLocked = isPremium && !this.isPremiumUser();
|
|
9810
9811
|
const qualityHeight = (quality as any).height || 0;
|
|
9811
|
-
|
|
9812
|
+
|
|
9812
9813
|
if (isLocked) {
|
|
9813
9814
|
const premiumLabel = this.premiumQualities?.premiumLabel || '🔒 Premium';
|
|
9814
9815
|
menuHTML += `<div class="uvf-settings-option quality-option premium-locked ${isActive}" data-quality="${quality.value}" data-quality-height="${qualityHeight}" data-quality-label="${quality.label}">${quality.label} <span style="margin-left: 4px; opacity: 0.7; font-size: 11px;">${premiumLabel}</span></div>`;
|
|
@@ -9816,7 +9817,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9816
9817
|
menuHTML += `<div class="uvf-settings-option quality-option ${isActive}" data-quality="${quality.value}">${quality.label}</div>`;
|
|
9817
9818
|
}
|
|
9818
9819
|
});
|
|
9819
|
-
|
|
9820
|
+
|
|
9820
9821
|
menuHTML += `</div></div>`;
|
|
9821
9822
|
}
|
|
9822
9823
|
|
|
@@ -9824,7 +9825,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9824
9825
|
if (this.settingsConfig.subtitles && this.availableSubtitles.length > 0) {
|
|
9825
9826
|
const currentSubtitle = this.availableSubtitles.find(s => s.value === this.currentSubtitle);
|
|
9826
9827
|
const currentSubtitleLabel = currentSubtitle ? currentSubtitle.label : 'Off';
|
|
9827
|
-
|
|
9828
|
+
|
|
9828
9829
|
menuHTML += `
|
|
9829
9830
|
<div class="uvf-accordion-item">
|
|
9830
9831
|
<div class="uvf-accordion-header" data-section="subtitles">
|
|
@@ -9840,18 +9841,18 @@ export class WebPlayer extends BasePlayer {
|
|
|
9840
9841
|
<div class="uvf-accordion-arrow">▼</div>
|
|
9841
9842
|
</div>
|
|
9842
9843
|
<div class="uvf-accordion-content" data-section="subtitles">`;
|
|
9843
|
-
|
|
9844
|
+
|
|
9844
9845
|
this.availableSubtitles.forEach(subtitle => {
|
|
9845
9846
|
const isActive = subtitle.value === this.currentSubtitle ? 'active' : '';
|
|
9846
9847
|
menuHTML += `<div class="uvf-settings-option subtitle-option ${isActive}" data-subtitle="${subtitle.value}">${subtitle.label}</div>`;
|
|
9847
9848
|
});
|
|
9848
|
-
|
|
9849
|
+
|
|
9849
9850
|
menuHTML += `</div></div>`;
|
|
9850
9851
|
}
|
|
9851
|
-
|
|
9852
|
+
|
|
9852
9853
|
// Close accordion container
|
|
9853
9854
|
menuHTML += '</div>';
|
|
9854
|
-
|
|
9855
|
+
|
|
9855
9856
|
// If no sections are enabled or available, show a message
|
|
9856
9857
|
if (menuHTML === '<div class="uvf-settings-accordion"></div>') {
|
|
9857
9858
|
menuHTML = '<div class="uvf-settings-accordion"><div class="uvf-settings-empty">No settings available</div></div>';
|
|
@@ -9859,7 +9860,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9859
9860
|
|
|
9860
9861
|
this.debugLog('Generated menu HTML length:', menuHTML.length);
|
|
9861
9862
|
this.debugLog('Generated menu HTML content:', menuHTML.substring(0, 200) + (menuHTML.length > 200 ? '...' : ''));
|
|
9862
|
-
|
|
9863
|
+
|
|
9863
9864
|
settingsMenu.innerHTML = menuHTML;
|
|
9864
9865
|
this.debugLog('Settings menu HTML set successfully');
|
|
9865
9866
|
|
|
@@ -9906,7 +9907,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9906
9907
|
// Native video - add common resolutions based on current resolution
|
|
9907
9908
|
const height = this.video.videoHeight;
|
|
9908
9909
|
const commonQualities = [2160, 1440, 1080, 720, 480, 360];
|
|
9909
|
-
|
|
9910
|
+
|
|
9910
9911
|
commonQualities.forEach(quality => {
|
|
9911
9912
|
if (quality <= height) {
|
|
9912
9913
|
detectedQualities.push({
|
|
@@ -9917,46 +9918,46 @@ export class WebPlayer extends BasePlayer {
|
|
|
9917
9918
|
}
|
|
9918
9919
|
});
|
|
9919
9920
|
}
|
|
9920
|
-
|
|
9921
|
+
|
|
9921
9922
|
// Apply quality filter if configured
|
|
9922
9923
|
if (this.qualityFilter) {
|
|
9923
9924
|
detectedQualities = this.applyQualityFilter(detectedQualities);
|
|
9924
9925
|
}
|
|
9925
|
-
|
|
9926
|
+
|
|
9926
9927
|
// Add filtered qualities to available list
|
|
9927
9928
|
this.availableQualities.push(...detectedQualities);
|
|
9928
9929
|
}
|
|
9929
|
-
|
|
9930
|
+
|
|
9930
9931
|
/**
|
|
9931
9932
|
* Apply quality filter to detected qualities
|
|
9932
9933
|
*/
|
|
9933
9934
|
private applyQualityFilter(qualities: Array<{ value: string; label: string; height?: number }>): Array<{ value: string; label: string; height?: number }> {
|
|
9934
9935
|
let filtered = [...qualities];
|
|
9935
9936
|
const filter = this.qualityFilter;
|
|
9936
|
-
|
|
9937
|
+
|
|
9937
9938
|
// Filter by allowed heights
|
|
9938
9939
|
if (filter.allowedHeights && filter.allowedHeights.length > 0) {
|
|
9939
9940
|
filtered = filtered.filter(q => q.height && filter.allowedHeights.includes(q.height));
|
|
9940
9941
|
}
|
|
9941
|
-
|
|
9942
|
+
|
|
9942
9943
|
// Filter by allowed labels
|
|
9943
9944
|
if (filter.allowedLabels && filter.allowedLabels.length > 0) {
|
|
9944
9945
|
filtered = filtered.filter(q => filter.allowedLabels.includes(q.label));
|
|
9945
9946
|
}
|
|
9946
|
-
|
|
9947
|
+
|
|
9947
9948
|
// Filter by minimum height
|
|
9948
9949
|
if (filter.minHeight !== undefined) {
|
|
9949
9950
|
filtered = filtered.filter(q => q.height && q.height >= filter.minHeight);
|
|
9950
9951
|
}
|
|
9951
|
-
|
|
9952
|
+
|
|
9952
9953
|
// Filter by maximum height
|
|
9953
9954
|
if (filter.maxHeight !== undefined) {
|
|
9954
9955
|
filtered = filtered.filter(q => q.height && q.height <= filter.maxHeight);
|
|
9955
9956
|
}
|
|
9956
|
-
|
|
9957
|
+
|
|
9957
9958
|
return filtered;
|
|
9958
9959
|
}
|
|
9959
|
-
|
|
9960
|
+
|
|
9960
9961
|
/**
|
|
9961
9962
|
* Set quality filter (can be called at runtime)
|
|
9962
9963
|
*/
|
|
@@ -9967,7 +9968,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9967
9968
|
this.updateSettingsMenu();
|
|
9968
9969
|
}
|
|
9969
9970
|
}
|
|
9970
|
-
|
|
9971
|
+
|
|
9971
9972
|
/**
|
|
9972
9973
|
* Set ad playing state (called by Google Ads Manager)
|
|
9973
9974
|
*/
|
|
@@ -9975,14 +9976,14 @@ export class WebPlayer extends BasePlayer {
|
|
|
9975
9976
|
this.isAdPlaying = isPlaying;
|
|
9976
9977
|
this.debugLog('Ad playing state:', isPlaying);
|
|
9977
9978
|
}
|
|
9978
|
-
|
|
9979
|
+
|
|
9979
9980
|
/**
|
|
9980
9981
|
* Check if ad is currently playing
|
|
9981
9982
|
*/
|
|
9982
9983
|
public isAdCurrentlyPlaying(): boolean {
|
|
9983
9984
|
return this.isAdPlaying;
|
|
9984
9985
|
}
|
|
9985
|
-
|
|
9986
|
+
|
|
9986
9987
|
/**
|
|
9987
9988
|
* Check if a quality level is premium
|
|
9988
9989
|
*/
|
|
@@ -9990,61 +9991,61 @@ export class WebPlayer extends BasePlayer {
|
|
|
9990
9991
|
if (!this.premiumQualities || !this.premiumQualities.enabled) {
|
|
9991
9992
|
return false;
|
|
9992
9993
|
}
|
|
9993
|
-
|
|
9994
|
+
|
|
9994
9995
|
const config = this.premiumQualities;
|
|
9995
9996
|
const height = quality.height || 0;
|
|
9996
9997
|
const label = quality.label || '';
|
|
9997
|
-
|
|
9998
|
+
|
|
9998
9999
|
// Check by specific heights
|
|
9999
10000
|
if (config.requiredHeights && config.requiredHeights.length > 0) {
|
|
10000
10001
|
if (config.requiredHeights.includes(height)) return true;
|
|
10001
10002
|
}
|
|
10002
|
-
|
|
10003
|
+
|
|
10003
10004
|
// Check by specific labels
|
|
10004
10005
|
if (config.requiredLabels && config.requiredLabels.length > 0) {
|
|
10005
10006
|
if (config.requiredLabels.includes(label)) return true;
|
|
10006
10007
|
}
|
|
10007
|
-
|
|
10008
|
+
|
|
10008
10009
|
// Check by minimum height threshold
|
|
10009
10010
|
if (config.minPremiumHeight !== undefined) {
|
|
10010
10011
|
if (height >= config.minPremiumHeight) return true;
|
|
10011
10012
|
}
|
|
10012
|
-
|
|
10013
|
+
|
|
10013
10014
|
return false;
|
|
10014
10015
|
}
|
|
10015
|
-
|
|
10016
|
+
|
|
10016
10017
|
/**
|
|
10017
10018
|
* Check if current user is premium
|
|
10018
10019
|
*/
|
|
10019
10020
|
private isPremiumUser(): boolean {
|
|
10020
10021
|
return this.premiumQualities?.isPremiumUser === true;
|
|
10021
10022
|
}
|
|
10022
|
-
|
|
10023
|
+
|
|
10023
10024
|
/**
|
|
10024
10025
|
* Handle click on locked premium quality
|
|
10025
10026
|
*/
|
|
10026
10027
|
private handlePremiumQualityClick(element: HTMLElement): void {
|
|
10027
10028
|
const height = parseInt(element.dataset.qualityHeight || '0');
|
|
10028
10029
|
const label = element.dataset.qualityLabel || '';
|
|
10029
|
-
|
|
10030
|
+
|
|
10030
10031
|
this.debugLog(`Premium quality clicked: ${label} (${height}p)`);
|
|
10031
|
-
|
|
10032
|
+
|
|
10032
10033
|
// Call custom callback if provided
|
|
10033
10034
|
if (this.premiumQualities?.onPremiumQualityClick) {
|
|
10034
10035
|
this.premiumQualities.onPremiumQualityClick({ height, label });
|
|
10035
10036
|
}
|
|
10036
|
-
|
|
10037
|
+
|
|
10037
10038
|
// Show notification
|
|
10038
10039
|
const message = this.premiumQualities?.premiumLabel || 'Premium';
|
|
10039
10040
|
this.showNotification(`${label} requires ${message.replace('🔒 ', '')}`);
|
|
10040
|
-
|
|
10041
|
+
|
|
10041
10042
|
// Redirect to unlock URL if provided
|
|
10042
10043
|
if (this.premiumQualities?.unlockUrl) {
|
|
10043
10044
|
setTimeout(() => {
|
|
10044
10045
|
window.location.href = this.premiumQualities.unlockUrl;
|
|
10045
10046
|
}, 1500);
|
|
10046
10047
|
}
|
|
10047
|
-
|
|
10048
|
+
|
|
10048
10049
|
// Close settings menu
|
|
10049
10050
|
this.hideSettingsMenu();
|
|
10050
10051
|
}
|
|
@@ -10101,10 +10102,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
10101
10102
|
header.addEventListener('click', (e) => {
|
|
10102
10103
|
e.preventDefault();
|
|
10103
10104
|
e.stopPropagation();
|
|
10104
|
-
|
|
10105
|
+
|
|
10105
10106
|
const accordionItem = header.parentElement;
|
|
10106
10107
|
const section = header.getAttribute('data-section');
|
|
10107
|
-
|
|
10108
|
+
|
|
10108
10109
|
if (accordionItem && section) {
|
|
10109
10110
|
this.toggleAccordionSection(accordionItem, section);
|
|
10110
10111
|
}
|
|
@@ -10127,13 +10128,13 @@ export class WebPlayer extends BasePlayer {
|
|
|
10127
10128
|
e.stopPropagation();
|
|
10128
10129
|
const target = e.target as HTMLElement;
|
|
10129
10130
|
const quality = target.dataset.quality || 'auto';
|
|
10130
|
-
|
|
10131
|
+
|
|
10131
10132
|
// Check if this is a locked premium quality
|
|
10132
10133
|
if (target.classList.contains('premium-locked')) {
|
|
10133
10134
|
this.handlePremiumQualityClick(target);
|
|
10134
10135
|
return;
|
|
10135
10136
|
}
|
|
10136
|
-
|
|
10137
|
+
|
|
10137
10138
|
this.setQualityFromSettings(quality);
|
|
10138
10139
|
this.updateAccordionAfterSelection('quality');
|
|
10139
10140
|
});
|
|
@@ -10149,19 +10150,19 @@ export class WebPlayer extends BasePlayer {
|
|
|
10149
10150
|
});
|
|
10150
10151
|
});
|
|
10151
10152
|
}
|
|
10152
|
-
|
|
10153
|
+
|
|
10153
10154
|
/**
|
|
10154
10155
|
* Toggle accordion section
|
|
10155
10156
|
*/
|
|
10156
10157
|
private toggleAccordionSection(accordionItem: Element, section: string): void {
|
|
10157
10158
|
const isExpanded = accordionItem.classList.contains('expanded');
|
|
10158
|
-
|
|
10159
|
+
|
|
10159
10160
|
// If clicking the same section that's already expanded, just close it
|
|
10160
10161
|
if (isExpanded) {
|
|
10161
10162
|
accordionItem.classList.remove('expanded');
|
|
10162
10163
|
return;
|
|
10163
10164
|
}
|
|
10164
|
-
|
|
10165
|
+
|
|
10165
10166
|
// Otherwise, close all sections and open the clicked one
|
|
10166
10167
|
const settingsMenu = document.getElementById('uvf-settings-menu');
|
|
10167
10168
|
if (settingsMenu) {
|
|
@@ -10169,30 +10170,30 @@ export class WebPlayer extends BasePlayer {
|
|
|
10169
10170
|
item.classList.remove('expanded');
|
|
10170
10171
|
});
|
|
10171
10172
|
}
|
|
10172
|
-
|
|
10173
|
+
|
|
10173
10174
|
// Open the clicked section
|
|
10174
10175
|
accordionItem.classList.add('expanded');
|
|
10175
10176
|
}
|
|
10176
|
-
|
|
10177
|
+
|
|
10177
10178
|
/**
|
|
10178
10179
|
* Hide settings menu with proper styling
|
|
10179
10180
|
*/
|
|
10180
10181
|
private hideSettingsMenu(): void {
|
|
10181
10182
|
const settingsMenu = document.getElementById('uvf-settings-menu');
|
|
10182
10183
|
if (!settingsMenu) return;
|
|
10183
|
-
|
|
10184
|
+
|
|
10184
10185
|
settingsMenu.classList.remove('active');
|
|
10185
|
-
|
|
10186
|
+
|
|
10186
10187
|
// Apply fallback styles to ensure menu is hidden
|
|
10187
10188
|
settingsMenu.style.display = 'none';
|
|
10188
10189
|
settingsMenu.style.visibility = 'hidden';
|
|
10189
10190
|
settingsMenu.style.opacity = '0';
|
|
10190
|
-
|
|
10191
|
+
|
|
10191
10192
|
// Also close any expanded accordions
|
|
10192
10193
|
settingsMenu.querySelectorAll('.uvf-accordion-item.expanded').forEach(item => {
|
|
10193
10194
|
item.classList.remove('expanded');
|
|
10194
10195
|
});
|
|
10195
|
-
|
|
10196
|
+
|
|
10196
10197
|
this.debugLog('Settings menu hidden via hideSettingsMenu()');
|
|
10197
10198
|
}
|
|
10198
10199
|
|
|
@@ -10207,7 +10208,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10207
10208
|
item.classList.remove('expanded');
|
|
10208
10209
|
});
|
|
10209
10210
|
}
|
|
10210
|
-
|
|
10211
|
+
|
|
10211
10212
|
// Close settings menu after selection on all devices
|
|
10212
10213
|
setTimeout(() => {
|
|
10213
10214
|
this.hideSettingsMenu();
|
|
@@ -10246,7 +10247,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10246
10247
|
*/
|
|
10247
10248
|
private setQualityFromSettings(quality: string): void {
|
|
10248
10249
|
this.currentQuality = quality;
|
|
10249
|
-
|
|
10250
|
+
|
|
10250
10251
|
if (quality === 'auto') {
|
|
10251
10252
|
// Enable auto quality with filter consideration
|
|
10252
10253
|
if (this.hls) {
|
|
@@ -10267,7 +10268,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10267
10268
|
} else {
|
|
10268
10269
|
// Set specific quality
|
|
10269
10270
|
const qualityIndex = parseInt(quality);
|
|
10270
|
-
|
|
10271
|
+
|
|
10271
10272
|
if (this.hls && !isNaN(qualityIndex) && this.hls.levels[qualityIndex]) {
|
|
10272
10273
|
this.hls.currentLevel = qualityIndex;
|
|
10273
10274
|
} else if (this.dash && !isNaN(qualityIndex)) {
|
|
@@ -10275,38 +10276,38 @@ export class WebPlayer extends BasePlayer {
|
|
|
10275
10276
|
this.dash.setQualityFor('video', qualityIndex);
|
|
10276
10277
|
}
|
|
10277
10278
|
}
|
|
10278
|
-
|
|
10279
|
+
|
|
10279
10280
|
this.debugLog(`Quality set to ${quality}`);
|
|
10280
10281
|
}
|
|
10281
|
-
|
|
10282
|
+
|
|
10282
10283
|
/**
|
|
10283
10284
|
* Apply quality filter to HLS auto quality selection
|
|
10284
10285
|
*/
|
|
10285
10286
|
private applyHLSQualityFilter(): void {
|
|
10286
10287
|
if (!this.hls || !this.hls.levels) return;
|
|
10287
|
-
|
|
10288
|
+
|
|
10288
10289
|
const allowedLevels: number[] = [];
|
|
10289
|
-
|
|
10290
|
+
|
|
10290
10291
|
this.hls.levels.forEach((level: any, index: number) => {
|
|
10291
10292
|
let allowed = true;
|
|
10292
|
-
|
|
10293
|
+
|
|
10293
10294
|
// Apply quality filter if exists
|
|
10294
10295
|
if (this.qualityFilter) {
|
|
10295
10296
|
const filter = this.qualityFilter;
|
|
10296
|
-
|
|
10297
|
+
|
|
10297
10298
|
if (filter.allowedHeights && filter.allowedHeights.length > 0) {
|
|
10298
10299
|
allowed = allowed && filter.allowedHeights.includes(level.height);
|
|
10299
10300
|
}
|
|
10300
|
-
|
|
10301
|
+
|
|
10301
10302
|
if (filter.minHeight !== undefined) {
|
|
10302
10303
|
allowed = allowed && level.height >= filter.minHeight;
|
|
10303
10304
|
}
|
|
10304
|
-
|
|
10305
|
+
|
|
10305
10306
|
if (filter.maxHeight !== undefined) {
|
|
10306
10307
|
allowed = allowed && level.height <= filter.maxHeight;
|
|
10307
10308
|
}
|
|
10308
10309
|
}
|
|
10309
|
-
|
|
10310
|
+
|
|
10310
10311
|
// Apply premium quality restrictions for non-premium users
|
|
10311
10312
|
if (this.premiumQualities && this.premiumQualities.enabled && !this.isPremiumUser()) {
|
|
10312
10313
|
const isPremium = this.isQualityPremium({ height: level.height, label: `${level.height}p` });
|
|
@@ -10314,24 +10315,24 @@ export class WebPlayer extends BasePlayer {
|
|
|
10314
10315
|
allowed = false; // Exclude premium qualities for non-premium users
|
|
10315
10316
|
}
|
|
10316
10317
|
}
|
|
10317
|
-
|
|
10318
|
+
|
|
10318
10319
|
if (allowed) {
|
|
10319
10320
|
allowedLevels.push(index);
|
|
10320
10321
|
}
|
|
10321
10322
|
});
|
|
10322
|
-
|
|
10323
|
+
|
|
10323
10324
|
// Set max and min auto level based on filter and premium restrictions
|
|
10324
10325
|
if (allowedLevels.length > 0) {
|
|
10325
10326
|
// Set the maximum level cap
|
|
10326
10327
|
this.hls.autoLevelCapping = Math.max(...allowedLevels);
|
|
10327
|
-
|
|
10328
|
+
|
|
10328
10329
|
// Enable auto quality
|
|
10329
10330
|
this.hls.currentLevel = -1;
|
|
10330
|
-
|
|
10331
|
+
|
|
10331
10332
|
// For minimum level, we need to use a different approach
|
|
10332
10333
|
// Monitor level changes and prevent switching below minimum
|
|
10333
10334
|
const minLevel = Math.min(...allowedLevels);
|
|
10334
|
-
|
|
10335
|
+
|
|
10335
10336
|
// Set up level switching listener to enforce minimum
|
|
10336
10337
|
const enforceLevelConstraints = () => {
|
|
10337
10338
|
if (this.hls && this.hls.currentLevel !== -1 && this.hls.currentLevel < minLevel) {
|
|
@@ -10339,52 +10340,52 @@ export class WebPlayer extends BasePlayer {
|
|
|
10339
10340
|
this.hls.currentLevel = minLevel;
|
|
10340
10341
|
}
|
|
10341
10342
|
};
|
|
10342
|
-
|
|
10343
|
+
|
|
10343
10344
|
// Remove old listener if exists
|
|
10344
10345
|
if (this.hls.listeners('hlsLevelSwitching').length > 0) {
|
|
10345
10346
|
this.hls.off('hlsLevelSwitching', enforceLevelConstraints);
|
|
10346
10347
|
}
|
|
10347
|
-
|
|
10348
|
+
|
|
10348
10349
|
// Add listener to enforce constraints
|
|
10349
10350
|
this.hls.on('hlsLevelSwitching', enforceLevelConstraints);
|
|
10350
|
-
|
|
10351
|
+
|
|
10351
10352
|
this.debugLog(`Auto quality limited to levels: ${allowedLevels.join(', ')} (min: ${minLevel}, max: ${Math.max(...allowedLevels)})`);
|
|
10352
10353
|
} else {
|
|
10353
10354
|
// No allowed levels - just use auto
|
|
10354
10355
|
this.hls.currentLevel = -1;
|
|
10355
10356
|
}
|
|
10356
10357
|
}
|
|
10357
|
-
|
|
10358
|
+
|
|
10358
10359
|
/**
|
|
10359
10360
|
* Apply quality filter to DASH auto quality selection
|
|
10360
10361
|
*/
|
|
10361
10362
|
private applyDASHQualityFilter(): void {
|
|
10362
10363
|
if (!this.dash) return;
|
|
10363
|
-
|
|
10364
|
+
|
|
10364
10365
|
try {
|
|
10365
10366
|
const videoQualities = this.dash.getBitrateInfoListFor('video');
|
|
10366
10367
|
const allowedIndices: number[] = [];
|
|
10367
|
-
|
|
10368
|
+
|
|
10368
10369
|
videoQualities.forEach((quality: any, index: number) => {
|
|
10369
10370
|
let allowed = true;
|
|
10370
|
-
|
|
10371
|
+
|
|
10371
10372
|
// Apply quality filter if exists
|
|
10372
10373
|
if (this.qualityFilter) {
|
|
10373
10374
|
const filter = this.qualityFilter;
|
|
10374
|
-
|
|
10375
|
+
|
|
10375
10376
|
if (filter.allowedHeights && filter.allowedHeights.length > 0) {
|
|
10376
10377
|
allowed = allowed && filter.allowedHeights.includes(quality.height);
|
|
10377
10378
|
}
|
|
10378
|
-
|
|
10379
|
+
|
|
10379
10380
|
if (filter.minHeight !== undefined) {
|
|
10380
10381
|
allowed = allowed && quality.height >= filter.minHeight;
|
|
10381
10382
|
}
|
|
10382
|
-
|
|
10383
|
+
|
|
10383
10384
|
if (filter.maxHeight !== undefined) {
|
|
10384
10385
|
allowed = allowed && quality.height <= filter.maxHeight;
|
|
10385
10386
|
}
|
|
10386
10387
|
}
|
|
10387
|
-
|
|
10388
|
+
|
|
10388
10389
|
// Apply premium quality restrictions for non-premium users
|
|
10389
10390
|
if (this.premiumQualities && this.premiumQualities.enabled && !this.isPremiumUser()) {
|
|
10390
10391
|
const isPremium = this.isQualityPremium({ height: quality.height, label: `${quality.height}p` });
|
|
@@ -10392,12 +10393,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
10392
10393
|
allowed = false; // Exclude premium qualities for non-premium users
|
|
10393
10394
|
}
|
|
10394
10395
|
}
|
|
10395
|
-
|
|
10396
|
+
|
|
10396
10397
|
if (allowed) {
|
|
10397
10398
|
allowedIndices.push(index);
|
|
10398
10399
|
}
|
|
10399
10400
|
});
|
|
10400
|
-
|
|
10401
|
+
|
|
10401
10402
|
// Set quality bounds for DASH
|
|
10402
10403
|
if (allowedIndices.length > 0) {
|
|
10403
10404
|
const settings = {
|
|
@@ -10422,7 +10423,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10422
10423
|
*/
|
|
10423
10424
|
private setSubtitle(subtitle: string): void {
|
|
10424
10425
|
this.currentSubtitle = subtitle;
|
|
10425
|
-
|
|
10426
|
+
|
|
10426
10427
|
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
10427
10428
|
// YouTube subtitles - limited control
|
|
10428
10429
|
if (subtitle === 'off') {
|
|
@@ -10441,7 +10442,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10441
10442
|
this.showShortcutIndicator('Subtitle language selection not available for YouTube');
|
|
10442
10443
|
return;
|
|
10443
10444
|
}
|
|
10444
|
-
|
|
10445
|
+
|
|
10445
10446
|
if (subtitle === 'off') {
|
|
10446
10447
|
// Disable all subtitles
|
|
10447
10448
|
if (this.video?.textTracks) {
|
|
@@ -10467,7 +10468,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10467
10468
|
});
|
|
10468
10469
|
}
|
|
10469
10470
|
}
|
|
10470
|
-
|
|
10471
|
+
|
|
10471
10472
|
this.debugLog(`Subtitle set to ${subtitle}`);
|
|
10472
10473
|
}
|
|
10473
10474
|
|
|
@@ -10485,11 +10486,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
10485
10486
|
if (tid) ids = [tid];
|
|
10486
10487
|
}
|
|
10487
10488
|
if (typeof media.setActiveTracks === 'function') {
|
|
10488
|
-
media.setActiveTracks(ids, () => {}, () => {});
|
|
10489
|
+
media.setActiveTracks(ids, () => { }, () => { });
|
|
10489
10490
|
} else if (typeof media.setActiveTrackIds === 'function') {
|
|
10490
10491
|
media.setActiveTrackIds(ids);
|
|
10491
10492
|
}
|
|
10492
|
-
} catch (_) {}
|
|
10493
|
+
} catch (_) { }
|
|
10493
10494
|
}
|
|
10494
10495
|
|
|
10495
10496
|
private onCastButtonClick(): void {
|
|
@@ -10498,20 +10499,20 @@ export class WebPlayer extends BasePlayer {
|
|
|
10498
10499
|
this.showAirPlayPicker();
|
|
10499
10500
|
return;
|
|
10500
10501
|
}
|
|
10501
|
-
|
|
10502
|
+
|
|
10502
10503
|
// Google Cast for non-iOS devices
|
|
10503
10504
|
try {
|
|
10504
10505
|
const castNs = (window as any).cast;
|
|
10505
10506
|
if (this.isCasting && castNs && castNs.framework) {
|
|
10506
10507
|
const ctx = castNs.framework.CastContext.getInstance();
|
|
10507
|
-
ctx.requestSession().catch(() => {});
|
|
10508
|
+
ctx.requestSession().catch(() => { });
|
|
10508
10509
|
return;
|
|
10509
10510
|
}
|
|
10510
|
-
} catch (_) {}
|
|
10511
|
+
} catch (_) { }
|
|
10511
10512
|
// Not casting yet
|
|
10512
10513
|
this.initCast();
|
|
10513
10514
|
}
|
|
10514
|
-
|
|
10515
|
+
|
|
10515
10516
|
/**
|
|
10516
10517
|
* Show AirPlay picker for iOS devices
|
|
10517
10518
|
*/
|
|
@@ -10520,7 +10521,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10520
10521
|
this.showNotification('Video not ready');
|
|
10521
10522
|
return;
|
|
10522
10523
|
}
|
|
10523
|
-
|
|
10524
|
+
|
|
10524
10525
|
// Check if AirPlay is supported
|
|
10525
10526
|
const videoElement = this.video as any;
|
|
10526
10527
|
if (typeof videoElement.webkitShowPlaybackTargetPicker === 'function') {
|
|
@@ -10544,7 +10545,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10544
10545
|
const ctx = castNs.framework.CastContext.getInstance();
|
|
10545
10546
|
const sess = ctx.getCurrentSession && ctx.getCurrentSession();
|
|
10546
10547
|
if (sess) {
|
|
10547
|
-
try { sess.endSession(true); } catch (_) {}
|
|
10548
|
+
try { sess.endSession(true); } catch (_) { }
|
|
10548
10549
|
this.disableCastRemoteControl();
|
|
10549
10550
|
this.showNotification('Stopped casting');
|
|
10550
10551
|
} else {
|
|
@@ -10583,9 +10584,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
10583
10584
|
const url = this.source?.url || this.video?.src || '';
|
|
10584
10585
|
const u = (url || '').toLowerCase();
|
|
10585
10586
|
const contentType = u.includes('.m3u8') ? 'application/x-mpegurl'
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10587
|
+
: u.includes('.mpd') ? 'application/dash+xml'
|
|
10588
|
+
: u.includes('.webm') ? 'video/webm'
|
|
10589
|
+
: 'video/mp4';
|
|
10589
10590
|
|
|
10590
10591
|
const chromeNs = (window as any).chrome;
|
|
10591
10592
|
const mediaInfo = new chromeNs.cast.media.MediaInfo(url, contentType);
|
|
@@ -10594,7 +10595,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10594
10595
|
const md = new chromeNs.cast.media.GenericMediaMetadata();
|
|
10595
10596
|
md.title = this.source?.metadata?.title || (this.video?.currentSrc ? this.video!.currentSrc.split('/').slice(-1)[0] : 'Web Player');
|
|
10596
10597
|
mediaInfo.metadata = md;
|
|
10597
|
-
} catch (_) {}
|
|
10598
|
+
} catch (_) { }
|
|
10598
10599
|
|
|
10599
10600
|
// Subtitle tracks -> Cast tracks mapping
|
|
10600
10601
|
const castTracks: any[] = [];
|
|
@@ -10610,7 +10611,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10610
10611
|
let nextId = 1;
|
|
10611
10612
|
for (let i = 0; i < this.subtitles.length; i++) {
|
|
10612
10613
|
const t = this.subtitles[i];
|
|
10613
|
-
const key = t.label || t.language || `Track ${i+1}`;
|
|
10614
|
+
const key = t.label || t.language || `Track ${i + 1}`;
|
|
10614
10615
|
try {
|
|
10615
10616
|
const track = new chromeNs.cast.media.Track(nextId, chromeNs.cast.media.TrackType.TEXT);
|
|
10616
10617
|
track.trackContentId = t.url;
|
|
@@ -10622,7 +10623,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10622
10623
|
castTracks.push(track);
|
|
10623
10624
|
this._castTrackIdByKey[key] = nextId;
|
|
10624
10625
|
nextId++;
|
|
10625
|
-
} catch (_) {}
|
|
10626
|
+
} catch (_) { }
|
|
10626
10627
|
}
|
|
10627
10628
|
}
|
|
10628
10629
|
if (castTracks.length > 0) {
|
|
@@ -10635,12 +10636,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
10635
10636
|
style.edgeColor = '#000000FF';
|
|
10636
10637
|
style.fontScale = 1.0;
|
|
10637
10638
|
mediaInfo.textTrackStyle = style;
|
|
10638
|
-
} catch (_) {}
|
|
10639
|
+
} catch (_) { }
|
|
10639
10640
|
}
|
|
10640
10641
|
|
|
10641
10642
|
const request = new chromeNs.cast.media.LoadRequest(mediaInfo);
|
|
10642
10643
|
request.autoplay = true;
|
|
10643
|
-
try { request.currentTime = Math.max(0, Math.floor(this.video?.currentTime || 0)); } catch (_) {}
|
|
10644
|
+
try { request.currentTime = Math.max(0, Math.floor(this.video?.currentTime || 0)); } catch (_) { }
|
|
10644
10645
|
// Determine selected subtitle key from currentSubtitleIndex
|
|
10645
10646
|
const currentIdx = this.currentSubtitleIndex;
|
|
10646
10647
|
this.selectedSubtitleKey = (currentIdx >= 0 && this.subtitles[currentIdx]) ? (this.subtitles[currentIdx].label || this.subtitles[currentIdx].language) : 'off';
|
|
@@ -10657,7 +10658,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10657
10658
|
this.showNotification('Cast failed');
|
|
10658
10659
|
}
|
|
10659
10660
|
}
|
|
10660
|
-
|
|
10661
|
+
|
|
10661
10662
|
private async shareVideo(): Promise<void> {
|
|
10662
10663
|
// Get share configuration
|
|
10663
10664
|
const shareConfig = this.config.share;
|
|
@@ -10705,7 +10706,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10705
10706
|
this.showNotification('Share failed');
|
|
10706
10707
|
}
|
|
10707
10708
|
}
|
|
10708
|
-
|
|
10709
|
+
|
|
10709
10710
|
/**
|
|
10710
10711
|
* Check if text is truncated and needs tooltip
|
|
10711
10712
|
*/
|
|
@@ -10727,9 +10728,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
10727
10728
|
const tooltip = document.createElement('div');
|
|
10728
10729
|
tooltip.className = 'uvf-text-tooltip';
|
|
10729
10730
|
tooltip.textContent = text;
|
|
10730
|
-
|
|
10731
|
+
|
|
10731
10732
|
element.appendChild(tooltip);
|
|
10732
|
-
|
|
10733
|
+
|
|
10733
10734
|
// Show tooltip with delay
|
|
10734
10735
|
setTimeout(() => {
|
|
10735
10736
|
tooltip.classList.add('show');
|
|
@@ -10765,11 +10766,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
10765
10766
|
this.showTextTooltip(titleElement, titleText);
|
|
10766
10767
|
}
|
|
10767
10768
|
});
|
|
10768
|
-
|
|
10769
|
+
|
|
10769
10770
|
titleElement.addEventListener('mouseleave', () => {
|
|
10770
10771
|
this.hideTextTooltip(titleElement);
|
|
10771
10772
|
});
|
|
10772
|
-
|
|
10773
|
+
|
|
10773
10774
|
// Touch support for mobile
|
|
10774
10775
|
titleElement.addEventListener('touchstart', () => {
|
|
10775
10776
|
const titleText = (this.source?.metadata?.title || '').toString().trim();
|
|
@@ -10790,11 +10791,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
10790
10791
|
this.showTextTooltip(descElement, descText);
|
|
10791
10792
|
}
|
|
10792
10793
|
});
|
|
10793
|
-
|
|
10794
|
+
|
|
10794
10795
|
descElement.addEventListener('mouseleave', () => {
|
|
10795
10796
|
this.hideTextTooltip(descElement);
|
|
10796
10797
|
});
|
|
10797
|
-
|
|
10798
|
+
|
|
10798
10799
|
// Touch support for mobile
|
|
10799
10800
|
descElement.addEventListener('touchstart', () => {
|
|
10800
10801
|
const descText = (this.source?.metadata?.description || '').toString().trim();
|
|
@@ -10817,7 +10818,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10817
10818
|
if (words.length <= maxWords) {
|
|
10818
10819
|
return { truncated: text, needsTooltip: false };
|
|
10819
10820
|
}
|
|
10820
|
-
|
|
10821
|
+
|
|
10821
10822
|
const truncated = words.slice(0, maxWords).join(' ') + '...';
|
|
10822
10823
|
return { truncated, needsTooltip: true };
|
|
10823
10824
|
}
|
|
@@ -10828,10 +10829,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
10828
10829
|
private applySmartTextDisplay(titleEl: HTMLElement | null, descEl: HTMLElement | null, titleText: string, descText: string): void {
|
|
10829
10830
|
const isDesktop = window.innerWidth >= 1024;
|
|
10830
10831
|
const isMobile = window.innerWidth < 768;
|
|
10831
|
-
|
|
10832
|
+
|
|
10832
10833
|
if (titleEl && titleText) {
|
|
10833
10834
|
const wordCount = titleText.split(' ').length;
|
|
10834
|
-
|
|
10835
|
+
|
|
10835
10836
|
if (isDesktop && wordCount > 8 && wordCount <= 15) {
|
|
10836
10837
|
// Use multiline for moderately long titles on desktop
|
|
10837
10838
|
titleEl.classList.add('multiline');
|
|
@@ -10847,10 +10848,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
10847
10848
|
titleEl.classList.remove('multiline');
|
|
10848
10849
|
}
|
|
10849
10850
|
}
|
|
10850
|
-
|
|
10851
|
+
|
|
10851
10852
|
if (descEl && descText) {
|
|
10852
10853
|
const wordCount = descText.split(' ').length;
|
|
10853
|
-
|
|
10854
|
+
|
|
10854
10855
|
if (isDesktop && wordCount > 15 && wordCount <= 25) {
|
|
10855
10856
|
// Use multiline for moderately long descriptions on desktop
|
|
10856
10857
|
descEl.classList.add('multiline');
|
|
@@ -10906,7 +10907,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10906
10907
|
setTimeout(() => {
|
|
10907
10908
|
this.setupTextTooltips();
|
|
10908
10909
|
}, 100); // Small delay to ensure elements are rendered
|
|
10909
|
-
|
|
10910
|
+
|
|
10910
10911
|
} catch (_) { /* ignore */ }
|
|
10911
10912
|
}
|
|
10912
10913
|
|
|
@@ -10921,26 +10922,26 @@ export class WebPlayer extends BasePlayer {
|
|
|
10921
10922
|
private canPlayVideo(): boolean {
|
|
10922
10923
|
const freeDuration = Number(this.config.freeDuration || 0);
|
|
10923
10924
|
const currentTime = this.video?.currentTime || 0;
|
|
10924
|
-
|
|
10925
|
+
|
|
10925
10926
|
// Always allow if no free duration limit is set
|
|
10926
10927
|
if (freeDuration <= 0) return true;
|
|
10927
|
-
|
|
10928
|
+
|
|
10928
10929
|
// Allow if payment was successful
|
|
10929
10930
|
if (this.paymentSuccessful) return true;
|
|
10930
|
-
|
|
10931
|
+
|
|
10931
10932
|
// Allow if within free preview duration
|
|
10932
10933
|
if (currentTime < freeDuration) return true;
|
|
10933
|
-
|
|
10934
|
+
|
|
10934
10935
|
// Check if paywall controller indicates user is authenticated
|
|
10935
|
-
if (this.paywallController &&
|
|
10936
|
-
|
|
10936
|
+
if (this.paywallController &&
|
|
10937
|
+
typeof this.paywallController.isAuthenticated === 'function') {
|
|
10937
10938
|
const isAuth = this.paywallController.isAuthenticated();
|
|
10938
10939
|
if (isAuth) {
|
|
10939
10940
|
this.paymentSuccessful = true;
|
|
10940
10941
|
return true;
|
|
10941
10942
|
}
|
|
10942
10943
|
}
|
|
10943
|
-
|
|
10944
|
+
|
|
10944
10945
|
return false;
|
|
10945
10946
|
}
|
|
10946
10947
|
|
|
@@ -10949,17 +10950,17 @@ export class WebPlayer extends BasePlayer {
|
|
|
10949
10950
|
*/
|
|
10950
10951
|
private enforcePaywallSecurity(): void {
|
|
10951
10952
|
this.debugLog('Enforcing paywall security');
|
|
10952
|
-
|
|
10953
|
+
|
|
10953
10954
|
// Pause video immediately
|
|
10954
10955
|
try {
|
|
10955
10956
|
if (this.video && !this.video.paused) {
|
|
10956
10957
|
this.video.pause();
|
|
10957
10958
|
}
|
|
10958
|
-
} catch (_) {}
|
|
10959
|
-
|
|
10959
|
+
} catch (_) { }
|
|
10960
|
+
|
|
10960
10961
|
// Activate paywall state
|
|
10961
10962
|
this.isPaywallActive = true;
|
|
10962
|
-
|
|
10963
|
+
|
|
10963
10964
|
// Show paywall overlay
|
|
10964
10965
|
if (this.paywallController) {
|
|
10965
10966
|
try {
|
|
@@ -10968,7 +10969,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10968
10969
|
this.debugError('Error showing paywall overlay:', error);
|
|
10969
10970
|
}
|
|
10970
10971
|
}
|
|
10971
|
-
|
|
10972
|
+
|
|
10972
10973
|
// Start monitoring for overlay tampering
|
|
10973
10974
|
this.startOverlayMonitoring();
|
|
10974
10975
|
}
|
|
@@ -10978,15 +10979,15 @@ export class WebPlayer extends BasePlayer {
|
|
|
10978
10979
|
*/
|
|
10979
10980
|
private startOverlayMonitoring(): void {
|
|
10980
10981
|
if (!this.playerWrapper || this.paymentSuccessful) return;
|
|
10981
|
-
|
|
10982
|
+
|
|
10982
10983
|
// Clear existing monitor
|
|
10983
10984
|
if (this.authValidationInterval) {
|
|
10984
10985
|
clearInterval(this.authValidationInterval);
|
|
10985
10986
|
this.authValidationInterval = null;
|
|
10986
10987
|
}
|
|
10987
|
-
|
|
10988
|
+
|
|
10988
10989
|
this.debugLog('Starting overlay monitoring - payment successful:', this.paymentSuccessful, 'paywall active:', this.isPaywallActive);
|
|
10989
|
-
|
|
10990
|
+
|
|
10990
10991
|
// Monitor every 1000ms (less aggressive than before)
|
|
10991
10992
|
this.authValidationInterval = setInterval(() => {
|
|
10992
10993
|
// First check: stop monitoring if payment successful or paywall inactive
|
|
@@ -10998,7 +10999,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
10998
10999
|
}
|
|
10999
11000
|
return;
|
|
11000
11001
|
}
|
|
11001
|
-
|
|
11002
|
+
|
|
11002
11003
|
// Double-check payment success before enforcing security
|
|
11003
11004
|
if (this.paymentSuccessful) {
|
|
11004
11005
|
this.debugLog('Payment successful detected during monitoring, stopping');
|
|
@@ -11008,20 +11009,20 @@ export class WebPlayer extends BasePlayer {
|
|
|
11008
11009
|
}
|
|
11009
11010
|
return;
|
|
11010
11011
|
}
|
|
11011
|
-
|
|
11012
|
+
|
|
11012
11013
|
// Check for overlay presence
|
|
11013
11014
|
const paywallOverlays = this.playerWrapper!.querySelectorAll('.uvf-paywall-overlay, .uvf-auth-overlay');
|
|
11014
11015
|
const visibleOverlays = Array.from(paywallOverlays).filter(overlay => {
|
|
11015
11016
|
const element = overlay as HTMLElement;
|
|
11016
|
-
return element.style.display !== 'none' &&
|
|
11017
|
-
|
|
11018
|
-
|
|
11017
|
+
return element.style.display !== 'none' &&
|
|
11018
|
+
element.offsetParent !== null &&
|
|
11019
|
+
window.getComputedStyle(element).visibility !== 'hidden';
|
|
11019
11020
|
});
|
|
11020
|
-
|
|
11021
|
+
|
|
11021
11022
|
if (visibleOverlays.length === 0) {
|
|
11022
11023
|
this.overlayRemovalAttempts++;
|
|
11023
11024
|
this.debugWarn(`Overlay removal attempt detected (${this.overlayRemovalAttempts}/${this.maxOverlayRemovalAttempts})`);
|
|
11024
|
-
|
|
11025
|
+
|
|
11025
11026
|
// Final check before taking action - ensure payment wasn't just completed
|
|
11026
11027
|
if (this.paymentSuccessful) {
|
|
11027
11028
|
this.debugLog('Payment successful detected, ignoring overlay removal');
|
|
@@ -11031,7 +11032,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
11031
11032
|
}
|
|
11032
11033
|
return;
|
|
11033
11034
|
}
|
|
11034
|
-
|
|
11035
|
+
|
|
11035
11036
|
if (this.overlayRemovalAttempts >= this.maxOverlayRemovalAttempts) {
|
|
11036
11037
|
this.handleSecurityViolation();
|
|
11037
11038
|
} else {
|
|
@@ -11039,7 +11040,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
11039
11040
|
this.enforcePaywallSecurity();
|
|
11040
11041
|
}
|
|
11041
11042
|
}
|
|
11042
|
-
|
|
11043
|
+
|
|
11043
11044
|
// Additional check: ensure video is paused if not authenticated
|
|
11044
11045
|
if (this.video && !this.video.paused && !this.paymentSuccessful) {
|
|
11045
11046
|
this.debugWarn('Unauthorized playback detected, pausing video');
|
|
@@ -11049,7 +11050,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
11049
11050
|
if (freeDuration > 0 && isFinite(freeDuration)) {
|
|
11050
11051
|
this.safeSetCurrentTime(freeDuration - 1);
|
|
11051
11052
|
}
|
|
11052
|
-
} catch (_) {}
|
|
11053
|
+
} catch (_) { }
|
|
11053
11054
|
}
|
|
11054
11055
|
}, 1000);
|
|
11055
11056
|
}
|
|
@@ -11059,7 +11060,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
11059
11060
|
*/
|
|
11060
11061
|
private handleSecurityViolation(): void {
|
|
11061
11062
|
this.debugError('Security violation detected - disabling video');
|
|
11062
|
-
|
|
11063
|
+
|
|
11063
11064
|
// Disable video completely
|
|
11064
11065
|
if (this.video) {
|
|
11065
11066
|
this.video.pause();
|
|
@@ -11067,10 +11068,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
11067
11068
|
this.video.src = ''; // Clear video source
|
|
11068
11069
|
this.video.style.display = 'none';
|
|
11069
11070
|
}
|
|
11070
|
-
|
|
11071
|
+
|
|
11071
11072
|
// Show security violation message
|
|
11072
11073
|
this.showSecurityViolationMessage();
|
|
11073
|
-
|
|
11074
|
+
|
|
11074
11075
|
// Clear monitoring interval
|
|
11075
11076
|
if (this.authValidationInterval) {
|
|
11076
11077
|
clearInterval(this.authValidationInterval);
|
|
@@ -11082,10 +11083,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
11082
11083
|
*/
|
|
11083
11084
|
private showSecurityViolationMessage(): void {
|
|
11084
11085
|
if (!this.playerWrapper) return;
|
|
11085
|
-
|
|
11086
|
+
|
|
11086
11087
|
// Clear existing content
|
|
11087
11088
|
this.playerWrapper.innerHTML = '';
|
|
11088
|
-
|
|
11089
|
+
|
|
11089
11090
|
// Create security violation overlay
|
|
11090
11091
|
const securityOverlay = document.createElement('div');
|
|
11091
11092
|
securityOverlay.style.cssText = `
|
|
@@ -11101,7 +11102,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
11101
11102
|
text-align: center;
|
|
11102
11103
|
padding: 40px;
|
|
11103
11104
|
`;
|
|
11104
|
-
|
|
11105
|
+
|
|
11105
11106
|
const messageContainer = document.createElement('div');
|
|
11106
11107
|
messageContainer.innerHTML = `
|
|
11107
11108
|
<div style="font-size: 24px; font-weight: bold; margin-bottom: 16px; color: #ff6b6b;">
|
|
@@ -11124,7 +11125,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
11124
11125
|
">Reload Page</button>
|
|
11125
11126
|
</div>
|
|
11126
11127
|
`;
|
|
11127
|
-
|
|
11128
|
+
|
|
11128
11129
|
securityOverlay.appendChild(messageContainer);
|
|
11129
11130
|
this.playerWrapper.appendChild(securityOverlay);
|
|
11130
11131
|
}
|
|
@@ -11134,25 +11135,25 @@ export class WebPlayer extends BasePlayer {
|
|
|
11134
11135
|
*/
|
|
11135
11136
|
private forceCleanupOverlays(): void {
|
|
11136
11137
|
this.debugLog('Force cleanup of overlays called');
|
|
11137
|
-
|
|
11138
|
+
|
|
11138
11139
|
if (!this.playerWrapper) return;
|
|
11139
|
-
|
|
11140
|
+
|
|
11140
11141
|
// Find and remove all overlay elements
|
|
11141
11142
|
const overlays = this.playerWrapper.querySelectorAll('.uvf-paywall-overlay, .uvf-auth-overlay');
|
|
11142
11143
|
overlays.forEach((overlay: Element) => {
|
|
11143
11144
|
const htmlOverlay = overlay as HTMLElement;
|
|
11144
11145
|
this.debugLog('Removing overlay:', htmlOverlay.className);
|
|
11145
|
-
|
|
11146
|
+
|
|
11146
11147
|
// Hide immediately
|
|
11147
11148
|
htmlOverlay.style.display = 'none';
|
|
11148
11149
|
htmlOverlay.classList.remove('active');
|
|
11149
|
-
|
|
11150
|
+
|
|
11150
11151
|
// Remove from DOM
|
|
11151
11152
|
if (htmlOverlay.parentNode) {
|
|
11152
11153
|
htmlOverlay.parentNode.removeChild(htmlOverlay);
|
|
11153
11154
|
}
|
|
11154
11155
|
});
|
|
11155
|
-
|
|
11156
|
+
|
|
11156
11157
|
// Also tell paywall controller to clean up
|
|
11157
11158
|
if (this.paywallController && typeof this.paywallController.destroyOverlays === 'function') {
|
|
11158
11159
|
this.debugLog('Calling paywallController.destroyOverlays()');
|