myetv-player 1.0.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 (50) hide show
  1. package/.github/workflows/npm-publish.yml +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +866 -0
  4. package/build.js +189 -0
  5. package/css/README.md +1 -0
  6. package/css/myetv-player.css +13702 -0
  7. package/css/myetv-player.min.css +1 -0
  8. package/dist/README.md +1 -0
  9. package/dist/myetv-player.js +6408 -0
  10. package/dist/myetv-player.min.js +6183 -0
  11. package/package.json +27 -0
  12. package/plugins/README.md +1 -0
  13. package/plugins/google-analytics/README.md +1 -0
  14. package/plugins/google-analytics/myetv-player-g-analytics-plugin.js +548 -0
  15. package/plugins/youtube/README.md +1 -0
  16. package/plugins/youtube/myetv-player-youtube-plugin.js +418 -0
  17. package/scss/README.md +1 -0
  18. package/scss/_audio-player.scss +21 -0
  19. package/scss/_base.scss +131 -0
  20. package/scss/_controls.scss +30 -0
  21. package/scss/_loading.scss +111 -0
  22. package/scss/_menus.scss +4070 -0
  23. package/scss/_mixins.scss +112 -0
  24. package/scss/_poster.scss +8 -0
  25. package/scss/_progress-bar.scss +2203 -0
  26. package/scss/_resolution.scss +68 -0
  27. package/scss/_responsive.scss +1532 -0
  28. package/scss/_themes.scss +30 -0
  29. package/scss/_title-overlay.scss +2262 -0
  30. package/scss/_tooltips.scss +7 -0
  31. package/scss/_variables.scss +49 -0
  32. package/scss/_video.scss +2401 -0
  33. package/scss/_volume.scss +1981 -0
  34. package/scss/_watermark.scss +8 -0
  35. package/scss/myetv-player.scss +51 -0
  36. package/scss/package.json +16 -0
  37. package/src/README.md +1 -0
  38. package/src/chapters.js +521 -0
  39. package/src/controls.js +1005 -0
  40. package/src/core.js +1650 -0
  41. package/src/events.js +330 -0
  42. package/src/fullscreen.js +82 -0
  43. package/src/i18n.js +348 -0
  44. package/src/playlist.js +177 -0
  45. package/src/plugins.js +384 -0
  46. package/src/quality.js +921 -0
  47. package/src/streaming.js +346 -0
  48. package/src/subtitles.js +426 -0
  49. package/src/utils.js +51 -0
  50. package/src/watermark.js +195 -0
