myetv-player 1.1.6 → 1.3.0

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.
Files changed (47) hide show
  1. package/css/myetv-player.css +153 -0
  2. package/css/myetv-player.min.css +1 -1
  3. package/dist/myetv-player.js +654 -129
  4. package/dist/myetv-player.min.js +579 -115
  5. package/package.json +35 -16
  6. package/plugins/twitch/myetv-player-twitch-plugin.js +125 -11
  7. package/plugins/vimeo/myetv-player-vimeo.js +80 -49
  8. package/plugins/youtube/README.md +5 -2
  9. package/plugins/youtube/myetv-player-youtube-plugin.js +891 -14
  10. package/.github/workflows/codeql.yml +0 -100
  11. package/.github/workflows/npm-publish.yml +0 -30
  12. package/SECURITY.md +0 -50
  13. package/build.js +0 -195
  14. package/scss/README.md +0 -161
  15. package/scss/_audio-player.scss +0 -21
  16. package/scss/_base.scss +0 -116
  17. package/scss/_controls.scss +0 -188
  18. package/scss/_loading.scss +0 -111
  19. package/scss/_menus.scss +0 -432
  20. package/scss/_mixins.scss +0 -112
  21. package/scss/_poster.scss +0 -8
  22. package/scss/_progress-bar.scss +0 -319
  23. package/scss/_resolution.scss +0 -68
  24. package/scss/_responsive.scss +0 -1360
  25. package/scss/_themes.scss +0 -30
  26. package/scss/_title-overlay.scss +0 -60
  27. package/scss/_tooltips.scss +0 -7
  28. package/scss/_variables.scss +0 -49
  29. package/scss/_video.scss +0 -221
  30. package/scss/_volume.scss +0 -122
  31. package/scss/_watermark.scss +0 -128
  32. package/scss/myetv-player.scss +0 -51
  33. package/scss/package.json +0 -16
  34. package/src/README.md +0 -560
  35. package/src/chapters.js +0 -521
  36. package/src/controls.js +0 -1243
  37. package/src/core.js +0 -1922
  38. package/src/events.js +0 -456
  39. package/src/fullscreen.js +0 -82
  40. package/src/i18n.js +0 -374
  41. package/src/playlist.js +0 -177
  42. package/src/plugins.js +0 -384
  43. package/src/quality.js +0 -963
  44. package/src/streaming.js +0 -346
  45. package/src/subtitles.js +0 -524
  46. package/src/utils.js +0 -65
  47. package/src/watermark.js +0 -246
