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