myetv-player 1.2.0 → 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 +131 -0
  2. package/css/myetv-player.min.css +1 -1
  3. package/dist/myetv-player.js +547 -102
  4. package/dist/myetv-player.min.js +486 -93
  5. package/package.json +35 -17
  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 +766 -6
  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 -204
  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 -1368
  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 -1242
  37. package/src/core.js +0 -1922
  38. package/src/events.js +0 -537
  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,1242 +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>
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"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></span>
433
- <span class="icon pause-icon hidden"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M6 4h4v16H6zm8 0h4v16h-4z"/></svg></span>
434
- </button>
435
-
436
- <button class="control-btn mute-btn" data-tooltip="mute_unmute">
437
- <span class="icon volume-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303z"/><path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89z"/><path d="M10.025 8a4.486 4.486 0 0 1-1.318 3.182L8 10.475A3.489 3.489 0 0 0 9.025 8c0-.966-.392-1.841-1.025-2.475l.707-.707A4.486 4.486 0 0 1 10.025 8M7 4a.5.5 0 0 0-.812-.39L3.825 5.5H1.5A.5.5 0 0 0 1 6v4a.5.5 0 0 0 .5.5h2.325l2.363 1.89A.5.5 0 0 0 7 12z"/></svg></span>
438
- <span class="icon mute-icon hidden"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06m7.137 2.096a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0"/></svg></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"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M3.5 12V4l7 4zm8-8v8l-7-4z"/></svg></span>
455
- </button>
456
- <button class="control-btn playlist-next-btn" data-tooltip="nextvideo" style="display: none;">
457
- <span class="icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M12.5 4v8l-7-4zm-8 0v8l7-4z"/></svg></span>
458
- </button>
459
-
460
- ${this.options.showSubtitles ? `
461
- <div class="subtitles-control" style="display: none;">
462
- <button class="control-btn subtitles-btn" data-tooltip="subtitles">
463
- <span class="icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1z"/><path d="M6.096 4.972c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152m4.915 0c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152M6.096 9.972c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152m4.915 0c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152"/></svg></span>
464
- </button>
465
- <div class="subtitles-menu">
466
- <div class="subtitles-option active" data-track="off">Off</div>
467
- </div>
468
- </div>
469
- ` : ''}
470
-
471
- ${this.options.showSpeedControl ? `
472
- <div class="speed-control">
473
- <button class="control-btn speed-btn" data-tooltip="playback_speed">1x</button>
474
- <div class="speed-menu">
475
- <div class="speed-option" data-speed="0.5">0.5x</div>
476
- <div class="speed-option" data-speed="0.75">0.75x</div>
477
- <div class="speed-option active" data-speed="1">1x</div>
478
- <div class="speed-option" data-speed="1.25">1.25x</div>
479
- <div class="speed-option" data-speed="1.5">1.5x</div>
480
- <div class="speed-option" data-speed="2">2x</div>
481
- </div>
482
- </div>
483
- ` : ''}
484
-
485
- ${this.options.showQualitySelector && this.originalSources && this.originalSources.length > 1 ? `
486
- <div class="quality-control">
487
- <button class="control-btn quality-btn" data-tooltip="video_quality">
488
- <div class="quality-btn-text">
489
- <div class="selected-quality">${this.t('auto')}</div>
490
- <div class="current-quality"></div>
491
- </div>
492
- </button>
493
- <div class="quality-menu">
494
- <div class="quality-option selected" data-quality="auto">${this.t('auto')}</div>
495
- ${this.originalSources.map(s =>
496
- `<div class="quality-option" data-quality="${s.quality}">${s.quality}</div>`
497
- ).join('')}
498
- </div>
499
- </div>
500
- ` : ''}
501
-
502
- ${this.options.showPictureInPicture && this.isPiPSupported ? `
503
- <button class="control-btn pip-btn" data-tooltip="picture_in_picture">
504
- <span class="icon pip-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h13A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 12.5zM1.5 3a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5z"/><path d="M8 8.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5z"/></svg></span>
505
- <span class="icon pip-exit-icon hidden"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M0 3.5A1.5 1.5 0 0 1 1.5 2h13A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 12.5zM1.5 3a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5z"/></svg></span>
506
- </button>
507
- ` : ''}
508
-
509
- <div class="settings-control">
510
- <button class="control-btn settings-btn" data-tooltip="settings_menu">
511
- <span class="icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52z"/></svg></span>
512
- </button>
513
- <div class="settings-menu"></div>
514
- </div>
515
-
516
- ${this.options.showFullscreen ? `
517
- <button class="control-btn fullscreen-btn" data-tooltip="fullscreen">
518
- <span class="icon fullscreen-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5M.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5m15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5"/></svg></span>
519
- <span class="icon exit-fullscreen-icon hidden"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M5.5 0a.5.5 0 0 1 .5.5v4A1.5 1.5 0 0 1 4.5 6h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5m5 0a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 10 4.5v-4a.5.5 0 0 1 .5-.5M0 10.5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 6 11.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5m10 1a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0z"/></svg></span>
520
- </button>
521
- ` : ''}
522
- </div>
523
- </div>
524
- </div>
525
- `;
526
-
527
- this.container.insertAdjacentHTML('beforeend', controlsHTML);
528
- this.controls = document.getElementById(controlsId);
529
-
530
- // NEW: Initialize responsive settings menu
531
- setTimeout(() => {
532
- this.initializeResponsiveMenu();
533
- this.updateControlbarHeight();
534
- }, 100);
535
- }
536
-
537
- /* Initialize responsive menu with dynamic width calculation */
538
- initializeResponsiveMenu() {
539
- if (!this.controls) return;
540
-
541
- // Track screen size
542
- this.isSmallScreen = false;
543
-
544
- // Check initial size
545
- this.checkScreenSize();
546
-
547
- // Bind resize handler with updateControlbarHeight
548
- const resizeHandler = () => {
549
- this.checkScreenSize();
550
- this.updateControlbarHeight();
551
- };
552
-
553
- // Bind del context
554
- this.resizeHandler = resizeHandler.bind(this);
555
- window.addEventListener('resize', this.resizeHandler);
556
-
557
- // Bind events for settings menu
558
- this.bindSettingsMenuEvents();
559
- }
560
-
561
- // Dynamic controlbar height tracking for watermark positioning
562
- updateControlbarHeight() {
563
- if (!this.controls) return;
564
-
565
- const height = this.controls.offsetHeight;
566
- if (this.container) {
567
-
568
- this.container.style.setProperty('--player-controls-height', `${height}px`);
569
-
570
- const watermark = this.container.querySelector('.video-watermark.watermark-bottomleft, .video-watermark.watermark-bottomright');
571
- if (watermark) {
572
- const hasControls = this.container.classList.contains('has-controls');
573
- const isHideOnAutoHide = watermark.classList.contains('hide-on-autohide');
574
-
575
- if (hasControls || !isHideOnAutoHide) {
576
- watermark.style.bottom = `${height + 15}px`;
577
- } else {
578
- watermark.style.bottom = '15px';
579
- }
580
- }
581
- }
582
-
583
- if (this.options.debug) {
584
- console.log(`Controlbar height updated: ${height}px`);
585
- }
586
- }
587
-
588
- /* Dynamic width calculation based on logo presence */
589
- getResponsiveThreshold() {
590
- // Check if brand logo is enabled and present
591
- const hasLogo = this.options.brandLogoEnabled && this.options.brandLogoUrl;
592
-
593
- // If logo is present, use higher threshold (650px), otherwise 550px
594
- return hasLogo ? 650 : 550;
595
- }
596
-
597
- /* Check if screen is under dynamic threshold */
598
- checkScreenSize() {
599
- const threshold = this.getResponsiveThreshold();
600
- const newIsSmallScreen = window.innerWidth <= threshold;
601
-
602
- if (newIsSmallScreen !== this.isSmallScreen) {
603
- this.isSmallScreen = newIsSmallScreen;
604
- this.updateSettingsMenuVisibility();
605
-
606
- if (this.options.debug) {
607
- console.log(`Screen check: ${window.innerWidth}px vs ${threshold}px (threshold), logo: ${this.options.brandLogoEnabled}, small: ${this.isSmallScreen}`);
608
- }
609
- }
610
- }
611
-
612
- /* Update settings menu visibility based on screen size */
613
- updateSettingsMenuVisibility() {
614
- const settingsControl = this.controls?.querySelector('.settings-control');
615
- if (!settingsControl) return;
616
-
617
- if (this.isSmallScreen) {
618
- // Show settings menu and hide individual controls
619
- settingsControl.style.display = 'block';
620
-
621
- // Hide controls that will be moved to settings menu
622
- const pipBtn = this.controls.querySelector('.pip-btn');
623
- const speedControl = this.controls.querySelector('.speed-control');
624
- const subtitlesControl = this.controls.querySelector('.subtitles-control');
625
-
626
- if (pipBtn) pipBtn.style.display = 'none';
627
- if (speedControl) speedControl.style.display = 'none';
628
- if (subtitlesControl) subtitlesControl.style.display = 'none';
629
-
630
- this.populateSettingsMenu();
631
- } else {
632
- // Hide settings menu and show individual controls
633
- settingsControl.style.display = 'none';
634
-
635
- // Show original controls
636
- const pipBtn = this.controls.querySelector('.pip-btn');
637
- const speedControl = this.controls.querySelector('.speed-control');
638
- const subtitlesControl = this.controls.querySelector('.subtitles-control');
639
-
640
- if (pipBtn && this.options.showPictureInPicture && this.isPiPSupported) {
641
- pipBtn.style.display = 'flex';
642
- }
643
- if (speedControl && this.options.showSpeedControl) {
644
- speedControl.style.display = 'block';
645
- }
646
- if (subtitlesControl && this.options.showSubtitles && this.textTracks.length > 0) {
647
- subtitlesControl.style.display = 'block';
648
- }
649
- }
650
- }
651
-
652
- /**
653
- * Populate settings menu with controls
654
- */
655
- populateSettingsMenu() {
656
- const settingsMenu = this.controls?.querySelector('.settings-menu');
657
- if (!settingsMenu) return;
658
-
659
- let menuHTML = '';
660
-
661
- // Picture-in-Picture option
662
- if (this.options.showPictureInPicture && this.isPiPSupported) {
663
- const pipLabel = this.t('picture_in_picture') || 'Picture-in-Picture';
664
- menuHTML += `<div class="settings-option" data-action="pip">
665
- <span class="settings-option-label">${pipLabel}</span>
666
- </div>`;
667
- }
668
-
669
- // Speed Control - expandable
670
- if (this.options.showSpeedControl) {
671
- const speedLabel = this.t('playback_speed') || 'Playback Speed';
672
- const currentSpeed = this.video ? this.video.playbackRate : 1;
673
-
674
- menuHTML += `
675
- <div class="settings-expandable-wrapper">
676
- <div class="settings-option expandable-trigger" data-action="speed-expand">
677
- <span class="settings-option-label">${speedLabel}: ${currentSpeed}x</span>
678
- <span class="expand-arrow">▼</span>
679
- </div>
680
- <div class="settings-expandable-content" style="display: none;">`;
681
-
682
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
683
- speeds.forEach(speed => {
684
- const isActive = Math.abs(speed - currentSpeed) < 0.01;
685
- menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-speed="${speed}">${speed}x</div>`;
686
- });
687
-
688
- menuHTML += `</div></div>`;
689
- }
690
-
691
- // Subtitles - expandable
692
- if (this.options.showSubtitles && this.textTracks && this.textTracks.length > 0) {
693
- const subtitlesLabel = this.t('subtitles') || 'Subtitles';
694
- const currentTrack = this.currentSubtitleTrack;
695
- const currentLabel = this.subtitlesEnabled && currentTrack ? currentTrack.label : (this.t('subtitlesoff') || 'Off');
696
-
697
- menuHTML += `
698
- <div class="settings-expandable-wrapper">
699
- <div class="settings-option expandable-trigger" data-action="subtitles-expand">
700
- <span class="settings-option-label">${subtitlesLabel}: ${currentLabel}</span>
701
- <span class="expand-arrow">▼</span>
702
- </div>
703
- <div class="settings-expandable-content" style="display: none;">`;
704
-
705
- // Off option
706
- menuHTML += `<div class="settings-suboption ${!this.subtitlesEnabled ? 'active' : ''}" data-track="off">${this.t('subtitlesoff') || 'Off'}</div>`;
707
-
708
- // Subtitle tracks
709
- this.textTracks.forEach((trackData, index) => {
710
- const isActive = this.currentSubtitleTrack === trackData.track;
711
- menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-track="${index}">${trackData.label}</div>`;
712
- });
713
-
714
- menuHTML += `</div></div>`;
715
- }
716
-
717
- settingsMenu.innerHTML = menuHTML;
718
-
719
- // Add scrollbar if needed
720
- this.addSettingsMenuScrollbar();
721
- }
722
-
723
- /**
724
- * Add scrollbar to settings menu on mobile
725
- */
726
- addSettingsMenuScrollbar() {
727
- const settingsMenu = this.controls?.querySelector('.settings-menu');
728
- if (!settingsMenu) return;
729
-
730
- const playerHeight = this.container.offsetHeight;
731
- const maxMenuHeight = playerHeight - 100;
732
-
733
- settingsMenu.style.maxHeight = `${maxMenuHeight}px`;
734
- settingsMenu.style.overflowY = 'auto';
735
- settingsMenu.style.overflowX = 'hidden';
736
-
737
- // Add scrollbar styling
738
- if (!document.getElementById('player-settings-scrollbar-style')) {
739
- const scrollbarStyle = document.createElement('style');
740
- scrollbarStyle.id = 'player-settings-scrollbar-style';
741
- scrollbarStyle.textContent = `
742
- .settings-menu::-webkit-scrollbar {
743
- width: 6px;
744
- }
745
- .settings-menu::-webkit-scrollbar-track {
746
- background: rgba(255,255,255,0.05);
747
- border-radius: 3px;
748
- }
749
- .settings-menu::-webkit-scrollbar-thumb {
750
- background: rgba(255,255,255,0.3);
751
- border-radius: 3px;
752
- }
753
- .settings-menu::-webkit-scrollbar-thumb:hover {
754
- background: rgba(255,255,255,0.5);
755
- }
756
- `;
757
- document.head.appendChild(scrollbarStyle);
758
- }
759
-
760
- settingsMenu.style.scrollbarWidth = 'thin';
761
- settingsMenu.style.scrollbarColor = 'rgba(255,255,255,0.3) transparent';
762
- }
763
-
764
- /**
765
- * Bind settings menu events
766
- */
767
- bindSettingsMenuEvents() {
768
- const settingsMenu = this.controls?.querySelector('.settings-menu');
769
- if (!settingsMenu) return;
770
-
771
- settingsMenu.addEventListener('click', (e) => {
772
- e.stopPropagation();
773
-
774
- // Handle expandable triggers
775
- if (e.target.classList.contains('expandable-trigger') || e.target.closest('.expandable-trigger')) {
776
- const trigger = e.target.classList.contains('expandable-trigger') ? e.target : e.target.closest('.expandable-trigger');
777
- const wrapper = trigger.closest('.settings-expandable-wrapper');
778
- const content = wrapper.querySelector('.settings-expandable-content');
779
- const arrow = trigger.querySelector('.expand-arrow');
780
-
781
- const isExpanded = content.style.display !== 'none';
782
-
783
- if (isExpanded) {
784
- content.style.display = 'none';
785
- arrow.style.transform = 'rotate(0deg)';
786
- } else {
787
- content.style.display = 'block';
788
- arrow.style.transform = 'rotate(180deg)';
789
- }
790
- return;
791
- }
792
-
793
- // Handle direct actions (like PiP)
794
- if (e.target.classList.contains('settings-option') || e.target.closest('.settings-option')) {
795
- const option = e.target.classList.contains('settings-option') ? e.target : e.target.closest('.settings-option');
796
- const action = option.getAttribute('data-action');
797
-
798
- if (action === 'pip') {
799
- this.togglePictureInPicture();
800
- return;
801
- }
802
- }
803
-
804
- // Handle submenu actions
805
- if (e.target.classList.contains('settings-suboption')) {
806
- const wrapper = e.target.closest('.settings-expandable-wrapper');
807
- const trigger = wrapper.querySelector('.expandable-trigger');
808
- const action = trigger.getAttribute('data-action');
809
-
810
- if (action === 'speed-expand') {
811
- const speed = parseFloat(e.target.getAttribute('data-speed'));
812
- if (speed && speed > 0 && this.video && !this.isChangingQuality) {
813
- this.video.playbackRate = speed;
814
-
815
- // Update active states
816
- wrapper.querySelectorAll('.settings-suboption').forEach(opt => opt.classList.remove('active'));
817
- e.target.classList.add('active');
818
-
819
- // Update trigger text
820
- const label = trigger.querySelector('.settings-option-label');
821
- if (label) {
822
- const speedLabel = this.t('playback_speed') || 'Playback Speed';
823
- label.textContent = `${speedLabel}: ${speed}x`;
824
- }
825
-
826
- // Trigger event
827
- this.triggerEvent('speedchange', { speed, previousSpeed: this.video.playbackRate });
828
- }
829
- } else if (action === 'subtitles-expand') {
830
- const trackData = e.target.getAttribute('data-track');
831
- if (trackData === 'off') {
832
- this.disableSubtitles();
833
- } else {
834
- const trackIndex = parseInt(trackData);
835
- this.enableSubtitleTrack(trackIndex);
836
- }
837
-
838
- // Update active states
839
- wrapper.querySelectorAll('.settings-suboption').forEach(opt => opt.classList.remove('active'));
840
- e.target.classList.add('active');
841
-
842
- // Update trigger text
843
- const label = trigger.querySelector('.settings-option-label');
844
- if (label) {
845
- const subtitlesLabel = this.t('subtitles') || 'Subtitles';
846
- label.textContent = `${subtitlesLabel}: ${e.target.textContent}`;
847
- }
848
- }
849
- }
850
- });
851
- }
852
-
853
- /* TITLE OVERLAY MANAGEMENT */
854
- showTitleOverlay() {
855
- if (this.titleOverlay && this.options.videoTitle) {
856
- this.titleOverlay.classList.add('show');
857
-
858
- if (this.options.persistentTitle) {
859
- this.titleOverlay.classList.add('persistent');
860
- } else {
861
- this.titleOverlay.classList.remove('persistent');
862
- }
863
- }
864
- return this;
865
- }
866
-
867
- hideTitleOverlay() {
868
- if (this.titleOverlay) {
869
- this.titleOverlay.classList.remove('show');
870
- this.titleOverlay.classList.remove('persistent');
871
- }
872
- return this;
873
- }
874
-
875
- toggleTitleOverlay(show = null) {
876
- if (show === null) {
877
- return this.titleOverlay && this.titleOverlay.classList.contains('show')
878
- ? this.hideTitleOverlay()
879
- : this.showTitleOverlay();
880
- }
881
-
882
- return show ? this.showTitleOverlay() : this.hideTitleOverlay();
883
- }
884
-
885
- /* KEYBOARD CONTROLS */
886
- setupKeyboardControls() {
887
- document.addEventListener('keydown', (e) => {
888
- // Ignore if user is typing in an input field
889
- if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
890
-
891
- // On keyboard input, treat as mouse movement for auto-hide
892
- if (this.options.autoHide && this.autoHideInitialized) {
893
- this.showControlsNow();
894
- this.resetAutoHideTimer();
895
- }
896
-
897
- switch (e.code) {
898
- case 'Space':
899
- e.preventDefault();
900
- this.togglePlayPause();
901
- break;
902
- case 'KeyM':
903
- this.toggleMute();
904
- break;
905
- case 'KeyF':
906
- if (this.options.showFullscreen) {
907
- this.toggleFullscreen();
908
- }
909
- break;
910
- case 'KeyP':
911
- if (this.options.showPictureInPicture && this.isPiPSupported) {
912
- this.togglePictureInPicture();
913
- }
914
- break;
915
- case 'KeyT':
916
- if (this.options.showTitleOverlay) {
917
- this.toggleTitleOverlay();
918
- }
919
- break;
920
- case 'KeyS':
921
- if (this.options.showSubtitles) {
922
- this.toggleSubtitles();
923
- }
924
- break;
925
- case 'KeyD':
926
- this.debugQuality ? this.disableQualityDebug() : this.enableQualityDebug();
927
- break;
928
- case 'ArrowLeft':
929
- e.preventDefault();
930
- this.skipTime(-10);
931
- break;
932
- case 'ArrowRight':
933
- e.preventDefault();
934
- this.skipTime(10);
935
- break;
936
- case 'ArrowUp':
937
- e.preventDefault();
938
- this.changeVolume(0.1);
939
- break;
940
- case 'ArrowDown':
941
- e.preventDefault();
942
- this.changeVolume(-0.1);
943
- break;
944
- }
945
- });
946
- }
947
-
948
- /* CONTROL ACTIONS */
949
- togglePlayPause() {
950
- if (!this.video || this.isChangingQuality) return;
951
-
952
- if (this.video.paused) {
953
- this.play();
954
- } else {
955
- this.pause();
956
- }
957
- }
958
-
959
- toggleMute() {
960
- if (!this.video) return;
961
-
962
- const wasMuted = this.video.muted;
963
- this.video.muted = !this.video.muted;
964
-
965
- this.updateMuteButton();
966
- this.updateVolumeSliderVisual();
967
- this.initVolumeTooltip();
968
-
969
- // Triggers volumechange event
970
- this.triggerEvent('volumechange', {
971
- volume: this.getVolume(),
972
- muted: this.isMuted(),
973
- previousMuted: wasMuted
974
- });
975
- }
976
-
977
- updateMuteButton() {
978
- if (!this.video || !this.volumeIcon || !this.muteIcon) return;
979
-
980
- if (this.video.muted || this.video.volume === 0) {
981
- this.volumeIcon.classList.add('hidden');
982
- this.muteIcon.classList.remove('hidden');
983
- } else {
984
- this.volumeIcon.classList.remove('hidden');
985
- this.muteIcon.classList.add('hidden');
986
- }
987
- }
988
-
989
- /* LOADING STATES */
990
- showLoading() {
991
- if (this.loadingOverlay) {
992
- this.loadingOverlay.classList.add('active');
993
- }
994
- }
995
-
996
- hideLoading() {
997
- if (this.loadingOverlay) {
998
- this.loadingOverlay.classList.remove('active');
999
- }
1000
- }
1001
-
1002
- /* FULLSCREEN CONTROLS */
1003
- toggleFullscreen() {
1004
- if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement) {
1005
- this.exitFullscreen();
1006
- } else {
1007
- this.enterFullscreen();
1008
- }
1009
- }
1010
-
1011
- updateFullscreenButton() {
1012
- if (!this.fullscreenIcon || !this.exitFullscreenIcon) return;
1013
-
1014
- const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
1015
-
1016
- if (isFullscreen) {
1017
- this.fullscreenIcon.classList.add('hidden');
1018
- this.exitFullscreenIcon.classList.remove('hidden');
1019
- } else {
1020
- this.fullscreenIcon.classList.remove('hidden');
1021
- this.exitFullscreenIcon.classList.add('hidden');
1022
- }
1023
-
1024
- // Triggers fullscreenchange event
1025
- this.triggerEvent('fullscreenchange', {
1026
- active: !!isFullscreen,
1027
- mode: isFullscreen ? 'enter' : 'exit'
1028
- });
1029
- }
1030
-
1031
- /* PICTURE IN PICTURE CONTROLS */
1032
- togglePictureInPicture() {
1033
- if (!this.isPiPSupported || !this.video) return;
1034
-
1035
- if (document.pictureInPictureElement) {
1036
- this.exitPictureInPicture();
1037
- } else {
1038
- this.enterPictureInPicture();
1039
- }
1040
- }
1041
-
1042
- /* SEEK TOOLTIP MANAGEMENT */
1043
- toggleSeekTooltip(show = null) {
1044
- if (show === null) {
1045
- this.options.showSeekTooltip = !this.options.showSeekTooltip;
1046
- } else {
1047
- this.options.showSeekTooltip = show;
1048
- }
1049
-
1050
- if (this.seekTooltip) {
1051
- if (this.options.showSeekTooltip) {
1052
- this.setupSeekTooltip();
1053
- } else {
1054
- this.seekTooltip.classList.remove('visible');
1055
- }
1056
- }
1057
- }
1058
-
1059
- /* AUTO-HIDE CONFIGURATION */
1060
- setAutoHideDelay(delay) {
1061
- if (typeof delay === 'number' && delay >= 0) {
1062
- this.options.autoHideDelay = delay;
1063
- if (this.options.debug) console.log(`Auto-hide delay set to ${delay}ms`);
1064
- }
1065
- return this;
1066
- }
1067
-
1068
- getAutoHideDelay() {
1069
- return this.options.autoHideDelay;
1070
- }
1071
-
1072
- enableAutoHide() {
1073
- if (!this.options.autoHide) {
1074
- this.options.autoHide = true;
1075
- if (!this.autoHideInitialized) {
1076
- this.initAutoHide();
1077
- }
1078
- if (this.options.debug) console.log('Auto-hide enabled');
1079
- }
1080
- return this;
1081
- }
1082
-
1083
- disableAutoHide() {
1084
- if (this.options.autoHide) {
1085
- this.options.autoHide = false;
1086
- if (this.autoHideTimer) {
1087
- clearTimeout(this.autoHideTimer);
1088
- this.autoHideTimer = null;
1089
- }
1090
- this.showControlsNow();
1091
- if (this.options.debug) console.log('Auto-hide disabled');
1092
- }
1093
- return this;
1094
- }
1095
-
1096
- forceShowControls() {
1097
- this.showControlsNow();
1098
- if (this.autoHideInitialized) {
1099
- this.resetAutoHideTimer();
1100
- }
1101
- return this;
1102
- }
1103
-
1104
- forceHideControls() {
1105
- if (!this.mouseOverControls && this.video && !this.video.paused) {
1106
- this.hideControlsNow();
1107
- }
1108
- return this;
1109
- }
1110
-
1111
- isAutoHideEnabled() {
1112
- return this.options.autoHide;
1113
- }
1114
-
1115
- isAutoHideInitialized() {
1116
- return this.autoHideInitialized;
1117
- }
1118
-
1119
- /**
1120
- * Hide mouse cursor in player container
1121
- * Only hides cursor in main container, not in plugin iframes
1122
- */
1123
- hideCursor() {
1124
- if (!this.options.hideCursor) {
1125
- return; // Do not hide cursor if option is disabled
1126
- }
1127
-
1128
- if (this.container) {
1129
- this.container.classList.add('hide-cursor');
1130
- if (this.options.debug) console.log('🖱️ Cursor hidden');
1131
- }
1132
- }
1133
-
1134
- /**
1135
- * Show mouse cursor in player container
1136
- */
1137
- showCursor() {
1138
- if (this.container) {
1139
- this.container.classList.remove('hide-cursor');
1140
- if (this.options.debug) console.log('🖱️ Cursor shown');
1141
- }
1142
- }
1143
-
1144
- /**
1145
- * Enable cursor hiding when controlbar is hidden
1146
- * @returns {Object} this
1147
- */
1148
- enableCursorHiding() {
1149
- this.options.hideCursor = true;
1150
- if (this.options.debug) console.log('Cursor hiding enabled');
1151
- return this;
1152
- }
1153
-
1154
- /**
1155
- * Disable cursor hiding - cursor will always be visible
1156
- * @returns {Object} this
1157
- */
1158
- disableCursorHiding() {
1159
- this.options.hideCursor = false;
1160
- this.showCursor(); // Ensure cursor is shown immediately
1161
- if (this.options.debug) console.log('Cursor hiding disabled');
1162
- return this;
1163
- }
1164
-
1165
- /**
1166
- * Check if cursor hiding is enabled
1167
- * @returns {Boolean} True if cursor hiding is enabled
1168
- */
1169
- isCursorHidingEnabled() {
1170
- return this.options.hideCursor;
1171
- }
1172
-
1173
- /* PLAYLIST CONTROLS */
1174
- showPlaylistControls() {
1175
- if (!this.playlistPrevBtn || !this.playlistNextBtn) return;
1176
-
1177
- this.playlistPrevBtn.style.display = 'flex';
1178
- this.playlistNextBtn.style.display = 'flex';
1179
- this.updatePlaylistButtons();
1180
-
1181
- if (this.options.debug) console.log('Playlist controls shown');
1182
- }
1183
-
1184
- hidePlaylistControls() {
1185
- if (!this.playlistPrevBtn || !this.playlistNextBtn) return;
1186
-
1187
- this.playlistPrevBtn.style.display = 'none';
1188
- this.playlistNextBtn.style.display = 'none';
1189
-
1190
- if (this.options.debug) console.log('Playlist controls hidden');
1191
- }
1192
-
1193
- updatePlaylistButtons() {
1194
- if (!this.playlistPrevBtn || !this.playlistNextBtn || !this.isPlaylistActive) return;
1195
-
1196
- const canGoPrev = this.currentPlaylistIndex > 0 || this.options.playlistLoop;
1197
- const canGoNext = this.currentPlaylistIndex < this.playlist.length - 1 || this.options.playlistLoop;
1198
-
1199
- this.playlistPrevBtn.disabled = !canGoPrev;
1200
- this.playlistNextBtn.disabled = !canGoNext;
1201
-
1202
- // Update visual state
1203
- if (canGoPrev) {
1204
- this.playlistPrevBtn.style.opacity = '1';
1205
- this.playlistPrevBtn.style.cursor = 'pointer';
1206
- } else {
1207
- this.playlistPrevBtn.style.opacity = '0.4';
1208
- this.playlistPrevBtn.style.cursor = 'not-allowed';
1209
- }
1210
-
1211
- if (canGoNext) {
1212
- this.playlistNextBtn.style.opacity = '1';
1213
- this.playlistNextBtn.style.cursor = 'pointer';
1214
- } else {
1215
- this.playlistNextBtn.style.opacity = '0.4';
1216
- this.playlistNextBtn.style.cursor = 'not-allowed';
1217
- }
1218
- }
1219
-
1220
- /* RESPONSIVE OPTIMIZATION */
1221
- optimizeButtonsForSmallHeight() {
1222
- const currentHeight = window.innerHeight;
1223
- const controlsRect = this.controls.getBoundingClientRect();
1224
-
1225
- // If controlbar is taller than 40% of viewport, optimize
1226
- if (controlsRect.height > currentHeight * 0.4) {
1227
- this.controls.classList.add('ultra-compact');
1228
- if (this.options.debug) console.log('Applied ultra-compact mode for height:', currentHeight);
1229
- } else {
1230
- this.controls.classList.remove('ultra-compact');
1231
- }
1232
-
1233
- // Hide non-essential buttons on very small heights
1234
- const nonEssentialButtons = this.controls.querySelectorAll('.pip-btn, .speed-control');
1235
- if (currentHeight < 180) {
1236
- nonEssentialButtons.forEach(btn => btn.style.display = 'none');
1237
- } else {
1238
- nonEssentialButtons.forEach(btn => btn.style.display = '');
1239
- }
1240
- }
1241
-
1242
- /* Controls methods for main class - All original functionality preserved exactly */