package/src/controls.js DELETED
@@ -1,1243 +0,0 @@
1
-
2
- /* Controls Module for MYETV Video Player
3
- * Conservative modularization - original code preserved exactly
4
- * Created by https://www.myetv.tv https://oskarcosimo.com
5
- */
6
-
7
- /* AUTO-HIDE SYSTEM */
8
- initAutoHide() {
9
- if (!this.options.autoHide) {
10
- if (this.options.debug) console.log('Auto-hide disabled in options');
11
- return;
12
- }
13
-
14
- if (this.autoHideInitialized) {
15
- if (this.options.debug) console.log('Auto-hide already initialized');
16
- return;
17
- }
18
-
19
- if (this.options.debug) console.log('Initializing auto-hide system');
20
-
21
- // CHECK DOM ELEMENTS EXISTENCE
22
- if (!this.container) {
23
- if (this.options.debug) console.error('Container not found! Auto-hide cannot work');
24
- return;
25
- }
26
-
27
- if (!this.controls) {
28
- if (this.options.debug) console.error('Controls not found! Auto-hide cannot work');
29
- return;
30
- }
31
-
32
- if (this.options.debug) console.log('DOM elements verified:', {
33
- container: !!this.container,
34
- controls: !!this.controls,
35
- video: !!this.video
36
- });
37
-
38
- // Show controls initially
39
- this.showControlsNow();
40
-
41
- // Event listener for mousemove
42
- this.container.addEventListener('mousemove', (e) => {
43
- if (this.autoHideDebug) {
44
- if (this.options.debug) console.log('Mouse movement in container - reset timer');
45
- }
46
- this.onMouseMoveInPlayer(e);
47
- });
48
-
49
- if (this.options.debug) console.log('📡 Event listener mousemove added to container');
50
-
51
- // Event listener for mouseenter/mouseleave
52
- this.controls.addEventListener('mouseenter', (e) => {
53
- if (this.autoHideDebug) {
54
- if (this.options.debug) console.log('Mouse ENTERS controls - cancel timer');
55
- }
56
- this.onMouseEnterControls(e);
57
- });
58
-
59
- this.controls.addEventListener('mouseleave', (e) => {
60
- if (this.autoHideDebug) {
61
- if (this.options.debug) console.log('Mouse EXITS controls - restart timer');
62
-
63
- // Touch events for mobile devices
64
- this.container.addEventListener('touchstart', () => {
65
- this.showControlsNow();
66
- this.resetAutoHideTimer();
67
- });
68
-
69
- this.container.addEventListener('touchend', () => {
70
- this.resetAutoHideTimer();
71
- });
72
- }
73
- this.onMouseLeaveControls(e);
74
- });
75
-
76
- if (this.options.debug) console.log('Event listener mouseenter/mouseleave added to controls');
77
-
78
- this.autoHideInitialized = true;
79
- if (this.options.debug) console.log('Auto-hide system fully initialized');
80
-
81
- // Test
82
- this.resetAutoHideTimer();
83
- if (this.options.debug) console.log('Initial timer started');
84
- }
85
-
86
- onMouseMoveInPlayer(e) {
87
- this.showControlsNow();
88
- this.showCursor();
89
- this.resetAutoHideTimer();
90
- }
91
-
92
- onMouseEnterControls(e) {
93
- this.mouseOverControls = true;
94
- this.showControlsNow();
95
-
96
- if (this.autoHideTimer) {
97
- clearTimeout(this.autoHideTimer);
98
- this.autoHideTimer = null;
99
- if (this.autoHideDebug) {
100
- if (this.options.debug) console.log('Auto-hide timer cancelled');
101
- }
102
- }
103
- }
104
-
105
- onMouseLeaveControls(e) {
106
- this.mouseOverControls = false;
107
- this.resetAutoHideTimer();
108
- }
109
-
110
- resetAutoHideTimer() {
111
- if (this.autoHideTimer) {
112
- clearTimeout(this.autoHideTimer);
113
- this.autoHideTimer = null;
114
- }
115
-
116
- const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
117
- if (this.mouseOverControls && !isTouchDevice) {
118
- if (this.autoHideDebug) {
119
- if (this.options.debug) console.log('Not starting timer - mouse on controls');
120
- }
121
- return;
122
- }
123
-
124
- if (this.video && this.video.paused) {
125
- if (this.autoHideDebug) {
126
- if (this.options.debug) console.log('Not starting timer - video paused');
127
- }
128
- return;
129
- }
130
-
131
- this.autoHideTimer = setTimeout(() => {
132
- if (this.autoHideDebug) {
133
- if (this.options.debug) console.log(`Timer expired after ${this.options.autoHideDelay}ms - nascondo controlli`);
134
- }
135
- this.hideControlsNow();
136
- }, this.options.autoHideDelay);
137
-
138
- if (this.autoHideDebug) {
139
- if (this.options.debug) console.log(`Auto-hide timer started: ${this.options.autoHideDelay}ms`);
140
- }
141
- }
142
-
143
- showControlsNow() {
144
- if (this.controls) {
145
- this.controls.classList.add('show');
146
-
147
- // Add has-controls class to container (for watermark visibility)
148
- if (this.container) {
149
- this.container.classList.add('has-controls');
150
- }
151
-
152
- this.updateControlbarHeight();
153
-
154
- // Update watermark position
155
- if (this.updateWatermarkPosition) {
156
- this.updateWatermarkPosition();
157
- }
158
-
159
- // Show title overlay with controls (if not persistent)
160
- if (this.options.showTitleOverlay && !this.options.persistentTitle && this.options.videoTitle) {
161
- this.showTitleOverlay();
162
- }
163
-
164
- // *show cursor when controls are shown*
165
- this.showCursor();
166
-
167
- if (this.autoHideDebug && this.options.debug) console.log('✅ Controls shown');
168
- }
169
- }
170
-
171
- hideControlsNow() {
172
- // Dont hide if mouse is still over controls (allow hiding on touch devices)
173
- const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
174
- if (this.mouseOverControls && !isTouchDevice) {
175
- if (this.autoHideDebug && this.options.debug) console.log('❌ Not hiding - mouse still over controls');
176
- return;
177
- }
178
-
179
- // Dont hide if video is paused
180
- if (this.video && this.video.paused) {
181
- if (this.autoHideDebug && this.options.debug) console.log('❌ Not hiding - video is paused');
182
- return;
183
- }
184
-
185
- if (this.controls) {
186
- this.controls.classList.remove('show');
187
-
188
- // Remove has-controls class from container (for watermark visibility)
189
- if (this.container) {
190
- this.container.classList.remove('has-controls');
191
- }
192
-
193
- this.updateControlbarHeight();
194
-
195
- // Update watermark position
196
- if (this.updateWatermarkPosition) {
197
- this.updateWatermarkPosition();
198
- }
199
-
200
- // Hide title overlay with controls (if not persistent)
201
- if (this.options.showTitleOverlay && !this.options.persistentTitle) {
202
- this.hideTitleOverlay();
203
- }
204
-
205
- // *hide cursor after controls are hidden*
206
- this.hideCursor();
207
-
208
- if (this.autoHideDebug && this.options.debug) console.log('✅ Controls hidden');
209
- }
210
- }
211
-
212
- showControls() {
213
- this.showControlsNow();
214
- this.resetAutoHideTimer();
215
- }
216
-
217
- hideControls() {
218
- this.hideControlsNow();
219
- }
220
-
221
- hideControlsWithDelay() {
222
- this.resetAutoHideTimer();
223
- }
224
-
225
- clearControlsTimeout() {
226
- if (this.autoHideTimer) {
227
- clearTimeout(this.autoHideTimer);
228
- this.autoHideTimer = null;
229
- }
230
- }
231
-
232
- // Default controlbar styles injection
233
- injectDefaultControlbarStyles() {
234
- if (document.getElementById('default-controlbar-styles')) {
235
- return;
236
- }
237
-
238
- const controlBarOpacity = Math.max(0, Math.min(1, this.options.controlBarOpacity));
239
- const titleOverlayOpacity = Math.max(0, Math.min(1, this.options.titleOverlayOpacity));
240
-
241
- const style = document.createElement('style');
242
- style.id = 'default-controlbar-styles';
243
- style.textContent = `
244
- .video-wrapper:not(.youtube-active):not(.vimeo-active):not(.facebook-active) .controls {
245
- background: linear-gradient(
246
- to top,
247
- rgba(0, 0, 0, ${controlBarOpacity}) 0%,
248
- rgba(0, 0, 0, ${controlBarOpacity * 0.89}) 20%,
249
- rgba(0, 0, 0, ${controlBarOpacity * 0.74}) 40%,
250
- rgba(0, 0, 0, ${controlBarOpacity * 0.53}) 60%,
251
- rgba(0, 0, 0, ${controlBarOpacity * 0.32}) 80%,
252
- rgba(0, 0, 0, ${controlBarOpacity * 0.21}) 100%
253
- );
254
- backdrop-filter: blur(3px);
255
- min-height: 60px;
256
- padding-bottom: 10px;
257
- }
258
-
259
- .video-wrapper:not(.youtube-active):not(.vimeo-active):not(.facebook-active) .title-overlay {
260
- background: linear-gradient(
261
- to bottom,
262
- rgba(0, 0, 0, ${titleOverlayOpacity}) 0%,
263
- rgba(0, 0, 0, ${titleOverlayOpacity * 0.89}) 20%,
264
- rgba(0, 0, 0, ${titleOverlayOpacity * 0.74}) 40%,
265
- rgba(0, 0, 0, ${titleOverlayOpacity * 0.53}) 60%,
266
- rgba(0, 0, 0, ${titleOverlayOpacity * 0.32}) 80%,
267
- rgba(0, 0, 0, ${titleOverlayOpacity * 0.21}) 100%
268
- );
269
- backdrop-filter: blur(3px);
270
- min-height: 80px;
271
- padding-top: 20px;
272
- }
273
- `;
274
-
275
- document.head.appendChild(style);
276
- }
277
-
278
- // Debug methods
279
- enableAutoHideDebug() {
280
- this.autoHideDebug = true;
281
- if (this.options.debug) console.log('AUTO-HIDE DEBUG ENABLED');
282
- if (this.options.debug) console.log('Stato attuale:', {
283
- initialized: this.autoHideInitialized,
284
- autoHide: this.options.autoHide,
285
- delay: this.options.autoHideDelay,
286
- mouseOverControls: this.mouseOverControls,
287
- timerActive: !!this.autoHideTimer,
288
- container: !!this.container,
289
- controls: !!this.controls,
290
- video: !!this.video,
291
- videoPaused: this.video ? this.video.paused : 'N/A'
292
- });
293
-
294
- if (!this.autoHideInitialized) {
295
- if (this.options.debug) console.log('Auto-hide NOT yet initialized! Initializing now...');
296
- this.initAutoHide();
297
- }
298
- }
299
-
300
- disableAutoHideDebug() {
301
- this.autoHideDebug = false;
302
- if (this.options.debug) console.log('Auto-hide debug disabled');
303
- }
304
-
305
- testAutoHide() {
306
- if (this.options.debug) console.log('TEST AUTO-HIDE COMPLETED:');
307
- if (this.options.debug) console.log('System status:', {
308
- initialized: this.autoHideInitialized,
309
- autoHide: this.options.autoHide,
310
- delay: this.options.autoHideDelay,
311
- mouseOverControls: this.mouseOverControls,
312
- timerActive: !!this.autoHideTimer
313
- });
314
-
315
- if (this.options.debug) console.log('Elementi DOM:', {
316
- container: !!this.container,
317
- controls: !!this.controls,
318
- video: !!this.video
319
- });
320
-
321
- if (this.options.debug) console.log('Stato video:', {
322
- paused: this.video ? this.video.paused : 'N/A',
323
- currentTime: this.video ? this.video.currentTime : 'N/A',
324
- duration: this.video ? this.video.duration : 'N/A'
325
- });
326
-
327
- if (!this.autoHideInitialized) {
328
- if (this.options.debug) console.log('PROBLEM: Auto-hide not initialized!');
329
- if (this.options.debug) console.log('Forcing initialization...');
330
- this.initAutoHide();
331
- } else {
332
- if (this.options.debug) console.log('Auto-hide initialized correctly');
333
- if (this.options.debug) console.log('Forcing timer reset for test...');
334
- this.resetAutoHideTimer();
335
- }
336
- }
337
-
338
- /* SUBTITLES UI MANAGEMENT */
339
- updateSubtitlesUI() {
340
- const subtitlesControl = this.controls?.querySelector('.subtitles-control');
341
-
342
- if (this.textTracks.length > 0 && this.options.showSubtitles) {
343
- if (subtitlesControl) {
344
- subtitlesControl.style.display = 'block';
345
- }
346
- this.populateSubtitlesMenu();
347
- } else {
348
- if (subtitlesControl) {
349
- subtitlesControl.style.display = 'none';
350
- }
351
- }
352
- }
353
-
354
- populateSubtitlesMenu() {
355
- const subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
356
- if (!subtitlesMenu) return;
357
-
358
- let menuHTML = `<div class="subtitles-option ${!this.subtitlesEnabled ? 'active' : ''}" data-track="off">${this.t('subtitlesoff') || 'Off'}</div>`;
359
-
360
- this.textTracks.forEach((trackData, index) => {
361
- const isActive = this.currentSubtitleTrack === trackData.track;
362
- menuHTML += `<div class="subtitles-option ${isActive ? 'active' : ''}" data-track="${index}">${trackData.label}</div>`;
363
- });
364
-
365
- subtitlesMenu.innerHTML = menuHTML;
366
- }
367
-
368
- toggleSubtitles() {
369
- if (this.textTracks.length === 0) return;
370
-
371
- if (this.subtitlesEnabled) {
372
- this.disableSubtitles();
373
- } else {
374
- this.enableSubtitleTrack(0);
375
- }
376
- }
377
-
378
- updateSubtitlesButton() {
379
- const subtitlesBtn = this.controls?.querySelector('.subtitles-btn');
380
- if (!subtitlesBtn) return;
381
-
382
- if (this.subtitlesEnabled) {
383
- subtitlesBtn.classList.add('active');
384
- subtitlesBtn.title = this.t('subtitlesdisable') || 'Disable subtitles';
385
- } else {
386
- subtitlesBtn.classList.remove('active');
387
- subtitlesBtn.title = this.t('subtitlesenable') || 'Enable subtitles';
388
- }
389
- }
390
-
391
- handleSubtitlesMenuClick(e) {
392
- if (!e.target.classList.contains('subtitles-option')) return;
393
-
394
- const trackData = e.target.getAttribute('data-track');
395
-
396
- if (trackData === 'off') {
397
- this.disableSubtitles();
398
- } else {
399
- const trackIndex = parseInt(trackData);
400
- this.enableSubtitleTrack(trackIndex);
401
- }
402
- }
403
-
404
- /* PLAYER CONTROLS SETUP */
405
- hideNativePlayer() {
406
- this.video.controls = false;
407
- this.video.setAttribute('controls', 'false');
408
- this.video.removeAttribute('controls');
409
- this.video.style.visibility = 'hidden';
410
- this.video.style.opacity = '0';
411
- this.video.style.pointerEvents = 'none';
412
- this.video.classList.add('video-player');
413
- }
414
-
415
- createControls() {
416
- const controlsId = `videoControls-${this.getUniqueId()}`;
417
-
418
- const controlsHTML = `
419
- <div class="controls" id="${controlsId}">
420
- <div class="progress-container">
421
- <div class="progress-bar">
422
- <div class="progress-buffer"></div>
423
- <div class="progress-filled"></div>
424
- </div>
425
- <div class="progress-handle progress-handle-${this.options.seekHandleShape}"></div> <!-- ✅ Fuori da progress-bar -->
426
- ${this.options.showSeekTooltip ? '<div class="seek-tooltip">0:00</div>' : ''}
427
- </div>
428
-
429
- <div class="controls-main">
430
- <div class="controls-left">
431
- <button class="control-btn play-pause-btn" data-tooltip="play_pause">
432
- <span class="icon play-icon">▶</span>
433
- <span class="icon pause-icon hidden">⏸</span>
434
- </button>
435
-
436
- <button class="control-btn mute-btn" data-tooltip="mute_unmute">
437
- <span class="icon volume-icon">🔊</span>
438
- <span class="icon mute-icon hidden">🔇</span>
439
- </button>
440
-
441
- <div class="volume-container" data-mobile-slider="${this.options.volumeSlider}">
442
- <input type="range" class="volume-slider" min="0" max="100" value="100" data-tooltip="volume">
443
- </div>
444
-
445
- <div class="time-display">
446
- <span class="current-time">0:00</span>
447
- <span>/</span>
448
- <span class="duration">0:00</span>
449
- </div>
450
- </div>
451
-
452
- <div class="controls-right">
453
- <button class="control-btn playlist-prev-btn" data-tooltip="prevvideo" style="display: none;">
454
- <span class="icon">⏮</span>
455
- </button>
456
-
457
- <button class="control-btn playlist-next-btn" data-tooltip="nextvideo" style="display: none;">
458
- <span class="icon">⏭</span>
459
- </button>
460
-
461
- ${this.options.showSubtitles ? `
462
- <div class="subtitles-control" style="display: none;">
463
- <button class="control-btn subtitles-btn" data-tooltip="subtitles">
464
- <span class="icon">CC</span>
465
- </button>
466
- <div class="subtitles-menu">
467
- <div class="subtitles-option active" data-track="off">Off</div>
468
- </div>
469
- </div>
470
- ` : ''}
471
-
472
- ${this.options.showSpeedControl ? `
473
- <div class="speed-control">
474
- <button class="control-btn speed-btn" data-tooltip="playback_speed">1x</button>
475
- <div class="speed-menu">
476
- <div class="speed-option" data-speed="0.5">0.5x</div>
477
- <div class="speed-option" data-speed="0.75">0.75x</div>
478
- <div class="speed-option active" data-speed="1">1x</div>
479
- <div class="speed-option" data-speed="1.25">1.25x</div>
480
- <div class="speed-option" data-speed="1.5">1.5x</div>
481
- <div class="speed-option" data-speed="2">2x</div>
482
- </div>
483
- </div>
484
- ` : ''}
485
-
486
- ${this.options.showQualitySelector && this.originalSources && this.originalSources.length > 1 ? `
487
- <div class="quality-control">
488
- <button class="control-btn quality-btn" data-tooltip="video_quality">
489
- <div class="quality-btn-text">
490
- <div class="selected-quality">${this.t('auto')}</div>
491
- <div class="current-quality"></div>
492
- </div>
493
- </button>
494
- <div class="quality-menu">
495
- <div class="quality-option selected" data-quality="auto">${this.t('auto')}</div>
496
- ${this.originalSources.map(s =>
497
- `<div class="quality-option" data-quality="${s.quality}">${s.quality}</div>`
498
- ).join('')}
499
- </div>
500
- </div>
501
- ` : ''}
502
-
503
- ${this.options.showPictureInPicture && this.isPiPSupported ? `
504
- <button class="control-btn pip-btn" data-tooltip="picture_in_picture">
505
- <span class="icon pip-icon">⧉</span>
506
- <span class="icon pip-exit-icon hidden">⧉</span>
507
- </button>
508
- ` : ''}
509
-
510
- <div class="settings-control">
511
- <button class="control-btn settings-btn" data-tooltip="settings_menu">
512
- <span class="">⚙</span>
513
- </button>
514
- <div class="settings-menu"></div>
515
- </div>
516
-
517
- ${this.options.showFullscreen ? `
518
- <button class="control-btn fullscreen-btn" data-tooltip="fullscreen">
519
- <span class="icon fullscreen-icon">⛶</span>
520
- <span class="icon exit-fullscreen-icon hidden">⛉</span>
521
- </button>
522
- ` : ''}
523
- </div>
524
- </div>
525
- </div>
526
- `;
527
-
528
- this.container.insertAdjacentHTML('beforeend', controlsHTML);
529
- this.controls = document.getElementById(controlsId);
530
-
531
- // NEW: Initialize responsive settings menu
532
- setTimeout(() => {
533
- this.initializeResponsiveMenu();
534
- this.updateControlbarHeight();
535
- }, 100);
536
- }
537
-
538
- /* Initialize responsive menu with dynamic width calculation */
539
- initializeResponsiveMenu() {
540
- if (!this.controls) return;
541
-
542
- // Track screen size
543
- this.isSmallScreen = false;
544
-
545
- // Check initial size
546
- this.checkScreenSize();
547
-
548
- // Bind resize handler with updateControlbarHeight
549
- const resizeHandler = () => {
550
- this.checkScreenSize();
551
- this.updateControlbarHeight();
552
- };
553
-
554
- // Bind del context
555
- this.resizeHandler = resizeHandler.bind(this);
556
- window.addEventListener('resize', this.resizeHandler);
557
-
558
- // Bind events for settings menu
559
- this.bindSettingsMenuEvents();
560
- }
561
-
562
- // Dynamic controlbar height tracking for watermark positioning
563
- updateControlbarHeight() {
564
- if (!this.controls) return;
565
-
566
- const height = this.controls.offsetHeight;
567
- if (this.container) {
568
-
569
- this.container.style.setProperty('--player-controls-height', `${height}px`);
570
-
571
- const watermark = this.container.querySelector('.video-watermark.watermark-bottomleft, .video-watermark.watermark-bottomright');
572
- if (watermark) {
573
- const hasControls = this.container.classList.contains('has-controls');
574
- const isHideOnAutoHide = watermark.classList.contains('hide-on-autohide');
575
-
576
- if (hasControls || !isHideOnAutoHide) {
577
- watermark.style.bottom = `${height + 15}px`;
578
- } else {
579
- watermark.style.bottom = '15px';
580
- }
581
- }
582
- }
583
-
584
- if (this.options.debug) {
585
- console.log(`Controlbar height updated: ${height}px`);
586
- }
587
- }
588
-
589
- /* Dynamic width calculation based on logo presence */
590
- getResponsiveThreshold() {
591
- // Check if brand logo is enabled and present
592
- const hasLogo = this.options.brandLogoEnabled && this.options.brandLogoUrl;
593
-
594
- // If logo is present, use higher threshold (650px), otherwise 550px
595
- return hasLogo ? 650 : 550;
596
- }
597
-
598
- /* Check if screen is under dynamic threshold */
599
- checkScreenSize() {
600
- const threshold = this.getResponsiveThreshold();
601
- const newIsSmallScreen = window.innerWidth <= threshold;
602
-
603
- if (newIsSmallScreen !== this.isSmallScreen) {
604
- this.isSmallScreen = newIsSmallScreen;
605
- this.updateSettingsMenuVisibility();
606
-
607
- if (this.options.debug) {
608
- console.log(`Screen check: ${window.innerWidth}px vs ${threshold}px (threshold), logo: ${this.options.brandLogoEnabled}, small: ${this.isSmallScreen}`);
609
- }
610
- }
611
- }
612
-
613
- /* Update settings menu visibility based on screen size */
614
- updateSettingsMenuVisibility() {
615
- const settingsControl = this.controls?.querySelector('.settings-control');
616
- if (!settingsControl) return;
617
-
618
- if (this.isSmallScreen) {
619
- // Show settings menu and hide individual controls
620
- settingsControl.style.display = 'block';
621
-
622
- // Hide controls that will be moved to settings menu
623
- const pipBtn = this.controls.querySelector('.pip-btn');
624
- const speedControl = this.controls.querySelector('.speed-control');
625
- const subtitlesControl = this.controls.querySelector('.subtitles-control');
626
-
627
- if (pipBtn) pipBtn.style.display = 'none';
628
- if (speedControl) speedControl.style.display = 'none';
629
- if (subtitlesControl) subtitlesControl.style.display = 'none';
630
-
631
- this.populateSettingsMenu();
632
- } else {
633
- // Hide settings menu and show individual controls
634
- settingsControl.style.display = 'none';
635
-
636
- // Show original controls
637
- const pipBtn = this.controls.querySelector('.pip-btn');
638
- const speedControl = this.controls.querySelector('.speed-control');
639
- const subtitlesControl = this.controls.querySelector('.subtitles-control');
640
-
641
- if (pipBtn && this.options.showPictureInPicture && this.isPiPSupported) {
642
- pipBtn.style.display = 'flex';
643
- }
644
- if (speedControl && this.options.showSpeedControl) {
645
- speedControl.style.display = 'block';
646
- }
647
- if (subtitlesControl && this.options.showSubtitles && this.textTracks.length > 0) {
648
- subtitlesControl.style.display = 'block';
649
- }
650
- }
651
- }
652
-
653
- /**
654
- * Populate settings menu with controls
655
- */
656
- populateSettingsMenu() {
657
- const settingsMenu = this.controls?.querySelector('.settings-menu');
658
- if (!settingsMenu) return;
659
-
660
- let menuHTML = '';
661
-
662
- // Picture-in-Picture option
663
- if (this.options.showPictureInPicture && this.isPiPSupported) {
664
- const pipLabel = this.t('picture_in_picture') || 'Picture-in-Picture';
665
- menuHTML += `<div class="settings-option" data-action="pip">
666
- <span class="settings-option-label">${pipLabel}</span>
667
- </div>`;
668
- }
669
-
670
- // Speed Control - expandable
671
- if (this.options.showSpeedControl) {
672
- const speedLabel = this.t('playback_speed') || 'Playback Speed';
673
- const currentSpeed = this.video ? this.video.playbackRate : 1;
674
-
675
- menuHTML += `
676
- <div class="settings-expandable-wrapper">
677
- <div class="settings-option expandable-trigger" data-action="speed-expand">
678
- <span class="settings-option-label">${speedLabel}: ${currentSpeed}x</span>
679
- <span class="expand-arrow">▼</span>
680
- </div>
681
- <div class="settings-expandable-content" style="display: none;">`;
682
-
683
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
684
- speeds.forEach(speed => {
685
- const isActive = Math.abs(speed - currentSpeed) < 0.01;
686
- menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-speed="${speed}">${speed}x</div>`;
687
- });
688
-
689
- menuHTML += `</div></div>`;
690
- }
691
-
692
- // Subtitles - expandable
693
- if (this.options.showSubtitles && this.textTracks && this.textTracks.length > 0) {
694
- const subtitlesLabel = this.t('subtitles') || 'Subtitles';
695
- const currentTrack = this.currentSubtitleTrack;
696
- const currentLabel = this.subtitlesEnabled && currentTrack ? currentTrack.label : (this.t('subtitlesoff') || 'Off');
697
-
698
- menuHTML += `
699
- <div class="settings-expandable-wrapper">
700
- <div class="settings-option expandable-trigger" data-action="subtitles-expand">
701
- <span class="settings-option-label">${subtitlesLabel}: ${currentLabel}</span>
702
- <span class="expand-arrow">▼</span>
703
- </div>
704
- <div class="settings-expandable-content" style="display: none;">`;
705
-
706
- // Off option
707
- menuHTML += `<div class="settings-suboption ${!this.subtitlesEnabled ? 'active' : ''}" data-track="off">${this.t('subtitlesoff') || 'Off'}</div>`;
708
-
709
- // Subtitle tracks
710
- this.textTracks.forEach((trackData, index) => {
711
- const isActive = this.currentSubtitleTrack === trackData.track;
712
- menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-track="${index}">${trackData.label}</div>`;
713
- });
714
-
715
- menuHTML += `</div></div>`;
716
- }
717
-
718
- settingsMenu.innerHTML = menuHTML;
719
-
720
- // Add scrollbar if needed
721
- this.addSettingsMenuScrollbar();
722
- }
723
-
724
- /**
725
- * Add scrollbar to settings menu on mobile
726
- */
727
- addSettingsMenuScrollbar() {
728
- const settingsMenu = this.controls?.querySelector('.settings-menu');
729
- if (!settingsMenu) return;
730
-
731
- const playerHeight = this.container.offsetHeight;
732
- const maxMenuHeight = playerHeight - 100;
733
-
734
- settingsMenu.style.maxHeight = `${maxMenuHeight}px`;
735
- settingsMenu.style.overflowY = 'auto';
736
- settingsMenu.style.overflowX = 'hidden';
737
-
738
- // Add scrollbar styling
739
- if (!document.getElementById('player-settings-scrollbar-style')) {
740
- const scrollbarStyle = document.createElement('style');
741
- scrollbarStyle.id = 'player-settings-scrollbar-style';
742
- scrollbarStyle.textContent = `
743
- .settings-menu::-webkit-scrollbar {
744
- width: 6px;
745
- }
746
- .settings-menu::-webkit-scrollbar-track {
747
- background: rgba(255,255,255,0.05);
748
- border-radius: 3px;
749
- }
750
- .settings-menu::-webkit-scrollbar-thumb {
751
- background: rgba(255,255,255,0.3);
752
- border-radius: 3px;
753
- }
754
- .settings-menu::-webkit-scrollbar-thumb:hover {
755
- background: rgba(255,255,255,0.5);
756
- }
757
- `;
758
- document.head.appendChild(scrollbarStyle);
759
- }
760
-
761
- settingsMenu.style.scrollbarWidth = 'thin';
762
- settingsMenu.style.scrollbarColor = 'rgba(255,255,255,0.3) transparent';
763
- }
764
-
765
- /**
766
- * Bind settings menu events
767
- */
768
- bindSettingsMenuEvents() {
769
- const settingsMenu = this.controls?.querySelector('.settings-menu');
770
- if (!settingsMenu) return;
771
-
772
- settingsMenu.addEventListener('click', (e) => {
773
- e.stopPropagation();
774
-
775
- // Handle expandable triggers
776
- if (e.target.classList.contains('expandable-trigger') || e.target.closest('.expandable-trigger')) {
777
- const trigger = e.target.classList.contains('expandable-trigger') ? e.target : e.target.closest('.expandable-trigger');
778
- const wrapper = trigger.closest('.settings-expandable-wrapper');
779
- const content = wrapper.querySelector('.settings-expandable-content');
780
- const arrow = trigger.querySelector('.expand-arrow');
781
-
782
- const isExpanded = content.style.display !== 'none';
783
-
784
- if (isExpanded) {
785
- content.style.display = 'none';
786
- arrow.style.transform = 'rotate(0deg)';
787
- } else {
788
- content.style.display = 'block';
789
- arrow.style.transform = 'rotate(180deg)';
790
- }
791
- return;
792
- }
793
-
794
- // Handle direct actions (like PiP)
795
- if (e.target.classList.contains('settings-option') || e.target.closest('.settings-option')) {
796
- const option = e.target.classList.contains('settings-option') ? e.target : e.target.closest('.settings-option');
797
- const action = option.getAttribute('data-action');
798
-
799
- if (action === 'pip') {
800
- this.togglePictureInPicture();
801
- return;
802
- }
803
- }
804
-
805
- // Handle submenu actions
806
- if (e.target.classList.contains('settings-suboption')) {
807
- const wrapper = e.target.closest('.settings-expandable-wrapper');
808
- const trigger = wrapper.querySelector('.expandable-trigger');
809
- const action = trigger.getAttribute('data-action');
810
-
811
- if (action === 'speed-expand') {
812
- const speed = parseFloat(e.target.getAttribute('data-speed'));
813
- if (speed && speed > 0 && this.video && !this.isChangingQuality) {
814
- this.video.playbackRate = speed;
815
-
816
- // Update active states
817
- wrapper.querySelectorAll('.settings-suboption').forEach(opt => opt.classList.remove('active'));
818
- e.target.classList.add('active');
819
-
820
- // Update trigger text
821
- const label = trigger.querySelector('.settings-option-label');
822
- if (label) {
823
- const speedLabel = this.t('playback_speed') || 'Playback Speed';
824
- label.textContent = `${speedLabel}: ${speed}x`;
825
- }
826
-
827
- // Trigger event
828
- this.triggerEvent('speedchange', { speed, previousSpeed: this.video.playbackRate });
829
- }
830
- } else if (action === 'subtitles-expand') {
831
- const trackData = e.target.getAttribute('data-track');
832
- if (trackData === 'off') {
833
- this.disableSubtitles();
834
- } else {
835
- const trackIndex = parseInt(trackData);
836
- this.enableSubtitleTrack(trackIndex);
837
- }
838
-
839
- // Update active states
840
- wrapper.querySelectorAll('.settings-suboption').forEach(opt => opt.classList.remove('active'));
841
- e.target.classList.add('active');
842
-
843
- // Update trigger text
844
- const label = trigger.querySelector('.settings-option-label');
845
- if (label) {
846
- const subtitlesLabel = this.t('subtitles') || 'Subtitles';
847
- label.textContent = `${subtitlesLabel}: ${e.target.textContent}`;
848
- }
849
- }
850
- }
851
- });
852
- }
853
-
854
- /* TITLE OVERLAY MANAGEMENT */
855
- showTitleOverlay() {
856
- if (this.titleOverlay && this.options.videoTitle) {
857
- this.titleOverlay.classList.add('show');
858
-
859
- if (this.options.persistentTitle) {
860
- this.titleOverlay.classList.add('persistent');
861
- } else {
862
- this.titleOverlay.classList.remove('persistent');
863
- }
864
- }
865
- return this;
866
- }
867
-
868
- hideTitleOverlay() {
869
- if (this.titleOverlay) {
870
- this.titleOverlay.classList.remove('show');
871
- this.titleOverlay.classList.remove('persistent');
872
- }
873
- return this;
874
- }
875
-
876
- toggleTitleOverlay(show = null) {
877
- if (show === null) {
878
- return this.titleOverlay && this.titleOverlay.classList.contains('show')
879
- ? this.hideTitleOverlay()
880
- : this.showTitleOverlay();
881
- }
882
-
883
- return show ? this.showTitleOverlay() : this.hideTitleOverlay();
884
- }
885
-
886
- /* KEYBOARD CONTROLS */
887
- setupKeyboardControls() {
888
- document.addEventListener('keydown', (e) => {
889
- // Ignore if user is typing in an input field
890
- if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
891
-
892
- // On keyboard input, treat as mouse movement for auto-hide
893
- if (this.options.autoHide && this.autoHideInitialized) {
894
- this.showControlsNow();
895
- this.resetAutoHideTimer();
896
- }
897
-
898
- switch (e.code) {
899
- case 'Space':
900
- e.preventDefault();
901
- this.togglePlayPause();
902
- break;
903
- case 'KeyM':
904
- this.toggleMute();
905
- break;
906
- case 'KeyF':
907
- if (this.options.showFullscreen) {
908
- this.toggleFullscreen();
909
- }
910
- break;
911
- case 'KeyP':
912
- if (this.options.showPictureInPicture && this.isPiPSupported) {
913
- this.togglePictureInPicture();
914
- }
915
- break;
916
- case 'KeyT':
917
- if (this.options.showTitleOverlay) {
918
- this.toggleTitleOverlay();
919
- }
920
- break;
921
- case 'KeyS':
922
- if (this.options.showSubtitles) {
923
- this.toggleSubtitles();
924
- }
925
- break;
926
- case 'KeyD':
927
- this.debugQuality ? this.disableQualityDebug() : this.enableQualityDebug();
928
- break;
929
- case 'ArrowLeft':
930
- e.preventDefault();
931
- this.skipTime(-10);
932
- break;
933
- case 'ArrowRight':
934
- e.preventDefault();
935
- this.skipTime(10);
936
- break;
937
- case 'ArrowUp':
938
- e.preventDefault();
939
- this.changeVolume(0.1);
940
- break;
941
- case 'ArrowDown':
942
- e.preventDefault();
943
- this.changeVolume(-0.1);
944
- break;
945
- }
946
- });
947
- }
948
-
949
- /* CONTROL ACTIONS */
950
- togglePlayPause() {
951
- if (!this.video || this.isChangingQuality) return;
952
-
953
- if (this.video.paused) {
954
- this.play();
955
- } else {
956
- this.pause();
957
- }
958
- }
959
-
960
- toggleMute() {
961
- if (!this.video) return;
962
-
963
- const wasMuted = this.video.muted;
964
- this.video.muted = !this.video.muted;
965
-
966
- this.updateMuteButton();
967
- this.updateVolumeSliderVisual();
968
- this.initVolumeTooltip();
969
-
970
- // Triggers volumechange event
971
- this.triggerEvent('volumechange', {
972
- volume: this.getVolume(),
973
- muted: this.isMuted(),
974
- previousMuted: wasMuted
975
- });
976
- }
977
-
978
- updateMuteButton() {
979
- if (!this.video || !this.volumeIcon || !this.muteIcon) return;
980
-
981
- if (this.video.muted || this.video.volume === 0) {
982
- this.volumeIcon.classList.add('hidden');
983
- this.muteIcon.classList.remove('hidden');
984
- } else {
985
- this.volumeIcon.classList.remove('hidden');
986
- this.muteIcon.classList.add('hidden');
987
- }
988
- }
989
-
990
- /* LOADING STATES */
991
- showLoading() {
992
- if (this.loadingOverlay) {
993
- this.loadingOverlay.classList.add('active');
994
- }
995
- }
996
-
997
- hideLoading() {
998
- if (this.loadingOverlay) {
999
- this.loadingOverlay.classList.remove('active');
1000
- }
1001
- }
1002
-
1003
- /* FULLSCREEN CONTROLS */
1004
- toggleFullscreen() {
1005
- if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement) {
1006
- this.exitFullscreen();
1007
- } else {
1008
- this.enterFullscreen();
1009
- }
1010
- }
1011
-
1012
- updateFullscreenButton() {
1013
- if (!this.fullscreenIcon || !this.exitFullscreenIcon) return;
1014
-
1015
- const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
1016
-
1017
- if (isFullscreen) {
1018
- this.fullscreenIcon.classList.add('hidden');
1019
- this.exitFullscreenIcon.classList.remove('hidden');
1020
- } else {
1021
- this.fullscreenIcon.classList.remove('hidden');
1022
- this.exitFullscreenIcon.classList.add('hidden');
1023
- }
1024
-
1025
- // Triggers fullscreenchange event
1026
- this.triggerEvent('fullscreenchange', {
1027
- active: !!isFullscreen,
1028
- mode: isFullscreen ? 'enter' : 'exit'
1029
- });
1030
- }
1031
-
1032
- /* PICTURE IN PICTURE CONTROLS */
1033
- togglePictureInPicture() {
1034
- if (!this.isPiPSupported || !this.video) return;
1035
-
1036
- if (document.pictureInPictureElement) {
1037
- this.exitPictureInPicture();
1038
- } else {
1039
- this.enterPictureInPicture();
1040
- }
1041
- }
1042
-
1043
- /* SEEK TOOLTIP MANAGEMENT */
1044
- toggleSeekTooltip(show = null) {
1045
- if (show === null) {
1046
- this.options.showSeekTooltip = !this.options.showSeekTooltip;
1047
- } else {
1048
- this.options.showSeekTooltip = show;
1049
- }
1050
-
1051
- if (this.seekTooltip) {
1052
- if (this.options.showSeekTooltip) {
1053
- this.setupSeekTooltip();
1054
- } else {
1055
- this.seekTooltip.classList.remove('visible');
1056
- }
1057
- }
1058
- }
1059
-
1060
- /* AUTO-HIDE CONFIGURATION */
1061
- setAutoHideDelay(delay) {
1062
- if (typeof delay === 'number' && delay >= 0) {
1063
- this.options.autoHideDelay = delay;
1064
- if (this.options.debug) console.log(`Auto-hide delay set to ${delay}ms`);
1065
- }
1066
- return this;
1067
- }
1068
-
1069
- getAutoHideDelay() {
1070
- return this.options.autoHideDelay;
1071
- }
1072
-
1073
- enableAutoHide() {
1074
- if (!this.options.autoHide) {
1075
- this.options.autoHide = true;
1076
- if (!this.autoHideInitialized) {
1077
- this.initAutoHide();
1078
- }
1079
- if (this.options.debug) console.log('Auto-hide enabled');
1080
- }
1081
- return this;
1082
- }
1083
-
1084
- disableAutoHide() {
1085
- if (this.options.autoHide) {
1086
- this.options.autoHide = false;
1087
- if (this.autoHideTimer) {
1088
- clearTimeout(this.autoHideTimer);
1089
- this.autoHideTimer = null;
1090
- }
1091
- this.showControlsNow();
1092
- if (this.options.debug) console.log('Auto-hide disabled');
1093
- }
1094
- return this;
1095
- }
1096
-
1097
- forceShowControls() {
1098
- this.showControlsNow();
1099
- if (this.autoHideInitialized) {
1100
- this.resetAutoHideTimer();
1101
- }
1102
- return this;
1103
- }
1104
-
1105
- forceHideControls() {
1106
- if (!this.mouseOverControls && this.video && !this.video.paused) {
1107
- this.hideControlsNow();
1108
- }
1109
- return this;
1110
- }
1111
-
1112
- isAutoHideEnabled() {
1113
- return this.options.autoHide;
1114
- }
1115
-
1116
- isAutoHideInitialized() {
1117
- return this.autoHideInitialized;
1118
- }
1119
-
1120
- /**
1121
- * Hide mouse cursor in player container
1122
- * Only hides cursor in main container, not in plugin iframes
1123
- */
1124
- hideCursor() {
1125
- if (!this.options.hideCursor) {
1126
- return; // Do not hide cursor if option is disabled
1127
- }
1128
-
1129
- if (this.container) {
1130
- this.container.classList.add('hide-cursor');
1131
- if (this.options.debug) console.log('🖱️ Cursor hidden');
1132
- }
1133
- }
1134
-
1135
- /**
1136
- * Show mouse cursor in player container
1137
- */
1138
- showCursor() {
1139
- if (this.container) {
1140
- this.container.classList.remove('hide-cursor');
1141
- if (this.options.debug) console.log('🖱️ Cursor shown');
1142
- }
1143
- }
1144
-
1145
- /**
1146
- * Enable cursor hiding when controlbar is hidden
1147
- * @returns {Object} this
1148
- */
1149
- enableCursorHiding() {
1150
- this.options.hideCursor = true;
1151
- if (this.options.debug) console.log('Cursor hiding enabled');
1152
- return this;
1153
- }
1154
-
1155
- /**
1156
- * Disable cursor hiding - cursor will always be visible
1157
- * @returns {Object} this
1158
- */
1159
- disableCursorHiding() {
1160
- this.options.hideCursor = false;
1161
- this.showCursor(); // Ensure cursor is shown immediately
1162
- if (this.options.debug) console.log('Cursor hiding disabled');
1163
- return this;
1164
- }
1165
-
1166
- /**
1167
- * Check if cursor hiding is enabled
1168
- * @returns {Boolean} True if cursor hiding is enabled
1169
- */
1170
- isCursorHidingEnabled() {
1171
- return this.options.hideCursor;
1172
- }
1173
-
1174
- /* PLAYLIST CONTROLS */
1175
- showPlaylistControls() {
1176
- if (!this.playlistPrevBtn || !this.playlistNextBtn) return;
1177
-
1178
- this.playlistPrevBtn.style.display = 'flex';
1179
- this.playlistNextBtn.style.display = 'flex';
1180
- this.updatePlaylistButtons();
1181
-
1182
- if (this.options.debug) console.log('Playlist controls shown');
1183
- }
1184
-
1185
- hidePlaylistControls() {
1186
- if (!this.playlistPrevBtn || !this.playlistNextBtn) return;
1187
-
1188
- this.playlistPrevBtn.style.display = 'none';
1189
- this.playlistNextBtn.style.display = 'none';
1190
-
1191
- if (this.options.debug) console.log('Playlist controls hidden');
1192
- }
1193
-
1194
- updatePlaylistButtons() {
1195
- if (!this.playlistPrevBtn || !this.playlistNextBtn || !this.isPlaylistActive) return;
1196
-
1197
- const canGoPrev = this.currentPlaylistIndex > 0 || this.options.playlistLoop;
1198
- const canGoNext = this.currentPlaylistIndex < this.playlist.length - 1 || this.options.playlistLoop;
1199
-
1200
- this.playlistPrevBtn.disabled = !canGoPrev;
1201
- this.playlistNextBtn.disabled = !canGoNext;
1202
-
1203
- // Update visual state
1204
- if (canGoPrev) {
1205
- this.playlistPrevBtn.style.opacity = '1';
1206
- this.playlistPrevBtn.style.cursor = 'pointer';
1207
- } else {
1208
- this.playlistPrevBtn.style.opacity = '0.4';
1209
- this.playlistPrevBtn.style.cursor = 'not-allowed';
1210
- }
1211
-
1212
- if (canGoNext) {
1213
- this.playlistNextBtn.style.opacity = '1';
1214
- this.playlistNextBtn.style.cursor = 'pointer';
1215
- } else {
1216
- this.playlistNextBtn.style.opacity = '0.4';
1217
- this.playlistNextBtn.style.cursor = 'not-allowed';
1218
- }
1219
- }
1220
-
1221
- /* RESPONSIVE OPTIMIZATION */
1222
- optimizeButtonsForSmallHeight() {
1223
- const currentHeight = window.innerHeight;
1224
- const controlsRect = this.controls.getBoundingClientRect();
1225
-
1226
- // If controlbar is taller than 40% of viewport, optimize
1227
- if (controlsRect.height > currentHeight * 0.4) {
1228
- this.controls.classList.add('ultra-compact');
1229
- if (this.options.debug) console.log('Applied ultra-compact mode for height:', currentHeight);
1230
- } else {
1231
- this.controls.classList.remove('ultra-compact');
1232
- }
1233
-
1234
- // Hide non-essential buttons on very small heights
1235
- const nonEssentialButtons = this.controls.querySelectorAll('.pip-btn, .speed-control');
1236
- if (currentHeight < 180) {
1237
- nonEssentialButtons.forEach(btn => btn.style.display = 'none');
1238
- } else {
1239
- nonEssentialButtons.forEach(btn => btn.style.display = '');
1240
- }
1241
- }
1242
-
1243
- /* Controls methods for main class - All original functionality preserved exactly */