@@ -0,0 +1,1005 @@
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 - IMPROVED AND COMPREHENSIVE */
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
+ this.onMouseLeaveControls(e);
64
+ });
65
+
66
+ if (this.options.debug) console.log('Event listener mouseenter/mouseleave added to controls');
67
+
68
+ this.autoHideInitialized = true;
69
+ if (this.options.debug) console.log('Auto-hide system fully initialized');
70
+
71
+ // Test
72
+ this.resetAutoHideTimer();
73
+ if (this.options.debug) console.log('Initial timer started');
74
+ }
75
+
76
+ onMouseMoveInPlayer(e) {
77
+ this.showControlsNow();
78
+ this.resetAutoHideTimer();
79
+ }
80
+
81
+ onMouseEnterControls(e) {
82
+ this.mouseOverControls = true;
83
+ this.showControlsNow();
84
+
85
+ if (this.autoHideTimer) {
86
+ clearTimeout(this.autoHideTimer);
87
+ this.autoHideTimer = null;
88
+ if (this.autoHideDebug) {
89
+ if (this.options.debug) console.log('Auto-hide timer cancelled');
90
+ }
91
+ }
92
+ }
93
+
94
+ onMouseLeaveControls(e) {
95
+ this.mouseOverControls = false;
96
+ this.resetAutoHideTimer();
97
+ }
98
+
99
+ resetAutoHideTimer() {
100
+ if (this.autoHideTimer) {
101
+ clearTimeout(this.autoHideTimer);
102
+ this.autoHideTimer = null;
103
+ }
104
+
105
+ if (this.mouseOverControls) {
106
+ if (this.autoHideDebug) {
107
+ if (this.options.debug) console.log('Not starting timer - mouse on controls');
108
+ }
109
+ return;
110
+ }
111
+
112
+ if (this.video && this.video.paused) {
113
+ if (this.autoHideDebug) {
114
+ if (this.options.debug) console.log('Not starting timer - video paused');
115
+ }
116
+ return;
117
+ }
118
+
119
+ this.autoHideTimer = setTimeout(() => {
120
+ if (this.autoHideDebug) {
121
+ if (this.options.debug) console.log(`Timer expired after ${this.options.autoHideDelay}ms - nascondo controlli`);
122
+ }
123
+ this.hideControlsNow();
124
+ }, this.options.autoHideDelay);
125
+
126
+ if (this.autoHideDebug) {
127
+ if (this.options.debug) console.log(`Auto-hide timer started: ${this.options.autoHideDelay}ms`);
128
+ }
129
+ }
130
+
131
+ showControlsNow() {
132
+ if (this.controls) {
133
+ this.controls.classList.add('show');
134
+ }
135
+
136
+ // ADD THIS: Add has-controls class to container for watermark visibility
137
+ if (this.container) {
138
+ this.container.classList.add('has-controls');
139
+ }
140
+
141
+ // Fix: Show title overlay with controls if not persistent
142
+ if (this.options.showTitleOverlay && !this.options.persistentTitle && this.options.videoTitle) {
143
+ this.showTitleOverlay();
144
+ }
145
+
146
+ if (this.autoHideDebug && this.options.debug) console.log('✅ Controls shown');
147
+ }
148
+
149
+ hideControlsNow() {
150
+ // Don't hide if mouse is still over controls
151
+ if (this.mouseOverControls) {
152
+ if (this.autoHideDebug && this.options.debug) console.log('⏸️ Not hiding - mouse still over controls');
153
+ return;
154
+ }
155
+
156
+ // Don't hide if video is paused
157
+ if (this.video && this.video.paused) {
158
+ if (this.autoHideDebug && this.options.debug) console.log('⏸️ Not hiding - video is paused');
159
+ return;
160
+ }
161
+
162
+ if (this.controls) {
163
+ this.controls.classList.remove('show');
164
+ }
165
+
166
+ // ADD THIS: Remove has-controls class from container for watermark visibility
167
+ if (this.container) {
168
+ this.container.classList.remove('has-controls');
169
+ }
170
+
171
+ // Fix: Hide title overlay with controls if not persistent
172
+ if (this.options.showTitleOverlay && !this.options.persistentTitle) {
173
+ this.hideTitleOverlay();
174
+ }
175
+
176
+ if (this.autoHideDebug && this.options.debug) console.log('❌ Controls hidden');
177
+ }
178
+
179
+ showControls() {
180
+ this.showControlsNow();
181
+ this.resetAutoHideTimer();
182
+ }
183
+
184
+ hideControls() {
185
+ this.hideControlsNow();
186
+ }
187
+
188
+ hideControlsWithDelay() {
189
+ this.resetAutoHideTimer();
190
+ }
191
+
192
+ clearControlsTimeout() {
193
+ if (this.autoHideTimer) {
194
+ clearTimeout(this.autoHideTimer);
195
+ this.autoHideTimer = null;
196
+ }
197
+ }
198
+
199
+ // Debug methods
200
+ enableAutoHideDebug() {
201
+ this.autoHideDebug = true;
202
+ if (this.options.debug) console.log('AUTO-HIDE DEBUG ENABLED');
203
+ if (this.options.debug) console.log('Stato attuale:', {
204
+ initialized: this.autoHideInitialized,
205
+ autoHide: this.options.autoHide,
206
+ delay: this.options.autoHideDelay,
207
+ mouseOverControls: this.mouseOverControls,
208
+ timerActive: !!this.autoHideTimer,
209
+ container: !!this.container,
210
+ controls: !!this.controls,
211
+ video: !!this.video,
212
+ videoPaused: this.video ? this.video.paused : 'N/A'
213
+ });
214
+
215
+ if (!this.autoHideInitialized) {
216
+ if (this.options.debug) console.log('Auto-hide NOT yet initialized! Initializing now...');
217
+ this.initAutoHide();
218
+ }
219
+ }
220
+
221
+ disableAutoHideDebug() {
222
+ this.autoHideDebug = false;
223
+ if (this.options.debug) console.log('Auto-hide debug disabled');
224
+ }
225
+
226
+ testAutoHide() {
227
+ if (this.options.debug) console.log('TEST AUTO-HIDE COMPLETED:');
228
+ if (this.options.debug) console.log('System status:', {
229
+ initialized: this.autoHideInitialized,
230
+ autoHide: this.options.autoHide,
231
+ delay: this.options.autoHideDelay,
232
+ mouseOverControls: this.mouseOverControls,
233
+ timerActive: !!this.autoHideTimer
234
+ });
235
+
236
+ if (this.options.debug) console.log('Elementi DOM:', {
237
+ container: !!this.container,
238
+ controls: !!this.controls,
239
+ video: !!this.video
240
+ });
241
+
242
+ if (this.options.debug) console.log('Stato video:', {
243
+ paused: this.video ? this.video.paused : 'N/A',
244
+ currentTime: this.video ? this.video.currentTime : 'N/A',
245
+ duration: this.video ? this.video.duration : 'N/A'
246
+ });
247
+
248
+ if (!this.autoHideInitialized) {
249
+ if (this.options.debug) console.log('PROBLEM: Auto-hide not initialized!');
250
+ if (this.options.debug) console.log('Forcing initialization...');
251
+ this.initAutoHide();
252
+ } else {
253
+ if (this.options.debug) console.log('Auto-hide initialized correctly');
254
+ if (this.options.debug) console.log('Forcing timer reset for test...');
255
+ this.resetAutoHideTimer();
256
+ }
257
+ }
258
+
259
+ /* SUBTITLES UI MANAGEMENT */
260
+ updateSubtitlesUI() {
261
+ const subtitlesControl = this.controls?.querySelector('.subtitles-control');
262
+
263
+ if (this.textTracks.length > 0 && this.options.showSubtitles) {
264
+ if (subtitlesControl) {
265
+ subtitlesControl.style.display = 'block';
266
+ }
267
+ this.populateSubtitlesMenu();
268
+ } else {
269
+ if (subtitlesControl) {
270
+ subtitlesControl.style.display = 'none';
271
+ }
272
+ }
273
+ }
274
+
275
+ populateSubtitlesMenu() {
276
+ const subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
277
+ if (!subtitlesMenu) return;
278
+
279
+ let menuHTML = `<div class="subtitles-option ${!this.subtitlesEnabled ? 'active' : ''}" data-track="off">${this.t('subtitlesoff') || 'Off'}</div>`;
280
+
281
+ this.textTracks.forEach((trackData, index) => {
282
+ const isActive = this.currentSubtitleTrack === trackData.track;
283
+ menuHTML += `<div class="subtitles-option ${isActive ? 'active' : ''}" data-track="${index}">${trackData.label}</div>`;
284
+ });
285
+
286
+ subtitlesMenu.innerHTML = menuHTML;
287
+ }
288
+
289
+ toggleSubtitles() {
290
+ if (this.textTracks.length === 0) return;
291
+
292
+ if (this.subtitlesEnabled) {
293
+ this.disableSubtitles();
294
+ } else {
295
+ this.enableSubtitleTrack(0);
296
+ }
297
+ }
298
+
299
+ updateSubtitlesButton() {
300
+ const subtitlesBtn = this.controls?.querySelector('.subtitles-btn');
301
+ if (!subtitlesBtn) return;
302
+
303
+ if (this.subtitlesEnabled) {
304
+ subtitlesBtn.classList.add('active');
305
+ subtitlesBtn.title = this.t('subtitlesdisable') || 'Disable subtitles';
306
+ } else {
307
+ subtitlesBtn.classList.remove('active');
308
+ subtitlesBtn.title = this.t('subtitlesenable') || 'Enable subtitles';
309
+ }
310
+ }
311
+
312
+ handleSubtitlesMenuClick(e) {
313
+ if (!e.target.classList.contains('subtitles-option')) return;
314
+
315
+ const trackData = e.target.getAttribute('data-track');
316
+
317
+ if (trackData === 'off') {
318
+ this.disableSubtitles();
319
+ } else {
320
+ const trackIndex = parseInt(trackData);
321
+ this.enableSubtitleTrack(trackIndex);
322
+ }
323
+ }
324
+
325
+ /* PLAYER CONTROLS SETUP */
326
+ hideNativePlayer() {
327
+ this.video.controls = false;
328
+ this.video.setAttribute('controls', 'false');
329
+ this.video.removeAttribute('controls');
330
+ this.video.style.visibility = 'hidden';
331
+ this.video.style.opacity = '0';
332
+ this.video.style.pointerEvents = 'none';
333
+ this.video.classList.add('video-player');
334
+ }
335
+
336
+ createControls() {
337
+ const controlsId = `videoControls-${this.getUniqueId()}`;
338
+
339
+ const controlsHTML = `
340
+ <div class="controls" id="${controlsId}">
341
+ <div class="progress-container">
342
+ <div class="progress-bar">
343
+ <div class="progress-buffer"></div>
344
+ <div class="progress-filled"></div>
345
+ <div class="progress-handle"></div>
346
+ </div>
347
+ ${this.options.showSeekTooltip ? '<div class="seek-tooltip">0:00</div>' : ''}
348
+ </div>
349
+
350
+ <div class="controls-main">
351
+ <div class="controls-left">
352
+ <button class="control-btn play-pause-btn" data-tooltip="play_pause">
353
+ <span class="icon play-icon">▶</span>
354
+ <span class="icon pause-icon hidden">⏸</span>
355
+ </button>
356
+
357
+ <button class="control-btn mute-btn" data-tooltip="mute_unmute">
358
+ <span class="icon volume-icon">🔊</span>
359
+ <span class="icon mute-icon hidden">🔇</span>
360
+ </button>
361
+
362
+ <div class="volume-container" data-orientation="${this.options.volumeSlider}">
363
+ <input type="range" class="volume-slider" min="0" max="100" value="100" data-tooltip="volume">
364
+ </div>
365
+
366
+ <div class="time-display">
367
+ <span class="current-time">0:00</span>
368
+ <span>/</span>
369
+ <span class="duration">0:00</span>
370
+ </div>
371
+ </div>
372
+
373
+ <div class="controls-right">
374
+ <button class="control-btn playlist-prev-btn" data-tooltip="prevvideo" style="display: none;">
375
+ <span class="icon">⏮</span>
376
+ </button>
377
+
378
+ <button class="control-btn playlist-next-btn" data-tooltip="nextvideo" style="display: none;">
379
+ <span class="icon">⏭</span>
380
+ </button>
381
+
382
+ ${this.options.showSubtitles ? `
383
+ <div class="subtitles-control" style="display: none;">
384
+ <button class="control-btn subtitles-btn" data-tooltip="subtitles">
385
+ <span class="icon">CC</span>
386
+ </button>
387
+ <div class="subtitles-menu">
388
+ <div class="subtitles-option active" data-track="off">Off</div>
389
+ </div>
390
+ </div>
391
+ ` : ''}
392
+
393
+ ${this.options.showSpeedControl ? `
394
+ <div class="speed-control">
395
+ <button class="control-btn speed-btn" data-tooltip="playback_speed">1x</button>
396
+ <div class="speed-menu">
397
+ <div class="speed-option" data-speed="0.5">0.5x</div>
398
+ <div class="speed-option" data-speed="0.75">0.75x</div>
399
+ <div class="speed-option active" data-speed="1">1x</div>
400
+ <div class="speed-option" data-speed="1.25">1.25x</div>
401
+ <div class="speed-option" data-speed="1.5">1.5x</div>
402
+ <div class="speed-option" data-speed="2">2x</div>
403
+ </div>
404
+ </div>
405
+ ` : ''}
406
+
407
+ ${this.options.showQualitySelector && this.originalSources && this.originalSources.length > 1 ? `
408
+ <div class="quality-control">
409
+ <button class="control-btn quality-btn" data-tooltip="video_quality">
410
+ <div class="quality-btn-text">
411
+ <div class="selected-quality">${this.t('auto')}</div>
412
+ <div class="current-quality"></div>
413
+ </div>
414
+ </button>
415
+ <div class="quality-menu">
416
+ <div class="quality-option selected" data-quality="auto">${this.t('auto')}</div>
417
+ ${this.originalSources.map(s =>
418
+ `<div class="quality-option" data-quality="${s.quality}">${s.quality}</div>`
419
+ ).join('')}
420
+ </div>
421
+ </div>
422
+ ` : ''}
423
+
424
+ ${this.options.showPictureInPicture && this.isPiPSupported ? `
425
+ <button class="control-btn pip-btn" data-tooltip="picture_in_picture">
426
+ <span class="icon pip-icon">⧉</span>
427
+ <span class="icon pip-exit-icon hidden">⧉</span>
428
+ </button>
429
+ ` : ''}
430
+
431
+ <div class="settings-control">
432
+ <button class="control-btn settings-btn" data-tooltip="settings_menu">
433
+ <span class="">⚙</span>
434
+ </button>
435
+ <div class="settings-menu"></div>
436
+ </div>
437
+
438
+ ${this.options.showFullscreen ? `
439
+ <button class="control-btn fullscreen-btn" data-tooltip="fullscreen">
440
+ <span class="icon fullscreen-icon">⛶</span>
441
+ <span class="icon exit-fullscreen-icon hidden">⛉</span>
442
+ </button>
443
+ ` : ''}
444
+ </div>
445
+ </div>
446
+ </div>
447
+ `;
448
+
449
+ this.container.insertAdjacentHTML('beforeend', controlsHTML);
450
+ this.controls = document.getElementById(controlsId);
451
+
452
+ // NEW: Initialize responsive settings menu
453
+ setTimeout(() => {
454
+ this.initializeResponsiveMenu();
455
+ }, 100);
456
+ }
457
+
458
+ /* NEW: Initialize responsive menu with dynamic width calculation */
459
+ initializeResponsiveMenu() {
460
+ if (!this.controls) return;
461
+
462
+ // Track screen size
463
+ this.isSmallScreen = false;
464
+
465
+ // Check initial size and set up listener
466
+ this.checkScreenSize();
467
+
468
+ window.addEventListener('resize', () => {
469
+ this.checkScreenSize();
470
+ });
471
+
472
+ // Bind events for settings menu
473
+ this.bindSettingsMenuEvents();
474
+ }
475
+
476
+ /* NEW: Dynamic width calculation based on logo presence */
477
+ getResponsiveThreshold() {
478
+ // Check if brand logo is enabled and present
479
+ const hasLogo = this.options.brandLogoEnabled && this.options.brandLogoUrl;
480
+
481
+ // If logo is present, use higher threshold (650px), otherwise 550px
482
+ return hasLogo ? 650 : 550;
483
+ }
484
+
485
+ /* NEW: Check if screen is under dynamic threshold */
486
+ checkScreenSize() {
487
+ const threshold = this.getResponsiveThreshold();
488
+ const newIsSmallScreen = window.innerWidth <= threshold;
489
+
490
+ if (newIsSmallScreen !== this.isSmallScreen) {
491
+ this.isSmallScreen = newIsSmallScreen;
492
+ this.updateSettingsMenuVisibility();
493
+
494
+ if (this.options.debug) {
495
+ console.log(`Screen check: ${window.innerWidth}px vs ${threshold}px threshold (logo: ${this.options.brandLogoEnabled}), small: ${this.isSmallScreen}`);
496
+ }
497
+ }
498
+ }
499
+
500
+ /* NEW: Update settings menu visibility based on screen size */
501
+ updateSettingsMenuVisibility() {
502
+ const settingsControl = this.controls?.querySelector('.settings-control');
503
+ if (!settingsControl) return;
504
+
505
+ if (this.isSmallScreen) {
506
+ // Show settings menu and hide individual controls
507
+ settingsControl.style.display = 'block';
508
+
509
+ // Hide controls that will be moved to settings menu
510
+ const pipBtn = this.controls.querySelector('.pip-btn');
511
+ const speedControl = this.controls.querySelector('.speed-control');
512
+ const subtitlesControl = this.controls.querySelector('.subtitles-control');
513
+
514
+ if (pipBtn) pipBtn.style.display = 'none';
515
+ if (speedControl) speedControl.style.display = 'none';
516
+ if (subtitlesControl) subtitlesControl.style.display = 'none';
517
+
518
+ this.populateSettingsMenu();
519
+ } else {
520
+ // Hide settings menu and show individual controls
521
+ settingsControl.style.display = 'none';
522
+
523
+ // Show original controls
524
+ const pipBtn = this.controls.querySelector('.pip-btn');
525
+ const speedControl = this.controls.querySelector('.speed-control');
526
+ const subtitlesControl = this.controls.querySelector('.subtitles-control');
527
+
528
+ if (pipBtn && this.options.showPictureInPicture && this.isPiPSupported) {
529
+ pipBtn.style.display = 'flex';
530
+ }
531
+ if (speedControl && this.options.showSpeedControl) {
532
+ speedControl.style.display = 'block';
533
+ }
534
+ if (subtitlesControl && this.options.showSubtitles && this.textTracks.length > 0) {
535
+ subtitlesControl.style.display = 'block';
536
+ }
537
+ }
538
+ }
539
+
540
+ /* NEW: Populate settings menu with controls */
541
+ populateSettingsMenu() {
542
+ const settingsMenu = this.controls?.querySelector('.settings-menu');
543
+ if (!settingsMenu) return;
544
+
545
+ let menuHTML = '';
546
+
547
+ // Picture-in-Picture option
548
+ if (this.options.showPictureInPicture && this.isPiPSupported) {
549
+ const pipLabel = this.t('picture_in_picture') || 'Picture-in-Picture';
550
+ menuHTML += `<div class="settings-option" data-action="pip">
551
+ <span class="settings-option-label">${pipLabel}</span>
552
+ </div>`;
553
+ }
554
+
555
+ // Speed Control submenu
556
+ if (this.options.showSpeedControl) {
557
+ const speedLabel = this.t('playback_speed') || 'Playback Speed';
558
+ const currentSpeed = this.video ? this.video.playbackRate : 1;
559
+ menuHTML += `<div class="settings-option" data-action="speed">
560
+ <span class="settings-option-label">${speedLabel}</span>
561
+ <span class="settings-option-value">${currentSpeed}x</span>
562
+ <div class="settings-submenu speed-submenu">
563
+ <div class="settings-suboption ${currentSpeed === 0.5 ? 'active' : ''}" data-speed="0.5">0.5x</div>
564
+ <div class="settings-suboption ${currentSpeed === 0.75 ? 'active' : ''}" data-speed="0.75">0.75x</div>
565
+ <div class="settings-suboption ${currentSpeed === 1 ? 'active' : ''}" data-speed="1">1x</div>
566
+ <div class="settings-suboption ${currentSpeed === 1.25 ? 'active' : ''}" data-speed="1.25">1.25x</div>
567
+ <div class="settings-suboption ${currentSpeed === 1.5 ? 'active' : ''}" data-speed="1.5">1.5x</div>
568
+ <div class="settings-suboption ${currentSpeed === 2 ? 'active' : ''}" data-speed="2">2x</div>
569
+ </div>
570
+ </div>`;
571
+ }
572
+
573
+ // Subtitles submenu
574
+ if (this.options.showSubtitles && this.textTracks && this.textTracks.length > 0) {
575
+ const subtitlesLabel = this.t('subtitles') || 'Subtitles';
576
+ const currentTrack = this.currentSubtitleTrack;
577
+ const currentLabel = this.subtitlesEnabled && currentTrack
578
+ ? (currentTrack.label || 'Track')
579
+ : (this.t('subtitlesoff') || 'Off');
580
+
581
+ menuHTML += `<div class="settings-option" data-action="subtitles">
582
+ <span class="settings-option-label">${subtitlesLabel}</span>
583
+ <span class="settings-option-value">${currentLabel}</span>
584
+ <div class="settings-submenu subtitles-submenu">
585
+ <div class="settings-suboption ${!this.subtitlesEnabled ? 'active' : ''}" data-track="off">
586
+ ${this.t('subtitlesoff') || 'Off'}
587
+ </div>`;
588
+
589
+ this.textTracks.forEach((trackData, index) => {
590
+ const isActive = this.currentSubtitleTrack === trackData.track;
591
+ menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-track="${index}">
592
+ ${trackData.label}
593
+ </div>`;
594
+ });
595
+
596
+ menuHTML += '</div></div>';
597
+ }
598
+
599
+ settingsMenu.innerHTML = menuHTML;
600
+ }
601
+
602
+ /* NEW: Bind settings menu events */
603
+ bindSettingsMenuEvents() {
604
+ const settingsMenu = this.controls?.querySelector('.settings-menu');
605
+ if (!settingsMenu) return;
606
+
607
+ settingsMenu.addEventListener('click', (e) => {
608
+ e.stopPropagation();
609
+
610
+ // Handle direct actions
611
+ if (e.target.classList.contains('settings-option') || e.target.closest('.settings-option')) {
612
+ const option = e.target.classList.contains('settings-option') ? e.target : e.target.closest('.settings-option');
613
+ const action = option.getAttribute('data-action');
614
+
615
+ if (action === 'pip') {
616
+ this.togglePictureInPicture();
617
+ return;
618
+ }
619
+ }
620
+
621
+ // Handle submenu actions
622
+ if (e.target.classList.contains('settings-suboption')) {
623
+ const parent = e.target.closest('.settings-option');
624
+ const action = parent?.getAttribute('data-action');
625
+
626
+ if (action === 'speed') {
627
+ const speed = parseFloat(e.target.getAttribute('data-speed'));
628
+ if (speed && speed > 0 && this.video && !this.isChangingQuality) {
629
+ this.video.playbackRate = speed;
630
+
631
+ // Update active states
632
+ parent.querySelectorAll('.settings-suboption').forEach(opt => {
633
+ opt.classList.remove('active');
634
+ });
635
+ e.target.classList.add('active');
636
+
637
+ // Update value display
638
+ const valueEl = parent.querySelector('.settings-option-value');
639
+ if (valueEl) valueEl.textContent = speed + 'x';
640
+
641
+ // Trigger event
642
+ this.triggerEvent('speedchange', { speed, previousSpeed: this.video.playbackRate });
643
+ }
644
+ }
645
+
646
+ else if (action === 'subtitles') {
647
+ const trackData = e.target.getAttribute('data-track');
648
+
649
+ if (trackData === 'off') {
650
+ this.disableSubtitles();
651
+ } else {
652
+ const trackIndex = parseInt(trackData);
653
+ this.enableSubtitleTrack(trackIndex);
654
+ }
655
+
656
+ // Update active states
657
+ parent.querySelectorAll('.settings-suboption').forEach(opt => {
658
+ opt.classList.remove('active');
659
+ });
660
+ e.target.classList.add('active');
661
+
662
+ // Update value display
663
+ const valueEl = parent.querySelector('.settings-option-value');
664
+ if (valueEl) valueEl.textContent = e.target.textContent;
665
+ }
666
+ }
667
+ });
668
+ }
669
+
670
+ /* TITLE OVERLAY MANAGEMENT */
671
+ showTitleOverlay() {
672
+ if (this.titleOverlay && this.options.videoTitle) {
673
+ this.titleOverlay.classList.add('show');
674
+
675
+ if (this.options.persistentTitle) {
676
+ this.titleOverlay.classList.add('persistent');
677
+ } else {
678
+ this.titleOverlay.classList.remove('persistent');
679
+ }
680
+ }
681
+ return this;
682
+ }
683
+
684
+ hideTitleOverlay() {
685
+ if (this.titleOverlay) {
686
+ this.titleOverlay.classList.remove('show');
687
+ this.titleOverlay.classList.remove('persistent');
688
+ }
689
+ return this;
690
+ }
691
+
692
+ toggleTitleOverlay(show = null) {
693
+ if (show === null) {
694
+ return this.titleOverlay && this.titleOverlay.classList.contains('show')
695
+ ? this.hideTitleOverlay()
696
+ : this.showTitleOverlay();
697
+ }
698
+
699
+ return show ? this.showTitleOverlay() : this.hideTitleOverlay();
700
+ }
701
+
702
+ /* KEYBOARD CONTROLS */
703
+ setupKeyboardControls() {
704
+ document.addEventListener('keydown', (e) => {
705
+ // Ignore if user is typing in an input field
706
+ if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
707
+
708
+ // On keyboard input, treat as mouse movement for auto-hide
709
+ if (this.options.autoHide && this.autoHideInitialized) {
710
+ this.showControlsNow();
711
+ this.resetAutoHideTimer();
712
+ }
713
+
714
+ switch (e.code) {
715
+ case 'Space':
716
+ e.preventDefault();
717
+ this.togglePlayPause();
718
+ break;
719
+ case 'KeyM':
720
+ this.toggleMute();
721
+ break;
722
+ case 'KeyF':
723
+ if (this.options.showFullscreen) {
724
+ this.toggleFullscreen();
725
+ }
726
+ break;
727
+ case 'KeyP':
728
+ if (this.options.showPictureInPicture && this.isPiPSupported) {
729
+ this.togglePictureInPicture();
730
+ }
731
+ break;
732
+ case 'KeyT':
733
+ if (this.options.showTitleOverlay) {
734
+ this.toggleTitleOverlay();
735
+ }
736
+ break;
737
+ case 'KeyS':
738
+ if (this.options.showSubtitles) {
739
+ this.toggleSubtitles();
740
+ }
741
+ break;
742
+ case 'KeyD':
743
+ this.debugQuality ? this.disableQualityDebug() : this.enableQualityDebug();
744
+ break;
745
+ case 'ArrowLeft':
746
+ e.preventDefault();
747
+ this.skipTime(-10);
748
+ break;
749
+ case 'ArrowRight':
750
+ e.preventDefault();
751
+ this.skipTime(10);
752
+ break;
753
+ case 'ArrowUp':
754
+ e.preventDefault();
755
+ this.changeVolume(0.1);
756
+ break;
757
+ case 'ArrowDown':
758
+ e.preventDefault();
759
+ this.changeVolume(-0.1);
760
+ break;
761
+ }
762
+ });
763
+ }
764
+
765
+ /* CONTROL ACTIONS */
766
+ togglePlayPause() {
767
+ if (!this.video || this.isChangingQuality) return;
768
+
769
+ if (this.video.paused) {
770
+ this.play();
771
+ } else {
772
+ this.pause();
773
+ }
774
+ }
775
+
776
+ toggleMute() {
777
+ if (!this.video) return;
778
+
779
+ const wasMuted = this.video.muted;
780
+ this.video.muted = !this.video.muted;
781
+
782
+ this.updateMuteButton();
783
+ this.updateVolumeSliderVisual();
784
+ this.initVolumeTooltip();
785
+
786
+ // Triggers volumechange event
787
+ this.triggerEvent('volumechange', {
788
+ volume: this.getVolume(),
789
+ muted: this.isMuted(),
790
+ previousMuted: wasMuted
791
+ });
792
+ }
793
+
794
+ updateMuteButton() {
795
+ if (!this.video || !this.volumeIcon || !this.muteIcon) return;
796
+
797
+ if (this.video.muted || this.video.volume === 0) {
798
+ this.volumeIcon.classList.add('hidden');
799
+ this.muteIcon.classList.remove('hidden');
800
+ } else {
801
+ this.volumeIcon.classList.remove('hidden');
802
+ this.muteIcon.classList.add('hidden');
803
+ }
804
+ }
805
+
806
+ /* LOADING STATES */
807
+ showLoading() {
808
+ if (this.loadingOverlay) {
809
+ this.loadingOverlay.classList.add('active');
810
+ }
811
+ }
812
+
813
+ hideLoading() {
814
+ if (this.loadingOverlay) {
815
+ this.loadingOverlay.classList.remove('active');
816
+ }
817
+ }
818
+
819
+ /* FULLSCREEN CONTROLS */
820
+ toggleFullscreen() {
821
+ if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement) {
822
+ this.exitFullscreen();
823
+ } else {
824
+ this.enterFullscreen();
825
+ }
826
+ }
827
+
828
+ updateFullscreenButton() {
829
+ if (!this.fullscreenIcon || !this.exitFullscreenIcon) return;
830
+
831
+ const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
832
+
833
+ if (isFullscreen) {
834
+ this.fullscreenIcon.classList.add('hidden');
835
+ this.exitFullscreenIcon.classList.remove('hidden');
836
+ } else {
837
+ this.fullscreenIcon.classList.remove('hidden');
838
+ this.exitFullscreenIcon.classList.add('hidden');
839
+ }
840
+
841
+ // Triggers fullscreenchange event
842
+ this.triggerEvent('fullscreenchange', {
843
+ active: !!isFullscreen,
844
+ mode: isFullscreen ? 'enter' : 'exit'
845
+ });
846
+ }
847
+
848
+ /* PICTURE IN PICTURE CONTROLS */
849
+ togglePictureInPicture() {
850
+ if (!this.isPiPSupported || !this.video) return;
851
+
852
+ if (document.pictureInPictureElement) {
853
+ this.exitPictureInPicture();
854
+ } else {
855
+ this.enterPictureInPicture();
856
+ }
857
+ }
858
+
859
+ /* SEEK TOOLTIP MANAGEMENT */
860
+ toggleSeekTooltip(show = null) {
861
+ if (show === null) {
862
+ this.options.showSeekTooltip = !this.options.showSeekTooltip;
863
+ } else {
864
+ this.options.showSeekTooltip = show;
865
+ }
866
+
867
+ if (this.seekTooltip) {
868
+ if (this.options.showSeekTooltip) {
869
+ this.setupSeekTooltip();
870
+ } else {
871
+ this.seekTooltip.classList.remove('visible');
872
+ }
873
+ }
874
+ }
875
+
876
+ /* AUTO-HIDE CONFIGURATION */
877
+ setAutoHideDelay(delay) {
878
+ if (typeof delay === 'number' && delay >= 0) {
879
+ this.options.autoHideDelay = delay;
880
+ if (this.options.debug) console.log(`Auto-hide delay set to ${delay}ms`);
881
+ }
882
+ return this;
883
+ }
884
+
885
+ getAutoHideDelay() {
886
+ return this.options.autoHideDelay;
887
+ }
888
+
889
+ enableAutoHide() {
890
+ if (!this.options.autoHide) {
891
+ this.options.autoHide = true;
892
+ if (!this.autoHideInitialized) {
893
+ this.initAutoHide();
894
+ }
895
+ if (this.options.debug) console.log('Auto-hide enabled');
896
+ }
897
+ return this;
898
+ }
899
+
900
+ disableAutoHide() {
901
+ if (this.options.autoHide) {
902
+ this.options.autoHide = false;
903
+ if (this.autoHideTimer) {
904
+ clearTimeout(this.autoHideTimer);
905
+ this.autoHideTimer = null;
906
+ }
907
+ this.showControlsNow();
908
+ if (this.options.debug) console.log('Auto-hide disabled');
909
+ }
910
+ return this;
911
+ }
912
+
913
+ forceShowControls() {
914
+ this.showControlsNow();
915
+ if (this.autoHideInitialized) {
916
+ this.resetAutoHideTimer();
917
+ }
918
+ return this;
919
+ }
920
+
921
+ forceHideControls() {
922
+ if (!this.mouseOverControls && this.video && !this.video.paused) {
923
+ this.hideControlsNow();
924
+ }
925
+ return this;
926
+ }
927
+
928
+ isAutoHideEnabled() {
929
+ return this.options.autoHide;
930
+ }
931
+
932
+ isAutoHideInitialized() {
933
+ return this.autoHideInitialized;
934
+ }
935
+
936
+ /* PLAYLIST CONTROLS */
937
+ showPlaylistControls() {
938
+ if (!this.playlistPrevBtn || !this.playlistNextBtn) return;
939
+
940
+ this.playlistPrevBtn.style.display = 'flex';
941
+ this.playlistNextBtn.style.display = 'flex';
942
+ this.updatePlaylistButtons();
943
+
944
+ if (this.options.debug) console.log('Playlist controls shown');
945
+ }
946
+
947
+ hidePlaylistControls() {
948
+ if (!this.playlistPrevBtn || !this.playlistNextBtn) return;
949
+
950
+ this.playlistPrevBtn.style.display = 'none';
951
+ this.playlistNextBtn.style.display = 'none';
952
+
953
+ if (this.options.debug) console.log('Playlist controls hidden');
954
+ }
955
+
956
+ updatePlaylistButtons() {
957
+ if (!this.playlistPrevBtn || !this.playlistNextBtn || !this.isPlaylistActive) return;
958
+
959
+ const canGoPrev = this.currentPlaylistIndex > 0 || this.options.playlistLoop;
960
+ const canGoNext = this.currentPlaylistIndex < this.playlist.length - 1 || this.options.playlistLoop;
961
+
962
+ this.playlistPrevBtn.disabled = !canGoPrev;
963
+ this.playlistNextBtn.disabled = !canGoNext;
964
+
965
+ // Update visual state
966
+ if (canGoPrev) {
967
+ this.playlistPrevBtn.style.opacity = '1';
968
+ this.playlistPrevBtn.style.cursor = 'pointer';
969
+ } else {
970
+ this.playlistPrevBtn.style.opacity = '0.4';
971
+ this.playlistPrevBtn.style.cursor = 'not-allowed';
972
+ }
973
+
974
+ if (canGoNext) {
975
+ this.playlistNextBtn.style.opacity = '1';
976
+ this.playlistNextBtn.style.cursor = 'pointer';
977
+ } else {
978
+ this.playlistNextBtn.style.opacity = '0.4';
979
+ this.playlistNextBtn.style.cursor = 'not-allowed';
980
+ }
981
+ }
982
+
983
+ /* RESPONSIVE OPTIMIZATION */
984
+ optimizeButtonsForSmallHeight() {
985
+ const currentHeight = window.innerHeight;
986
+ const controlsRect = this.controls.getBoundingClientRect();
987
+
988
+ // If controlbar is taller than 40% of viewport, optimize
989
+ if (controlsRect.height > currentHeight * 0.4) {
990
+ this.controls.classList.add('ultra-compact');
991
+ if (this.options.debug) console.log('Applied ultra-compact mode for height:', currentHeight);
992
+ } else {
993
+ this.controls.classList.remove('ultra-compact');
994
+ }
995
+
996
+ // Hide non-essential buttons on very small heights
997
+ const nonEssentialButtons = this.controls.querySelectorAll('.pip-btn, .speed-control');
998
+ if (currentHeight < 180) {
999
+ nonEssentialButtons.forEach(btn => btn.style.display = 'none');
1000
+ } else {
1001
+ nonEssentialButtons.forEach(btn => btn.style.display = '');
1002
+ }
1003
+ }
1004
+
1005
+ /* Controls methods for main class - All original functionality preserved exactly */