myetv-player 1.0.0 → 1.0.8

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 (38) hide show
  1. package/.github/workflows/codeql.yml +100 -0
  2. package/README.md +49 -58
  3. package/SECURITY.md +50 -0
  4. package/css/myetv-player.css +424 -219
  5. package/css/myetv-player.min.css +1 -1
  6. package/dist/myetv-player.js +1759 -1502
  7. package/dist/myetv-player.min.js +1705 -1469
  8. package/package.json +7 -1
  9. package/plugins/README.md +1016 -0
  10. package/plugins/cloudflare/README.md +1068 -0
  11. package/plugins/cloudflare/myetv-player-cloudflare-stream-plugin.js +556 -0
  12. package/plugins/facebook/README.md +1024 -0
  13. package/plugins/facebook/myetv-player-facebook-plugin.js +437 -0
  14. package/plugins/gamepad-remote-controller/README.md +816 -0
  15. package/plugins/gamepad-remote-controller/myetv-player-gamepad-remote-plugin.js +678 -0
  16. package/plugins/google-adsense-ads/README.md +1 -0
  17. package/plugins/google-adsense-ads/g-adsense-ads-plugin.js +158 -0
  18. package/plugins/google-ima-ads/README.md +1 -0
  19. package/plugins/google-ima-ads/g-ima-ads-plugin.js +355 -0
  20. package/plugins/twitch/README.md +1185 -0
  21. package/plugins/twitch/myetv-player-twitch-plugin.js +569 -0
  22. package/plugins/vast-vpaid-ads/README.md +1 -0
  23. package/plugins/vast-vpaid-ads/vast-vpaid-ads-plugin.js +346 -0
  24. package/plugins/vimeo/README.md +1416 -0
  25. package/plugins/vimeo/myetv-player-vimeo.js +640 -0
  26. package/plugins/youtube/README.md +851 -0
  27. package/plugins/youtube/myetv-player-youtube-plugin.js +1714 -210
  28. package/scss/README.md +160 -0
  29. package/scss/_controls.scss +184 -30
  30. package/scss/_menus.scss +840 -672
  31. package/scss/_responsive.scss +67 -105
  32. package/scss/_volume.scss +67 -105
  33. package/src/README.md +559 -0
  34. package/src/controls.js +17 -5
  35. package/src/core.js +1237 -1060
  36. package/src/i18n.js +27 -1
  37. package/src/quality.js +478 -436
  38. package/src/subtitles.js +2 -2
@@ -283,14 +283,40 @@ class VideoPlayerI18n {
283
283
  // Add new translations
284
284
  addTranslations(lang, translations) {
285
285
  try {
286
+ // SECURITY: Prevent prototype pollution by validating lang parameter
287
+ if (!this.isValidLanguageKey(lang)) {
288
+ console.warn('Invalid language key rejected:', lang);
289
+ return;
290
+ }
291
+
286
292
  if (!this.translations[lang]) {
287
293
  this.translations[lang] = {};
288
294
  }
289
- Object.assign(this.translations[lang], translations);
295
+
296
+ // SECURITY: Only copy safe properties from translations object
297
+ for (const key in translations) {
298
+ if (translations.hasOwnProperty(key) && this.isValidLanguageKey(key)) {
299
+ this.translations[lang][key] = translations[key];
300
+ }
301
+ }
290
302
  } catch (error) {
291
303
  console.warn('Error adding translations:', error);
292
304
  }
293
305
  }
306
+
307
+ // SECURITY: Validate that a key is safe (not a prototype polluting key)
308
+ isValidLanguageKey(key) {
309
+ if (typeof key !== 'string') return false;
310
+
311
+ // Reject dangerous prototype-polluting keys
312
+ const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
313
+ if (dangerousKeys.includes(key.toLowerCase())) {
314
+ return false;
315
+ }
316
+
317
+ // Accept only alphanumeric keys with underscore/dash (e.g., 'en', 'it', 'play_pause')
318
+ return /^[a-zA-Z0-9_-]+$/.test(key);
319
+ }
294
320
  }
295
321
 
296
322
  // Safe initialization with error handling
@@ -373,432 +399,435 @@ window.registerMYETVPlugin = registerPlugin;
373
399
  class MYETVvideoplayer {
374
400
 
375
401
  constructor(videoElement, options = {}) {
376
- this.video = typeof videoElement === 'string'
377
- ? document.getElementById(videoElement)
378
- : videoElement;
379
-
380
- if (!this.video) {
381
- throw new Error('Video element not found: ' + videoElement);
382
- }
383
-
384
- this.options = {
385
- showQualitySelector: true,
386
- showSpeedControl: true,
387
- showFullscreen: true,
388
- showPictureInPicture: true,
389
- showSubtitles: true,
390
- subtitlesEnabled: false,
391
- autoHide: true,
392
- autoHideDelay: 3000,
393
- poster: null, // URL of poster image
394
- showPosterOnEnd: false, // Show poster again when video ends
395
- keyboardControls: true,
396
- showSeekTooltip: true,
397
- showTitleOverlay: false,
398
- videoTitle: '',
399
- persistentTitle: false,
400
- debug: false, // Enable/disable debug logging
401
- autoplay: false, // if video should autoplay at start
402
- defaultQuality: 'auto', // 'auto', '1080p', '720p', '480p', etc.
403
- language: null, // language of the player (default english)
404
- pauseClick: true, // the player should be paused when click over the video area
405
- doubleTapPause: true, // first tap (or click) show the controlbar, second tap (or click) pause
406
- brandLogoEnabled: false, // Enable/disable brand logo
407
- brandLogoUrl: '', // URL for brand logo image
408
- brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
409
- playlistEnabled: true, // Enable/disable playlist detection
410
- playlistAutoPlay: true, // Auto-play next video when current ends
411
- playlistLoop: false, // Loop playlist when reaching the end
412
- loop: false, // Loop video when it ends (restart from beginning)
413
- volumeSlider: 'horizontal', //volume slider type: 'horizontal' or 'vertical'
414
- // WATERMARK OVERLAY
415
- watermarkUrl: '', // URL of watermark image
416
- watermarkLink: '', // Optional URL to open when clicking watermark
417
- watermarkPosition: 'bottomright', // Position: topleft, topright, bottomleft, bottomright
418
- watermarkTitle: '', // Optional tooltip title
419
- hideWatermark: true, // Hide watermark with controls (default: true)
420
- // ADAPTIVE STREAMING SUPPORT
421
- adaptiveStreaming: false, // Enable DASH/HLS adaptive streaming
422
- dashLibUrl: 'https://cdn.dashjs.org/latest/dash.all.min.js', // Dash.js library URL
423
- hlsLibUrl: 'https://cdn.jsdelivr.net/npm/hls.js@latest', // HLS.js library URL
424
- adaptiveQualityControl: true, // Show quality control for adaptive streams
425
- // AUDIO PLAYER
426
- audiofile: false,
427
- audiowave: false,
428
- // RESOLUTION CONTROL
429
- resolution: "normal", // "normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"
430
- ...options
431
- };
402
+ this.video = typeof videoElement === 'string'
403
+ ? document.getElementById(videoElement)
404
+ : videoElement;
432
405
 
433
- this.isUserSeeking = false;
434
- this.controlsTimeout = null;
435
- this.titleTimeout = null;
436
- this.currentQualityIndex = 0;
437
- this.qualities = [];
438
- this.originalSources = [];
439
- this.isPiPSupported = this.checkPiPSupport();
440
- this.seekTooltip = null;
441
- this.titleOverlay = null;
442
- this.isPlayerReady = false;
443
-
444
- // Subtitle management
445
- this.textTracks = [];
446
- this.currentSubtitleTrack = null;
447
- this.subtitlesEnabled = false;
448
- this.customSubtitleRenderer = null;
449
-
450
- // Chapter management
451
- this.chapters = [];
452
- this.chapterMarkersContainer = null;
453
- this.chapterTooltip = null;
454
-
455
- // Dual quality indicator management
456
- this.selectedQuality = this.options.defaultQuality || 'auto';
457
- this.currentPlayingQuality = null;
458
- this.qualityMonitorInterval = null;
406
+ if (!this.video) {
407
+ throw new Error('Video element not found: ' + videoElement);
408
+ }
409
+
410
+ this.options = {
411
+ showQualitySelector: true,
412
+ showSpeedControl: true,
413
+ showFullscreen: true,
414
+ showPictureInPicture: true,
415
+ showSubtitles: true,
416
+ subtitlesEnabled: false,
417
+ autoHide: true,
418
+ autoHideDelay: 3000,
419
+ poster: null, // URL of poster image
420
+ showPosterOnEnd: false, // Show poster again when video ends
421
+ keyboardControls: true,
422
+ showSeekTooltip: true,
423
+ showTitleOverlay: false,
424
+ videoTitle: '',
425
+ persistentTitle: false,
426
+ debug: false, // Enable/disable debug logging
427
+ autoplay: false, // if video should autoplay at start
428
+ defaultQuality: 'auto', // 'auto', '1080p', '720p', '480p', etc.
429
+ language: null, // language of the player (default english)
430
+ pauseClick: true, // the player should be paused when click over the video area
431
+ doubleTapPause: true, // first tap (or click) show the controlbar, second tap (or click) pause
432
+ brandLogoEnabled: false, // Enable/disable brand logo
433
+ brandLogoUrl: '', // URL for brand logo image
434
+ brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
435
+ playlistEnabled: true, // Enable/disable playlist detection
436
+ playlistAutoPlay: true, // Auto-play next video when current ends
437
+ playlistLoop: false, // Loop playlist when reaching the end
438
+ loop: false, // Loop video when it ends (restart from beginning)
439
+ volumeSlider: 'show', // Mobile volume slider: 'show' (horizontal popup) or 'hide' (no slider on mobile)
440
+ // WATERMARK OVERLAY
441
+ watermarkUrl: '', // URL of watermark image
442
+ watermarkLink: '', // Optional URL to open when clicking watermark
443
+ watermarkPosition: 'bottomright', // Position: topleft, topright, bottomleft, bottomright
444
+ watermarkTitle: '', // Optional tooltip title
445
+ hideWatermark: true, // Hide watermark with controls (default: true)
446
+ // ADAPTIVE STREAMING SUPPORT
447
+ adaptiveStreaming: false, // Enable DASH/HLS adaptive streaming
448
+ dashLibUrl: 'https://cdn.dashjs.org/latest/dash.all.min.js', // Dash.js library URL
449
+ hlsLibUrl: 'https://cdn.jsdelivr.net/npm/hls.js@latest', // HLS.js library URL
450
+ adaptiveQualityControl: true, // Show quality control for adaptive streams
451
+ //seek shape
452
+ seekHandleShape: 'circle', // Available shape: none, circle, square, diamond, arrow, triangle, heart, star
453
+ // AUDIO PLAYER
454
+ audiofile: false,
455
+ audiowave: false,
456
+ // RESOLUTION CONTROL
457
+ resolution: "normal", // "normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"
458
+ ...options
459
+ };
459
460
 
460
- // Quality change management
461
- this.qualityChangeTimeout = null;
462
- this.isChangingQuality = false;
461
+ this.isUserSeeking = false;
462
+ this.controlsTimeout = null;
463
+ this.titleTimeout = null;
464
+ this.currentQualityIndex = 0;
465
+ this.qualities = [];
466
+ this.originalSources = [];
467
+ this.setupMenuToggles(); // Initialize menu toggle system
468
+ this.isPiPSupported = this.checkPiPSupport();
469
+ this.seekTooltip = null;
470
+ this.titleOverlay = null;
471
+ this.isPlayerReady = false;
472
+
473
+ // Subtitle management
474
+ this.textTracks = [];
475
+ this.currentSubtitleTrack = null;
476
+ this.subtitlesEnabled = false;
477
+ this.customSubtitleRenderer = null;
463
478
 
464
- // Quality debug
465
- this.debugQuality = false;
479
+ // Chapter management
480
+ this.chapters = [];
481
+ this.chapterMarkersContainer = null;
482
+ this.chapterTooltip = null;
466
483
 
467
- // Auto-hide system
468
- this.autoHideTimer = null;
469
- this.mouseOverControls = false;
470
- this.autoHideDebug = false;
471
- this.autoHideInitialized = false;
484
+ // Dual quality indicator management
485
+ this.selectedQuality = this.options.defaultQuality || 'auto';
486
+ this.currentPlayingQuality = null;
487
+ this.qualityMonitorInterval = null;
472
488
 
473
- // Poster management
474
- this.posterOverlay = null;
489
+ // Quality change management
490
+ this.qualityChangeTimeout = null;
491
+ this.isChangingQuality = false;
492
+
493
+ // Quality debug
494
+ this.debugQuality = false;
495
+
496
+ // Auto-hide system
497
+ this.autoHideTimer = null;
498
+ this.mouseOverControls = false;
499
+ this.autoHideDebug = false;
500
+ this.autoHideInitialized = false;
501
+
502
+ // Poster management
503
+ this.posterOverlay = null;
475
504
 
476
505
  // Watermark overlay
477
506
  this.watermarkElement = null;
478
507
 
479
- // Custom event system
480
- this.eventCallbacks = {
481
- 'played': [],
482
- 'paused': [],
483
- 'subtitlechange': [],
484
- 'chapterchange': [],
485
- 'pipchange': [],
486
- 'fullscreenchange': [],
487
- 'speedchange': [],
488
- 'timeupdate': [],
489
- 'volumechange': [],
490
- 'qualitychange': [],
491
- 'playlistchange': [],
492
- 'ended': []
493
- };
508
+ // Custom event system
509
+ this.eventCallbacks = {
510
+ 'played': [],
511
+ 'paused': [],
512
+ 'subtitlechange': [],
513
+ 'chapterchange': [],
514
+ 'pipchange': [],
515
+ 'fullscreenchange': [],
516
+ 'speedchange': [],
517
+ 'timeupdate': [],
518
+ 'volumechange': [],
519
+ 'qualitychange': [],
520
+ 'playlistchange': [],
521
+ 'ended': []
522
+ };
494
523
 
495
- // Playlist management
496
- this.playlist = [];
497
- this.currentPlaylistIndex = -1;
498
- this.playlistId = null;
499
- this.isPlaylistActive = false;
500
-
501
- // Adaptive streaming management
502
- this.dashPlayer = null;
503
- this.hlsPlayer = null;
504
- this.adaptiveStreamingType = null; // 'dash', 'hls', or null
505
- this.isAdaptiveStream = false;
506
- this.adaptiveQualities = [];
507
- this.librariesLoaded = {
508
- dash: false,
509
- hls: false
510
- };
524
+ // Playlist management
525
+ this.playlist = [];
526
+ this.currentPlaylistIndex = -1;
527
+ this.playlistId = null;
528
+ this.isPlaylistActive = false;
529
+
530
+ // Adaptive streaming management
531
+ this.dashPlayer = null;
532
+ this.hlsPlayer = null;
533
+ this.adaptiveStreamingType = null; // 'dash', 'hls', or null
534
+ this.isAdaptiveStream = false;
535
+ this.adaptiveQualities = [];
536
+ this.librariesLoaded = {
537
+ dash: false,
538
+ hls: false
539
+ };
511
540
 
512
- this.lastTimeUpdate = 0; // For throttling timeupdate events
541
+ this.lastTimeUpdate = 0; // For throttling timeupdate events
513
542
 
514
- if (this.options.language && this.isI18nAvailable()) {
515
- VideoPlayerTranslations.setLanguage(this.options.language);
516
- }
517
- // Apply autoplay if enabled
518
- if (options.autoplay) {
519
- this.video.autoplay = true;
520
- }
543
+ if (this.options.language && this.isI18nAvailable()) {
544
+ VideoPlayerTranslations.setLanguage(this.options.language);
545
+ }
546
+ // Apply autoplay if enabled
547
+ if (options.autoplay) {
548
+ this.video.autoplay = true;
549
+ }
521
550
 
522
- try {
523
- this.interceptAutoLoading();
524
- this.createPlayerStructure();
525
- this.initializeElements();
526
- // audio player adaptation
527
- this.adaptToAudioFile = function () {
528
- if (this.options.audiofile) {
529
- // Nascondere video
530
- if (this.video) {
531
- this.video.style.display = 'none';
532
- }
533
- if (this.container) {
534
- this.container.classList.add('audio-player');
535
- }
536
- if (this.options.audiowave) {
537
- this.initAudioWave();
538
- }
551
+ try {
552
+ this.interceptAutoLoading();
553
+ this.createPlayerStructure();
554
+ this.initializeElements();
555
+ // audio player adaptation
556
+ this.adaptToAudioFile = function () {
557
+ if (this.options.audiofile) {
558
+ // Nascondere video
559
+ if (this.video) {
560
+ this.video.style.display = 'none';
539
561
  }
540
- };
541
- // Audio wave with Web Audio API
542
- this.initAudioWave = function () {
543
- if (!this.video) return;
544
-
545
- this.audioWaveCanvas = document.createElement('canvas');
546
- this.audioWaveCanvas.className = 'audio-wave-canvas';
547
- this.container.appendChild(this.audioWaveCanvas);
548
-
549
- const canvasCtx = this.audioWaveCanvas.getContext('2d');
550
- const WIDTH = this.audioWaveCanvas.width = this.container.clientWidth;
551
- const HEIGHT = this.audioWaveCanvas.height = 60; // altezza onda audio
552
-
553
- // Setup Web Audio API
554
- const AudioContext = window.AudioContext || window.webkitAudioContext;
555
- this.audioCtx = new AudioContext();
556
- this.analyser = this.audioCtx.createAnalyser();
557
- this.source = this.audioCtx.createMediaElementSource(this.video);
558
- this.source.connect(this.analyser);
559
- this.analyser.connect(this.audioCtx.destination);
560
-
561
- this.analyser.fftSize = 2048;
562
- const bufferLength = this.analyser.fftSize;
563
- const dataArray = new Uint8Array(bufferLength);
564
-
565
- // canvas
566
- const draw = () => {
567
- requestAnimationFrame(draw);
568
- this.analyser.getByteTimeDomainData(dataArray);
569
-
570
- canvasCtx.fillStyle = '#222';
571
- canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
572
-
573
- canvasCtx.lineWidth = 2;
574
- canvasCtx.strokeStyle = '#33ccff';
575
- canvasCtx.beginPath();
576
-
577
- const sliceWidth = WIDTH / bufferLength;
578
- let x = 0;
579
-
580
- for (let i = 0; i < bufferLength; i++) {
581
- const v = dataArray[i] / 128.0;
582
- const y = v * HEIGHT / 2;
583
-
584
- if (i === 0) {
585
- canvasCtx.moveTo(x, y);
586
- } else {
587
- canvasCtx.lineTo(x, y);
588
- }
589
-
590
- x += sliceWidth;
562
+ if (this.container) {
563
+ this.container.classList.add('audio-player');
564
+ }
565
+ if (this.options.audiowave) {
566
+ this.initAudioWave();
567
+ }
568
+ }
569
+ };
570
+ // Audio wave with Web Audio API
571
+ this.initAudioWave = function () {
572
+ if (!this.video) return;
573
+
574
+ this.audioWaveCanvas = document.createElement('canvas');
575
+ this.audioWaveCanvas.className = 'audio-wave-canvas';
576
+ this.container.appendChild(this.audioWaveCanvas);
577
+
578
+ const canvasCtx = this.audioWaveCanvas.getContext('2d');
579
+ const WIDTH = this.audioWaveCanvas.width = this.container.clientWidth;
580
+ const HEIGHT = this.audioWaveCanvas.height = 60; // altezza onda audio
581
+
582
+ // Setup Web Audio API
583
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
584
+ this.audioCtx = new AudioContext();
585
+ this.analyser = this.audioCtx.createAnalyser();
586
+ this.source = this.audioCtx.createMediaElementSource(this.video);
587
+ this.source.connect(this.analyser);
588
+ this.analyser.connect(this.audioCtx.destination);
589
+
590
+ this.analyser.fftSize = 2048;
591
+ const bufferLength = this.analyser.fftSize;
592
+ const dataArray = new Uint8Array(bufferLength);
593
+
594
+ // canvas
595
+ const draw = () => {
596
+ requestAnimationFrame(draw);
597
+ this.analyser.getByteTimeDomainData(dataArray);
598
+
599
+ canvasCtx.fillStyle = '#222';
600
+ canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
601
+
602
+ canvasCtx.lineWidth = 2;
603
+ canvasCtx.strokeStyle = '#33ccff';
604
+ canvasCtx.beginPath();
605
+
606
+ const sliceWidth = WIDTH / bufferLength;
607
+ let x = 0;
608
+
609
+ for (let i = 0; i < bufferLength; i++) {
610
+ const v = dataArray[i] / 128.0;
611
+ const y = v * HEIGHT / 2;
612
+
613
+ if (i === 0) {
614
+ canvasCtx.moveTo(x, y);
615
+ } else {
616
+ canvasCtx.lineTo(x, y);
591
617
  }
592
- canvasCtx.lineTo(WIDTH, HEIGHT / 2);
593
- canvasCtx.stroke();
594
- };
595
618
 
596
- draw();
619
+ x += sliceWidth;
620
+ }
621
+ canvasCtx.lineTo(WIDTH, HEIGHT / 2);
622
+ canvasCtx.stroke();
597
623
  };
598
- this.adaptToAudioFile();
599
- this.bindEvents();
600
624
 
601
- if (this.options.keyboardControls) {
602
- this.setupKeyboardControls();
603
- }
625
+ draw();
626
+ };
627
+ this.adaptToAudioFile();
628
+ this.bindEvents();
604
629
 
605
- this.updateVolumeSliderVisual();
606
- this.initVolumeTooltip();
607
- this.updateTooltips();
608
- this.markPlayerReady();
609
- this.initializePluginSystem();
610
- this.restoreSourcesAsync();
630
+ if (this.options.keyboardControls) {
631
+ this.setupKeyboardControls();
632
+ }
611
633
 
612
- this.initializeSubtitles();
613
- this.initializeQualityMonitoring();
634
+ this.updateVolumeSliderVisual();
635
+ this.initVolumeTooltip();
636
+ this.updateTooltips();
637
+ this.markPlayerReady();
638
+ this.initializePluginSystem();
639
+ this.restoreSourcesAsync();
614
640
 
615
- this.initializeResolution();
616
- this.initializeChapters();
617
- this.initializePoster();
618
- this.initializeWatermark();
641
+ this.initializeSubtitles();
642
+ this.initializeQualityMonitoring();
619
643
 
620
- } catch (error) {
621
- if (this.options.debug) console.error('Video player initialization error:', error);
622
- }
623
- }
644
+ this.initializeResolution();
645
+ this.initializeChapters();
646
+ this.initializePoster();
647
+ this.initializeWatermark();
624
648
 
625
- getPlayerState() {
626
- return {
627
- isPlaying: !this.isPaused(),
628
- isPaused: this.isPaused(),
629
- currentTime: this.getCurrentTime(),
630
- duration: this.getDuration(),
631
- volume: this.getVolume(),
632
- isMuted: this.isMuted(),
633
- playbackRate: this.getPlaybackRate(),
634
- isFullscreen: this.isFullscreenActive(),
635
- isPictureInPicture: this.isPictureInPictureActive(),
636
- subtitlesEnabled: this.isSubtitlesEnabled(),
637
- currentSubtitle: this.getCurrentSubtitleTrack(),
638
- selectedQuality: this.getSelectedQuality(),
639
- currentQuality: this.getCurrentPlayingQuality(),
640
- isAutoQuality: this.isAutoQualityActive()
641
- };
642
- }
643
-
644
- isI18nAvailable() {
645
- return typeof VideoPlayerTranslations !== 'undefined' &&
646
- VideoPlayerTranslations !== null &&
647
- typeof VideoPlayerTranslations.t === 'function';
649
+ } catch (error) {
650
+ if (this.options.debug) console.error('Video player initialization error:', error);
648
651
  }
652
+ }
649
653
 
650
- t(key) {
651
- if (this.isI18nAvailable()) {
652
- try {
653
- return VideoPlayerTranslations.t(key);
654
- } catch (error) {
655
- if (this.options.debug) console.warn('Translation error:', error);
656
- }
657
- }
658
-
659
- const fallback = {
660
- 'play_pause': 'Play/Pause (Space)',
661
- 'mute_unmute': 'Mute/Unmute (M)',
662
- 'volume': 'Volume',
663
- 'playback_speed': 'Playback speed',
664
- 'video_quality': 'Video quality',
665
- 'picture_in_picture': 'Picture-in-Picture (P)',
666
- 'fullscreen': 'Fullscreen (F)',
667
- 'subtitles': 'Subtitles (S)',
668
- 'subtitles_enable': 'Enable subtitles',
669
- 'subtitles_disable': 'Disable subtitles',
670
- 'subtitles_off': 'Off',
671
- 'auto': 'Auto',
672
- 'brand_logo': 'Brand logo',
673
- 'next_video': 'Next video (N)',
674
- 'prev_video': 'Previous video (P)',
675
- 'playlist_next': 'Next',
676
- 'playlist_prev': 'Previous'
677
- };
654
+ getPlayerState() {
655
+ return {
656
+ isPlaying: !this.isPaused(),
657
+ isPaused: this.isPaused(),
658
+ currentTime: this.getCurrentTime(),
659
+ duration: this.getDuration(),
660
+ volume: this.getVolume(),
661
+ isMuted: this.isMuted(),
662
+ playbackRate: this.getPlaybackRate(),
663
+ isFullscreen: this.isFullscreenActive(),
664
+ isPictureInPicture: this.isPictureInPictureActive(),
665
+ subtitlesEnabled: this.isSubtitlesEnabled(),
666
+ currentSubtitle: this.getCurrentSubtitleTrack(),
667
+ selectedQuality: this.getSelectedQuality(),
668
+ currentQuality: this.getCurrentPlayingQuality(),
669
+ isAutoQuality: this.isAutoQualityActive()
670
+ };
671
+ }
678
672
 
679
- return fallback[key] || key;
680
- }
673
+ isI18nAvailable() {
674
+ return typeof VideoPlayerTranslations !== 'undefined' &&
675
+ VideoPlayerTranslations !== null &&
676
+ typeof VideoPlayerTranslations.t === 'function';
677
+ }
681
678
 
682
- interceptAutoLoading() {
683
- this.saveOriginalSources();
684
- this.disableSources();
679
+ t(key) {
680
+ if (this.isI18nAvailable()) {
681
+ try {
682
+ return VideoPlayerTranslations.t(key);
683
+ } catch (error) {
684
+ if (this.options.debug) console.warn('Translation error:', error);
685
+ }
686
+ }
687
+
688
+ const fallback = {
689
+ 'play_pause': 'Play/Pause (Space)',
690
+ 'mute_unmute': 'Mute/Unmute (M)',
691
+ 'volume': 'Volume',
692
+ 'playback_speed': 'Playback speed',
693
+ 'video_quality': 'Video quality',
694
+ 'picture_in_picture': 'Picture-in-Picture (P)',
695
+ 'fullscreen': 'Fullscreen (F)',
696
+ 'subtitles': 'Subtitles (S)',
697
+ 'subtitles_enable': 'Enable subtitles',
698
+ 'subtitles_disable': 'Disable subtitles',
699
+ 'subtitles_off': 'Off',
700
+ 'auto': 'Auto',
701
+ 'brand_logo': 'Brand logo',
702
+ 'next_video': 'Next video (N)',
703
+ 'prev_video': 'Previous video (P)',
704
+ 'playlist_next': 'Next',
705
+ 'playlist_prev': 'Previous'
706
+ };
685
707
 
686
- this.video.preload = 'none';
687
- this.video.controls = false;
688
- this.video.autoplay = false;
708
+ return fallback[key] || key;
709
+ }
689
710
 
690
- if (this.video.src && this.video.src !== window.location.href) {
691
- this.originalSrc = this.video.src;
692
- this.video.removeAttribute('src');
693
- this.video.src = '';
694
- }
711
+ interceptAutoLoading() {
712
+ this.saveOriginalSources();
713
+ this.disableSources();
695
714
 
696
- this.hideNativePlayer();
715
+ this.video.preload = 'none';
716
+ this.video.controls = false;
717
+ this.video.autoplay = false;
697
718
 
698
- if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
719
+ if (this.video.src && this.video.src !== window.location.href) {
720
+ this.originalSrc = this.video.src;
721
+ this.video.removeAttribute('src');
722
+ this.video.src = '';
699
723
  }
700
724
 
701
- saveOriginalSources() {
702
- const sources = this.video.querySelectorAll('source');
703
- this.originalSources = [];
725
+ this.hideNativePlayer();
704
726
 
705
- sources.forEach((source, index) => {
706
- if (source.src) {
707
- this.originalSources.push({
708
- element: source,
709
- src: source.src,
710
- type: source.type || 'video/mp4',
711
- quality: source.getAttribute('data-quality') || `quality-${index}`,
712
- index: index
713
- });
714
- }
715
- });
727
+ if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
728
+ }
716
729
 
717
- if (this.options.debug) console.log(`📁 Saved ${this.originalSources.length} sources originali:`, this.originalSources);
718
- }
730
+ saveOriginalSources() {
731
+ const sources = this.video.querySelectorAll('source');
732
+ this.originalSources = [];
733
+
734
+ sources.forEach((source, index) => {
735
+ if (source.src) {
736
+ this.originalSources.push({
737
+ element: source,
738
+ src: source.src,
739
+ type: source.type || 'video/mp4',
740
+ quality: source.getAttribute('data-quality') || `quality-${index}`,
741
+ index: index
742
+ });
743
+ }
744
+ });
719
745
 
720
- disableSources() {
721
- const sources = this.video.querySelectorAll('source');
722
- sources.forEach(source => {
723
- if (source.src) {
724
- source.removeAttribute('src');
725
- }
726
- });
727
- }
746
+ if (this.options.debug) console.log(`📁 Saved ${this.originalSources.length} sources originali:`, this.originalSources);
747
+ }
728
748
 
729
- restoreSourcesAsync() {
730
- setTimeout(() => {
731
- this.restoreSources();
732
- }, 200);
733
- }
749
+ disableSources() {
750
+ const sources = this.video.querySelectorAll('source');
751
+ sources.forEach(source => {
752
+ if (source.src) {
753
+ source.removeAttribute('src');
754
+ }
755
+ });
756
+ }
734
757
 
735
- async restoreSources() {
736
- try {
737
- // Check for adaptive streaming first
738
- let adaptiveSource = null;
739
-
740
- if (this.originalSrc) {
741
- adaptiveSource = this.originalSrc;
742
- } else if (this.originalSources.length > 0) {
743
- // Check if any source is adaptive
744
- const firstSource = this.originalSources[0];
745
- if (firstSource.src && this.detectStreamType(firstSource.src)) {
746
- adaptiveSource = firstSource.src;
747
- }
748
- }
758
+ restoreSourcesAsync() {
759
+ setTimeout(() => {
760
+ this.restoreSources();
761
+ }, 200);
762
+ }
749
763
 
750
- // Initialize adaptive streaming if detected
751
- if (adaptiveSource && this.options.adaptiveStreaming) {
752
- const adaptiveInitialized = await this.initializeAdaptiveStreaming(adaptiveSource);
753
- if (adaptiveInitialized) {
754
- if (this.options.debug) console.log('📡 Adaptive streaming initialized');
755
- return;
756
- }
764
+ async restoreSources() {
765
+ try {
766
+ // Check for adaptive streaming first
767
+ let adaptiveSource = null;
768
+
769
+ if (this.originalSrc) {
770
+ adaptiveSource = this.originalSrc;
771
+ } else if (this.originalSources.length > 0) {
772
+ // Check if any source is adaptive
773
+ const firstSource = this.originalSources[0];
774
+ if (firstSource.src && this.detectStreamType(firstSource.src)) {
775
+ adaptiveSource = firstSource.src;
757
776
  }
777
+ }
758
778
 
759
- // Fallback to traditional sources
760
- if (this.originalSrc) {
761
- this.video.src = this.originalSrc;
779
+ // Initialize adaptive streaming if detected
780
+ if (adaptiveSource && this.options.adaptiveStreaming) {
781
+ const adaptiveInitialized = await this.initializeAdaptiveStreaming(adaptiveSource);
782
+ if (adaptiveInitialized) {
783
+ if (this.options.debug) console.log('📡 Adaptive streaming initialized');
784
+ return;
762
785
  }
786
+ }
763
787
 
764
- this.originalSources.forEach(sourceData => {
765
- if (sourceData.element && sourceData.src) {
766
- sourceData.element.src = sourceData.src;
767
- }
768
- });
769
-
770
- this.qualities = this.originalSources.map(s => ({
771
- src: s.src,
772
- quality: s.quality,
773
- type: s.type
774
- }));
788
+ // Fallback to traditional sources
789
+ if (this.originalSrc) {
790
+ this.video.src = this.originalSrc;
791
+ }
775
792
 
776
- if (this.originalSrc && this.qualities.length === 0) {
777
- this.qualities.push({
778
- src: this.originalSrc,
779
- quality: 'default',
780
- type: 'video/mp4'
781
- });
793
+ this.originalSources.forEach(sourceData => {
794
+ if (sourceData.element && sourceData.src) {
795
+ sourceData.element.src = sourceData.src;
782
796
  }
797
+ });
783
798
 
784
- if (this.qualities.length > 0) {
785
- this.video.load();
799
+ this.qualities = this.originalSources.map(s => ({
800
+ src: s.src,
801
+ quality: s.quality,
802
+ type: s.type
803
+ }));
786
804
 
787
- // CRITICAL: Re-initialize subtitles AFTER video.load() completes
788
- this.video.addEventListener('loadedmetadata', () => {
789
- setTimeout(() => {
790
- this.reinitializeSubtitles();
791
- if (this.options.debug) console.log('🔄 Subtitles re-initialized after video load');
792
- }, 300);
793
- }, { once: true });
794
- }
805
+ if (this.originalSrc && this.qualities.length === 0) {
806
+ this.qualities.push({
807
+ src: this.originalSrc,
808
+ quality: 'default',
809
+ type: 'video/mp4'
810
+ });
811
+ }
795
812
 
796
- if (this.options.debug) console.log('✅ Sources ripristinate:', this.qualities);
813
+ if (this.qualities.length > 0) {
814
+ this.video.load();
797
815
 
798
- } catch (error) {
799
- if (this.options.debug) console.error('❌ Errore ripristino sources:', error);
816
+ // CRITICAL: Re-initialize subtitles AFTER video.load() completes
817
+ this.video.addEventListener('loadedmetadata', () => {
818
+ setTimeout(() => {
819
+ this.reinitializeSubtitles();
820
+ if (this.options.debug) console.log('🔄 Subtitles re-initialized after video load');
821
+ }, 300);
822
+ }, { once: true });
800
823
  }
824
+
825
+ if (this.options.debug) console.log('✅ Sources ripristinate:', this.qualities);
826
+
827
+ } catch (error) {
828
+ if (this.options.debug) console.error('❌ Errore ripristino sources:', error);
801
829
  }
830
+ }
802
831
 
803
832
  reinitializeSubtitles() {
804
833
  if (this.options.debug) console.log('🔄 Re-initializing subtitles...');
@@ -844,630 +873,706 @@ getDefaultSubtitleTrack() {
844
873
  return -1;
845
874
  }
846
875
 
847
- markPlayerReady() {
848
- setTimeout(() => {
849
- this.isPlayerReady = true;
850
- if (this.container) {
851
- this.container.classList.add('player-initialized');
852
- }
876
+ markPlayerReady() {
877
+ setTimeout(() => {
878
+ this.isPlayerReady = true;
879
+ if (this.container) {
880
+ this.container.classList.add('player-initialized');
881
+ }
853
882
 
854
- if (this.video) {
855
- this.video.style.visibility = '';
856
- this.video.style.opacity = '';
857
- this.video.style.pointerEvents = '';
858
- }
883
+ if (this.video) {
884
+ this.video.style.visibility = '';
885
+ this.video.style.opacity = '';
886
+ this.video.style.pointerEvents = '';
887
+ }
859
888
 
860
- // INITIALIZE AUTO-HIDE AFTER EVERYTHING IS READY
861
- setTimeout(() => {
862
- if (this.options.autoHide && !this.autoHideInitialized) {
863
- this.initAutoHide();
864
- }
889
+ // INITIALIZE AUTO-HIDE AFTER EVERYTHING IS READY
890
+ setTimeout(() => {
891
+ if (this.options.autoHide && !this.autoHideInitialized) {
892
+ this.initAutoHide();
893
+ }
865
894
 
866
- // Fix: Apply default quality (auto or specific)
867
- if (this.selectedQuality && this.qualities && this.qualities.length > 0) {
868
- if (this.options.debug) console.log(`🎯 Applying defaultQuality: "${this.selectedQuality}"`);
895
+ // Fix: Apply default quality (auto or specific)
896
+ if (this.selectedQuality && this.qualities && this.qualities.length > 0) {
897
+ if (this.options.debug) console.log(`🎯 Applying defaultQuality: "${this.selectedQuality}"`);
869
898
 
870
- if (this.selectedQuality === 'auto') {
871
- this.enableAutoQuality();
899
+ if (this.selectedQuality === 'auto') {
900
+ this.enableAutoQuality();
901
+ } else {
902
+ // Check if requested quality is available
903
+ const requestedQuality = this.qualities.find(q => q.quality === this.selectedQuality);
904
+ if (requestedQuality) {
905
+ if (this.options.debug) console.log(`✅ Quality "${this.selectedQuality}" available`);
906
+ this.setQuality(this.selectedQuality);
872
907
  } else {
873
- // Check if requested quality is available
874
- const requestedQuality = this.qualities.find(q => q.quality === this.selectedQuality);
875
- if (requestedQuality) {
876
- if (this.options.debug) console.log(`✅ Quality "${this.selectedQuality}" available`);
877
- this.setQuality(this.selectedQuality);
878
- } else {
879
- if (this.options.debug) console.warn(`⚠️ Quality "${this.selectedQuality}" not available - fallback to auto`);
880
- if (this.options.debug) console.log('📋 Available qualities:', this.qualities.map(q => q.quality));
881
- this.enableAutoQuality();
882
- }
908
+ if (this.options.debug) console.warn(`⚠️ Quality "${this.selectedQuality}" not available - fallback to auto`);
909
+ if (this.options.debug) console.log('📋 Available qualities:', this.qualities.map(q => q.quality));
910
+ this.enableAutoQuality();
883
911
  }
884
912
  }
913
+ }
885
914
 
886
- // Autoplay
887
- if (this.options.autoplay) {
888
- if (this.options.debug) console.log('🎬 Autoplay enabled');
889
- setTimeout(() => {
890
- this.video.play().catch(error => {
891
- if (this.options.debug) console.warn('⚠️ Autoplay blocked:', error);
892
- });
893
- }, 100);
894
- }
895
- }, 200);
915
+ // Autoplay
916
+ if (this.options.autoplay) {
917
+ if (this.options.debug) console.log('🎬 Autoplay enabled');
918
+ setTimeout(() => {
919
+ this.video.play().catch(error => {
920
+ if (this.options.debug) console.warn('⚠️ Autoplay blocked:', error);
921
+ });
922
+ }, 100);
923
+ }
924
+ }, 200);
896
925
 
897
- }, 100);
898
- }
926
+ }, 100);
927
+ }
899
928
 
900
- createPlayerStructure() {
901
- let wrapper = this.video.closest('.video-wrapper');
902
- if (!wrapper) {
903
- wrapper = document.createElement('div');
904
- wrapper.className = 'video-wrapper';
905
- this.video.parentNode.insertBefore(wrapper, this.video);
906
- wrapper.appendChild(this.video);
907
- }
929
+ createPlayerStructure() {
930
+ let wrapper = this.video.closest('.video-wrapper');
931
+ if (!wrapper) {
932
+ wrapper = document.createElement('div');
933
+ wrapper.className = 'video-wrapper';
934
+ this.video.parentNode.insertBefore(wrapper, this.video);
935
+ wrapper.appendChild(this.video);
936
+ }
908
937
 
909
- this.container = wrapper;
938
+ this.container = wrapper;
910
939
 
911
- this.createInitialLoading();
912
- this.createLoadingOverlay();
913
- this.collectVideoQualities();
914
- this.createControls();
915
- this.createBrandLogo();
916
- this.detectPlaylist();
940
+ this.createInitialLoading();
941
+ this.createLoadingOverlay();
942
+ this.collectVideoQualities();
943
+ this.createControls();
944
+ this.createBrandLogo();
945
+ this.detectPlaylist();
917
946
 
918
- if (this.options.showTitleOverlay) {
919
- this.createTitleOverlay();
920
- }
947
+ if (this.options.showTitleOverlay) {
948
+ this.createTitleOverlay();
921
949
  }
950
+ }
922
951
 
923
- createInitialLoading() {
924
- const initialLoader = document.createElement('div');
925
- initialLoader.className = 'initial-loading';
926
- initialLoader.innerHTML = '<div class="loading-spinner"></div>';
927
- this.container.appendChild(initialLoader);
928
- this.initialLoading = initialLoader;
929
- }
952
+ createInitialLoading() {
953
+ const initialLoader = document.createElement('div');
954
+ initialLoader.className = 'initial-loading';
955
+ initialLoader.innerHTML = '<div class="loading-spinner"></div>';
956
+ this.container.appendChild(initialLoader);
957
+ this.initialLoading = initialLoader;
958
+ }
930
959
 
931
- collectVideoQualities() {
932
- if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
933
- }
960
+ collectVideoQualities() {
961
+ if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
962
+ }
934
963
 
935
- createLoadingOverlay() {
936
- const overlay = document.createElement('div');
937
- overlay.className = 'loading-overlay';
938
- overlay.id = 'loadingOverlay-' + this.getUniqueId();
939
- overlay.innerHTML = '<div class="loading-spinner"></div>';
940
- this.container.appendChild(overlay);
941
- this.loadingOverlay = overlay;
942
- }
964
+ createLoadingOverlay() {
965
+ const overlay = document.createElement('div');
966
+ overlay.className = 'loading-overlay';
967
+ overlay.id = 'loadingOverlay-' + this.getUniqueId();
968
+ overlay.innerHTML = '<div class="loading-spinner"></div>';
969
+ this.container.appendChild(overlay);
970
+ this.loadingOverlay = overlay;
971
+ }
943
972
 
944
- createTitleOverlay() {
945
- const overlay = document.createElement('div');
946
- overlay.className = 'title-overlay';
947
- overlay.id = 'titleOverlay-' + this.getUniqueId();
973
+ createTitleOverlay() {
974
+ const overlay = document.createElement('div');
975
+ overlay.className = 'title-overlay';
976
+ overlay.id = 'titleOverlay-' + this.getUniqueId();
948
977
 
949
- const titleText = document.createElement('h2');
950
- titleText.className = 'title-text';
951
- titleText.textContent = this.options.videoTitle || '';
978
+ const titleText = document.createElement('h2');
979
+ titleText.className = 'title-text';
980
+ titleText.textContent = this.options.videoTitle || '';
952
981
 
953
- overlay.appendChild(titleText);
982
+ overlay.appendChild(titleText);
954
983
 
955
- if (this.controls) {
956
- this.container.insertBefore(overlay, this.controls);
957
- } else {
958
- this.container.appendChild(overlay);
959
- }
984
+ if (this.controls) {
985
+ this.container.insertBefore(overlay, this.controls);
986
+ } else {
987
+ this.container.appendChild(overlay);
988
+ }
960
989
 
961
- this.titleOverlay = overlay;
990
+ this.titleOverlay = overlay;
962
991
 
963
- if (this.options.persistentTitle && this.options.videoTitle) {
964
- this.showTitleOverlay();
965
- }
992
+ if (this.options.persistentTitle && this.options.videoTitle) {
993
+ this.showTitleOverlay();
966
994
  }
995
+ }
967
996
 
968
- updateTooltips() {
969
- if (!this.controls) return;
997
+ updateTooltips() {
998
+ if (!this.controls) return;
970
999
 
971
- try {
972
- this.controls.querySelectorAll('[data-tooltip]').forEach(element => {
973
- const key = element.getAttribute('data-tooltip');
974
- element.title = this.t(key);
975
- });
1000
+ try {
1001
+ this.controls.querySelectorAll('[data-tooltip]').forEach(element => {
1002
+ const key = element.getAttribute('data-tooltip');
1003
+ element.title = this.t(key);
1004
+ });
976
1005
 
977
- const autoOption = this.controls.querySelector('.quality-option[data-quality="auto"]');
978
- if (autoOption) {
979
- autoOption.textContent = this.t('auto');
980
- }
981
- } catch (error) {
982
- if (this.options.debug) console.warn('Errore aggiornamento tooltip:', error);
1006
+ const autoOption = this.controls.querySelector('.quality-option[data-quality="auto"]');
1007
+ if (autoOption) {
1008
+ autoOption.textContent = this.t('auto');
983
1009
  }
1010
+ } catch (error) {
1011
+ if (this.options.debug) console.warn('Errore aggiornamento tooltip:', error);
984
1012
  }
1013
+ }
985
1014
 
986
- setLanguage(lang) {
987
- if (this.isI18nAvailable()) {
988
- try {
989
- if (VideoPlayerTranslations.setLanguage(lang)) {
990
- this.updateTooltips();
991
- return true;
992
- }
993
- } catch (error) {
994
- if (this.options.debug) console.warn('Errore cambio lingua:', error);
1015
+ setLanguage(lang) {
1016
+ if (this.isI18nAvailable()) {
1017
+ try {
1018
+ if (VideoPlayerTranslations.setLanguage(lang)) {
1019
+ this.updateTooltips();
1020
+ return true;
995
1021
  }
1022
+ } catch (error) {
1023
+ if (this.options.debug) console.warn('Errore cambio lingua:', error);
996
1024
  }
997
- return false;
998
1025
  }
1026
+ return false;
1027
+ }
999
1028
 
1000
- setVideoTitle(title) {
1001
- this.options.videoTitle = title || '';
1029
+ setVideoTitle(title) {
1030
+ this.options.videoTitle = title || '';
1002
1031
 
1003
- if (this.titleOverlay) {
1004
- const titleElement = this.titleOverlay.querySelector('.title-text');
1005
- if (titleElement) {
1006
- titleElement.textContent = this.options.videoTitle;
1007
- }
1032
+ if (this.titleOverlay) {
1033
+ const titleElement = this.titleOverlay.querySelector('.title-text');
1034
+ if (titleElement) {
1035
+ titleElement.textContent = this.options.videoTitle;
1036
+ }
1008
1037
 
1009
- if (title) {
1010
- this.showTitleOverlay();
1038
+ if (title) {
1039
+ this.showTitleOverlay();
1011
1040
 
1012
- if (!this.options.persistentTitle) {
1013
- this.clearTitleTimeout();
1014
- this.titleTimeout = setTimeout(() => {
1015
- this.hideTitleOverlay();
1016
- }, 3000);
1017
- }
1041
+ if (!this.options.persistentTitle) {
1042
+ this.clearTitleTimeout();
1043
+ this.titleTimeout = setTimeout(() => {
1044
+ this.hideTitleOverlay();
1045
+ }, 3000);
1018
1046
  }
1019
1047
  }
1020
-
1021
- return this;
1022
1048
  }
1023
1049
 
1024
- getVideoTitle() {
1025
- return this.options.videoTitle;
1026
- }
1050
+ return this;
1051
+ }
1052
+
1053
+ getVideoTitle() {
1054
+ return this.options.videoTitle;
1055
+ }
1027
1056
 
1028
- setPersistentTitle(persistent) {
1029
- this.options.persistentTitle = persistent;
1057
+ setPersistentTitle(persistent) {
1058
+ this.options.persistentTitle = persistent;
1030
1059
 
1031
- if (this.titleOverlay && this.options.videoTitle) {
1032
- if (persistent) {
1033
- this.showTitleOverlay();
1060
+ if (this.titleOverlay && this.options.videoTitle) {
1061
+ if (persistent) {
1062
+ this.showTitleOverlay();
1063
+ this.clearTitleTimeout();
1064
+ } else {
1065
+ this.titleOverlay.classList.remove('persistent');
1066
+ if (this.titleOverlay.classList.contains('show')) {
1034
1067
  this.clearTitleTimeout();
1035
- } else {
1036
- this.titleOverlay.classList.remove('persistent');
1037
- if (this.titleOverlay.classList.contains('show')) {
1038
- this.clearTitleTimeout();
1039
- this.titleTimeout = setTimeout(() => {
1040
- this.hideTitleOverlay();
1041
- }, 3000);
1042
- }
1068
+ this.titleTimeout = setTimeout(() => {
1069
+ this.hideTitleOverlay();
1070
+ }, 3000);
1043
1071
  }
1044
1072
  }
1045
-
1046
- return this;
1047
1073
  }
1048
1074
 
1049
- enableTitleOverlay() {
1050
- if (!this.titleOverlay && !this.options.showTitleOverlay) {
1051
- this.options.showTitleOverlay = true;
1052
- this.createTitleOverlay();
1053
- }
1054
- return this;
1055
- }
1075
+ return this;
1076
+ }
1056
1077
 
1057
- disableTitleOverlay() {
1058
- if (this.titleOverlay) {
1059
- this.titleOverlay.remove();
1060
- this.titleOverlay = null;
1061
- }
1062
- this.options.showTitleOverlay = false;
1063
- return this;
1078
+ enableTitleOverlay() {
1079
+ if (!this.titleOverlay && !this.options.showTitleOverlay) {
1080
+ this.options.showTitleOverlay = true;
1081
+ this.createTitleOverlay();
1064
1082
  }
1083
+ return this;
1084
+ }
1065
1085
 
1066
- getUniqueId() {
1067
- return Math.random().toString(36).substr(2, 9);
1086
+ disableTitleOverlay() {
1087
+ if (this.titleOverlay) {
1088
+ this.titleOverlay.remove();
1089
+ this.titleOverlay = null;
1068
1090
  }
1091
+ this.options.showTitleOverlay = false;
1092
+ return this;
1093
+ }
1094
+
1095
+ getUniqueId() {
1096
+ return Math.random().toString(36).substr(2, 9);
1097
+ }
1069
1098
 
1070
- initializeElements() {
1071
- this.progressContainer = this.controls?.querySelector('.progress-container');
1072
- this.progressFilled = this.controls?.querySelector('.progress-filled');
1073
- this.progressBuffer = this.controls?.querySelector('.progress-buffer');
1074
- this.progressHandle = this.controls?.querySelector('.progress-handle');
1075
- this.seekTooltip = this.controls?.querySelector('.seek-tooltip');
1099
+ initializeElements() {
1100
+ this.progressContainer = this.controls?.querySelector('.progress-container');
1101
+ this.progressFilled = this.controls?.querySelector('.progress-filled');
1102
+ this.progressBuffer = this.controls?.querySelector('.progress-buffer');
1103
+ this.progressHandle = this.controls?.querySelector('.progress-handle');
1104
+ this.seekTooltip = this.controls?.querySelector('.seek-tooltip');
1105
+
1106
+ this.playPauseBtn = this.controls?.querySelector('.play-pause-btn');
1107
+ this.muteBtn = this.controls?.querySelector('.mute-btn');
1108
+ this.fullscreenBtn = this.controls?.querySelector('.fullscreen-btn');
1109
+ this.speedBtn = this.controls?.querySelector('.speed-btn');
1110
+ this.qualityBtn = this.controls?.querySelector('.quality-btn');
1111
+ this.pipBtn = this.controls?.querySelector('.pip-btn');
1112
+ this.subtitlesBtn = this.controls?.querySelector('.subtitles-btn');
1113
+ this.playlistPrevBtn = this.controls?.querySelector('.playlist-prev-btn');
1114
+ this.playlistNextBtn = this.controls?.querySelector('.playlist-next-btn');
1115
+
1116
+ this.playIcon = this.controls?.querySelector('.play-icon');
1117
+ this.pauseIcon = this.controls?.querySelector('.pause-icon');
1118
+ this.volumeIcon = this.controls?.querySelector('.volume-icon');
1119
+ this.muteIcon = this.controls?.querySelector('.mute-icon');
1120
+ this.fullscreenIcon = this.controls?.querySelector('.fullscreen-icon');
1121
+ this.exitFullscreenIcon = this.controls?.querySelector('.exit-fullscreen-icon');
1122
+ this.pipIcon = this.controls?.querySelector('.pip-icon');
1123
+ this.pipExitIcon = this.controls?.querySelector('.pip-exit-icon');
1124
+
1125
+ this.volumeSlider = this.controls?.querySelector('.volume-slider');
1126
+ this.currentTimeEl = this.controls?.querySelector('.current-time');
1127
+ this.durationEl = this.controls?.querySelector('.duration');
1128
+ this.speedMenu = this.controls?.querySelector('.speed-menu');
1129
+ this.qualityMenu = this.controls?.querySelector('.quality-menu');
1130
+ this.subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
1131
+ }
1076
1132
 
1077
- this.playPauseBtn = this.controls?.querySelector('.play-pause-btn');
1078
- this.muteBtn = this.controls?.querySelector('.mute-btn');
1079
- this.fullscreenBtn = this.controls?.querySelector('.fullscreen-btn');
1080
- this.speedBtn = this.controls?.querySelector('.speed-btn');
1081
- this.qualityBtn = this.controls?.querySelector('.quality-btn');
1082
- this.pipBtn = this.controls?.querySelector('.pip-btn');
1083
- this.subtitlesBtn = this.controls?.querySelector('.subtitles-btn');
1084
- this.playlistPrevBtn = this.controls?.querySelector('.playlist-prev-btn');
1085
- this.playlistNextBtn = this.controls?.querySelector('.playlist-next-btn');
1133
+ // Generic method to close all active menus (works with plugins too)
1134
+ closeAllMenus() {
1135
+ // Find all elements with class ending in '-menu' that have 'active' class
1136
+ const allMenus = this.controls?.querySelectorAll('[class*="-menu"].active');
1137
+ allMenus?.forEach(menu => {
1138
+ menu.classList.remove('active');
1139
+ });
1086
1140
 
1087
- this.playIcon = this.controls?.querySelector('.play-icon');
1088
- this.pauseIcon = this.controls?.querySelector('.pause-icon');
1089
- this.volumeIcon = this.controls?.querySelector('.volume-icon');
1090
- this.muteIcon = this.controls?.querySelector('.mute-icon');
1091
- this.fullscreenIcon = this.controls?.querySelector('.fullscreen-icon');
1092
- this.exitFullscreenIcon = this.controls?.querySelector('.exit-fullscreen-icon');
1093
- this.pipIcon = this.controls?.querySelector('.pip-icon');
1094
- this.pipExitIcon = this.controls?.querySelector('.pip-exit-icon');
1141
+ // Remove active state from all control buttons
1142
+ const allButtons = this.controls?.querySelectorAll('.control-btn.active');
1143
+ allButtons?.forEach(btn => {
1144
+ btn.classList.remove('active');
1145
+ });
1146
+ }
1095
1147
 
1096
- this.volumeSlider = this.controls?.querySelector('.volume-slider');
1097
- this.currentTimeEl = this.controls?.querySelector('.current-time');
1098
- this.durationEl = this.controls?.querySelector('.duration');
1099
- this.speedMenu = this.controls?.querySelector('.speed-menu');
1100
- this.qualityMenu = this.controls?.querySelector('.quality-menu');
1101
- this.subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
1102
- }
1148
+ // Generic menu toggle setup (works with core menus and plugin menus)
1149
+ setupMenuToggles() {
1150
+ // Delegate click events to control bar for any button with associated menu
1151
+ if (this.controls) {
1152
+ this.controls.addEventListener('click', (e) => {
1153
+ // Find if clicked element is a control button or inside one
1154
+ const button = e.target.closest('.control-btn');
1155
+
1156
+ if (!button) return;
1157
+
1158
+ // Get button classes to find associated menu
1159
+ const buttonClasses = button.className.split(' ');
1160
+ let menuClass = null;
1161
+
1162
+ // Find if this button has an associated menu (e.g., speed-btn -> speed-menu)
1163
+ for (const cls of buttonClasses) {
1164
+ if (cls.endsWith('-btn')) {
1165
+ const menuName = cls.replace('-btn', '-menu');
1166
+ const menu = this.controls.querySelector('.' + menuName);
1167
+ if (menu) {
1168
+ menuClass = menuName;
1169
+ break;
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ if (!menuClass) return;
1103
1175
 
1104
- updateVolumeSliderVisual() {
1105
- if (!this.video || !this.container) return;
1176
+ e.stopPropagation();
1106
1177
 
1107
- const volume = this.video.muted ? 0 : this.video.volume;
1108
- const percentage = Math.round(volume * 100);
1178
+ // Get the menu element
1179
+ const menu = this.controls.querySelector('.' + menuClass);
1180
+ const isOpen = menu.classList.contains('active');
1109
1181
 
1110
- this.container.style.setProperty('--player-volume-fill', percentage + '%');
1182
+ // Close all menus first
1183
+ this.closeAllMenus();
1111
1184
 
1112
- if (this.volumeSlider) {
1113
- this.volumeSlider.value = percentage;
1114
- }
1185
+ // If menu was closed, open it
1186
+ if (!isOpen) {
1187
+ menu.classList.add('active');
1188
+ button.classList.add('active');
1189
+ }
1190
+ });
1115
1191
  }
1116
1192
 
1117
- createVolumeTooltip() {
1118
- const volumeContainer = this.controls?.querySelector('.volume-container');
1119
- if (!volumeContainer || volumeContainer.querySelector('.volume-tooltip')) {
1120
- return; // Tooltip already present
1193
+ // Close menus when clicking outside controls
1194
+ document.addEventListener('click', (e) => {
1195
+ if (!this.controls?.contains(e.target)) {
1196
+ this.closeAllMenus();
1121
1197
  }
1198
+ });
1199
+ }
1122
1200
 
1123
- const tooltip = document.createElement('div');
1124
- tooltip.className = 'volume-tooltip';
1125
- tooltip.textContent = '50%';
1126
- volumeContainer.appendChild(tooltip);
1201
+ updateVolumeSliderVisual() {
1202
+ if (!this.video || !this.container) return;
1127
1203
 
1128
- this.volumeTooltip = tooltip;
1204
+ const volume = this.video.muted ? 0 : this.video.volume;
1205
+ const percentage = Math.round(volume * 100);
1129
1206
 
1130
- if (this.options.debug) {
1131
- console.log('Dynamic volume tooltip created');
1132
- }
1207
+ this.container.style.setProperty('--player-volume-fill', percentage + '%');
1208
+
1209
+ if (this.volumeSlider) {
1210
+ this.volumeSlider.value = percentage;
1133
1211
  }
1212
+ }
1134
1213
 
1135
- updateVolumeTooltip() {
1136
- if (!this.volumeTooltip || !this.video) return;
1214
+ createVolumeTooltip() {
1215
+ const volumeContainer = this.controls?.querySelector('.volume-container');
1216
+ if (!volumeContainer || volumeContainer.querySelector('.volume-tooltip')) {
1217
+ return; // Tooltip already present
1218
+ }
1137
1219
 
1138
- const volume = Math.round(this.video.volume * 100);
1139
- this.volumeTooltip.textContent = volume + '%';
1220
+ const tooltip = document.createElement('div');
1221
+ tooltip.className = 'volume-tooltip';
1222
+ tooltip.textContent = '50%';
1223
+ volumeContainer.appendChild(tooltip);
1140
1224
 
1141
- // Aggiorna la posizione del tooltip
1142
- this.updateVolumeTooltipPosition(this.video.volume);
1225
+ this.volumeTooltip = tooltip;
1143
1226
 
1144
- if (this.options.debug) {
1145
- console.log('Volume tooltip updated:', volume + '%');
1146
- }
1227
+ if (this.options.debug) {
1228
+ console.log('Dynamic volume tooltip created');
1147
1229
  }
1230
+ }
1148
1231
 
1149
- updateVolumeTooltipPosition(volumeValue = null) {
1150
- if (!this.volumeTooltip || !this.video) return;
1232
+ updateVolumeTooltip() {
1233
+ if (!this.volumeTooltip || !this.video) return;
1151
1234
 
1152
- const volumeSlider = this.controls?.querySelector('.volume-slider');
1153
- if (!volumeSlider) return;
1235
+ const volume = Math.round(this.video.volume * 100);
1236
+ this.volumeTooltip.textContent = volume + '%';
1154
1237
 
1155
- // If no volume provided, use current volume
1156
- if (volumeValue === null) {
1157
- volumeValue = this.video.volume;
1158
- }
1238
+ // Aggiorna la posizione del tooltip
1239
+ this.updateVolumeTooltipPosition(this.video.volume);
1159
1240
 
1160
- // Calcola la posizione esatta del thumb
1161
- const sliderRect = volumeSlider.getBoundingClientRect();
1162
- const sliderWidth = sliderRect.width;
1241
+ if (this.options.debug) {
1242
+ console.log('Volume tooltip updated:', volume + '%');
1243
+ }
1244
+ }
1163
1245
 
1164
- // Thumb size is typically 14px (as defined in CSS)
1165
- const thumbSize = 14; // var(--player-volume-handle-size)
1246
+ updateVolumeTooltipPosition(volumeValue = null) {
1247
+ if (!this.volumeTooltip || !this.video) return;
1166
1248
 
1167
- // Calcola la posizione del centro del thumb
1168
- // Il thumb si muove da thumbSize/2 a (sliderWidth - thumbSize/2)
1169
- const availableWidth = sliderWidth - thumbSize;
1170
- const thumbCenterPosition = (thumbSize / 2) + (availableWidth * volumeValue);
1249
+ const volumeSlider = this.controls?.querySelector('.volume-slider');
1250
+ if (!volumeSlider) return;
1171
1251
 
1172
- // Converti in percentuale relativa al container dello slider
1173
- const percentage = (thumbCenterPosition / sliderWidth) * 100;
1252
+ // If no volume provided, use current volume
1253
+ if (volumeValue === null) {
1254
+ volumeValue = this.video.volume;
1255
+ }
1174
1256
 
1175
- // Posiziona il tooltip
1176
- this.volumeTooltip.style.left = percentage + '%';
1257
+ // Calcola la posizione esatta del thumb
1258
+ const sliderRect = volumeSlider.getBoundingClientRect();
1259
+ const sliderWidth = sliderRect.width;
1177
1260
 
1178
- if (this.options.debug) {
1179
- console.log('Volume tooltip position updated:', {
1180
- volumeValue: volumeValue,
1181
- percentage: percentage + '%',
1182
- thumbCenter: thumbCenterPosition,
1183
- sliderWidth: sliderWidth
1184
- });
1185
- }
1186
- }
1261
+ // Thumb size is typically 14px (as defined in CSS)
1262
+ const thumbSize = 14; // var(--player-volume-handle-size)
1187
1263
 
1188
- initVolumeTooltip() {
1189
- this.createVolumeTooltip();
1190
- this.setupVolumeTooltipEvents();
1264
+ // Calcola la posizione del centro del thumb
1265
+ // Il thumb si muove da thumbSize/2 a (sliderWidth - thumbSize/2)
1266
+ const availableWidth = sliderWidth - thumbSize;
1267
+ const thumbCenterPosition = (thumbSize / 2) + (availableWidth * volumeValue);
1191
1268
 
1192
- // Set initial position immediately
1193
- setTimeout(() => {
1194
- if (this.volumeTooltip && this.video) {
1195
- this.updateVolumeTooltipPosition(this.video.volume);
1196
- this.updateVolumeTooltip();
1197
- }
1198
- }, 50); // Shorter delay for faster initialization
1269
+ // Converti in percentuale relativa al container dello slider
1270
+ const percentage = (thumbCenterPosition / sliderWidth) * 100;
1271
+
1272
+ // Posiziona il tooltip
1273
+ this.volumeTooltip.style.left = percentage + '%';
1274
+
1275
+ if (this.options.debug) {
1276
+ console.log('Volume tooltip position updated:', {
1277
+ volumeValue: volumeValue,
1278
+ percentage: percentage + '%',
1279
+ thumbCenter: thumbCenterPosition,
1280
+ sliderWidth: sliderWidth
1281
+ });
1199
1282
  }
1283
+ }
1200
1284
 
1201
- updateVolumeSliderVisualWithTooltip() {
1202
- const volumeSlider = this.controls?.querySelector('.volume-slider');
1203
- if (!volumeSlider || !this.video) return;
1285
+ initVolumeTooltip() {
1286
+ this.createVolumeTooltip();
1204
1287
 
1205
- const volume = this.video.volume || 0;
1206
- const percentage = Math.round(volume * 100);
1288
+ // Set initial position immediately
1289
+ setTimeout(() => {
1290
+ if (this.volumeTooltip && this.video) {
1291
+ this.updateVolumeTooltipPosition(this.video.volume);
1292
+ this.updateVolumeTooltip();
1293
+ }
1294
+ }, 50); // Shorter delay for faster initialization
1295
+ }
1207
1296
 
1208
- volumeSlider.value = volume;
1297
+ updateVolumeSliderVisualWithTooltip() {
1298
+ const volumeSlider = this.controls?.querySelector('.volume-slider');
1299
+ if (!volumeSlider || !this.video) return;
1209
1300
 
1210
- // Update CSS custom property per il riempimento visuale
1211
- const volumeFillPercentage = percentage + '%';
1212
- volumeSlider.style.setProperty('--player-volume-fill', volumeFillPercentage);
1301
+ const volume = this.video.volume || 0;
1302
+ const percentage = Math.round(volume * 100);
1213
1303
 
1214
- // Aggiorna anche il tooltip se presente (testo e posizione)
1215
- this.updateVolumeTooltip();
1304
+ volumeSlider.value = volume;
1216
1305
 
1217
- if (this.options.debug) {
1218
- console.log('Volume slider aggiornato:', {
1219
- volume: volume,
1220
- percentage: percentage,
1221
- fillPercentage: volumeFillPercentage
1222
- });
1223
- }
1306
+ // Update CSS custom property per il riempimento visuale
1307
+ const volumeFillPercentage = percentage + '%';
1308
+ volumeSlider.style.setProperty('--player-volume-fill', volumeFillPercentage);
1309
+
1310
+ // Aggiorna anche il tooltip se presente (testo e posizione)
1311
+ this.updateVolumeTooltip();
1312
+
1313
+ if (this.options.debug) {
1314
+ console.log('Volume slider aggiornato:', {
1315
+ volume: volume,
1316
+ percentage: percentage,
1317
+ fillPercentage: volumeFillPercentage
1318
+ });
1224
1319
  }
1320
+ }
1225
1321
 
1226
- // Volume slider type: 'horizontal' or 'vertical'
1227
- setVolumeSliderOrientation(orientation) {
1228
- if (!['horizontal', 'vertical'].includes(orientation)) {
1229
- if (this.options.debug) console.warn('Invalid volume slider orientation:', orientation);
1322
+ /**
1323
+ * Set mobile volume slider visibility
1324
+ * @param {String} mode - 'show' (horizontal popup) or 'hide' (no slider on mobile)
1325
+ * @returns {Object} this
1326
+ */
1327
+ setMobileVolumeSlider(mode) {
1328
+ if (!['show', 'hide'].includes(mode)) {
1329
+ if (this.options.debug) console.warn('Invalid mobile volume slider mode:', mode);
1230
1330
  return this;
1231
1331
  }
1232
1332
 
1233
- this.options.volumeSlider = orientation;
1333
+ this.options.mobileVolumeSlider = mode;
1234
1334
  const volumeContainer = this.controls?.querySelector('.volume-container');
1235
1335
  if (volumeContainer) {
1236
- volumeContainer.setAttribute('data-orientation', orientation);
1336
+ // Set data attribute for CSS to use
1337
+ volumeContainer.setAttribute('data-mobile-slider', mode);
1338
+ if (this.options.debug) console.log('Mobile volume slider set to:', mode);
1237
1339
  }
1238
-
1239
- if (this.options.debug) console.log('Volume slider orientation set to:', orientation);
1240
1340
  return this;
1241
1341
  }
1242
1342
 
1243
- getVolumeSliderOrientation() {
1244
- return this.options.volumeSlider;
1343
+ /**
1344
+ * Get mobile volume slider mode
1345
+ * @returns {String} Current mobile volume slider mode
1346
+ */
1347
+ getMobileVolumeSlider() {
1348
+ return this.options.mobileVolumeSlider;
1245
1349
  }
1246
1350
 
1351
+ initVolumeTooltip() {
1247
1352
 
1248
- initVolumeTooltip() {
1249
-
1250
- this.createVolumeTooltip();
1353
+ this.createVolumeTooltip();
1251
1354
 
1252
- // Setup events
1253
- this.setupVolumeTooltipEvents();
1254
-
1255
- setTimeout(() => {
1256
- this.updateVolumeTooltip();
1257
- }, 200);
1355
+ setTimeout(() => {
1356
+ this.updateVolumeTooltip();
1357
+ }, 200);
1258
1358
 
1259
- if (this.options.debug) {
1260
- console.log('Dynamic volume tooltip inizializzato');
1261
- }
1359
+ if (this.options.debug) {
1360
+ console.log('Dynamic volume tooltip inizializzation');
1262
1361
  }
1362
+ }
1263
1363
 
1264
- setupSeekTooltip() {
1265
- if (!this.options.showSeekTooltip || !this.progressContainer || !this.seekTooltip) return;
1364
+ setupSeekTooltip() {
1365
+ if (!this.options.showSeekTooltip || !this.progressContainer || !this.seekTooltip) return;
1266
1366
 
1267
- this.progressContainer.addEventListener('mouseenter', () => {
1268
- if (this.seekTooltip) {
1269
- this.seekTooltip.classList.add('visible');
1270
- }
1271
- });
1367
+ this.progressContainer.addEventListener('mouseenter', () => {
1368
+ if (this.seekTooltip) {
1369
+ this.seekTooltip.classList.add('visible');
1370
+ }
1371
+ });
1272
1372
 
1273
- this.progressContainer.addEventListener('mouseleave', () => {
1274
- if (this.seekTooltip) {
1275
- this.seekTooltip.classList.remove('visible');
1276
- }
1277
- });
1373
+ this.progressContainer.addEventListener('mouseleave', () => {
1374
+ if (this.seekTooltip) {
1375
+ this.seekTooltip.classList.remove('visible');
1376
+ }
1377
+ });
1278
1378
 
1279
- this.progressContainer.addEventListener('mousemove', (e) => {
1280
- this.updateSeekTooltip(e);
1281
- });
1282
- }
1379
+ this.progressContainer.addEventListener('mousemove', (e) => {
1380
+ this.updateSeekTooltip(e);
1381
+ });
1382
+ }
1283
1383
 
1284
- updateSeekTooltip(e) {
1285
- if (!this.seekTooltip || !this.progressContainer || !this.video || !this.video.duration) return;
1384
+ updateSeekTooltip(e) {
1385
+ if (!this.seekTooltip || !this.progressContainer || !this.video || !this.video.duration) return;
1286
1386
 
1287
- const rect = this.progressContainer.getBoundingClientRect();
1288
- const clickX = e.clientX - rect.left;
1289
- const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1290
- const targetTime = percentage * this.video.duration;
1387
+ const rect = this.progressContainer.getBoundingClientRect();
1388
+ const clickX = e.clientX - rect.left;
1389
+ const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1390
+ const targetTime = percentage * this.video.duration;
1291
1391
 
1292
- this.seekTooltip.textContent = this.formatTime(targetTime);
1392
+ this.seekTooltip.textContent = this.formatTime(targetTime);
1293
1393
 
1294
- const tooltipRect = this.seekTooltip.getBoundingClientRect();
1295
- let leftPosition = clickX;
1394
+ const tooltipRect = this.seekTooltip.getBoundingClientRect();
1395
+ let leftPosition = clickX;
1296
1396
 
1297
- const tooltipWidth = tooltipRect.width || 50;
1298
- const containerWidth = rect.width;
1397
+ const tooltipWidth = tooltipRect.width || 50;
1398
+ const containerWidth = rect.width;
1299
1399
 
1300
- leftPosition = Math.max(tooltipWidth / 2, Math.min(containerWidth - tooltipWidth / 2, clickX));
1400
+ leftPosition = Math.max(tooltipWidth / 2, Math.min(containerWidth - tooltipWidth / 2, clickX));
1301
1401
 
1302
- this.seekTooltip.style.left = leftPosition + 'px';
1303
- }
1402
+ this.seekTooltip.style.left = leftPosition + 'px';
1403
+ }
1304
1404
 
1305
- play() {
1306
- if (!this.video || this.isChangingQuality) return;
1405
+ play() {
1406
+ if (!this.video || this.isChangingQuality) return;
1307
1407
 
1308
- this.video.play().catch(err => {
1309
- if (this.options.debug) console.log('Play failed:', err);
1310
- });
1408
+ this.video.play().catch(err => {
1409
+ if (this.options.debug) console.log('Play failed:', err);
1410
+ });
1311
1411
 
1312
- if (this.playIcon) this.playIcon.classList.add('hidden');
1313
- if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
1412
+ if (this.playIcon) this.playIcon.classList.add('hidden');
1413
+ if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
1314
1414
 
1315
- // Trigger event played
1316
- this.triggerEvent('played', {
1317
- currentTime: this.getCurrentTime(),
1318
- duration: this.getDuration()
1319
- });
1320
- }
1415
+ // Trigger event played
1416
+ this.triggerEvent('played', {
1417
+ currentTime: this.getCurrentTime(),
1418
+ duration: this.getDuration()
1419
+ });
1420
+ }
1321
1421
 
1322
- pause() {
1323
- if (!this.video) return;
1422
+ pause() {
1423
+ if (!this.video) return;
1324
1424
 
1325
- this.video.pause();
1326
- if (this.playIcon) this.playIcon.classList.remove('hidden');
1327
- if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
1425
+ this.video.pause();
1426
+ if (this.playIcon) this.playIcon.classList.remove('hidden');
1427
+ if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
1328
1428
 
1329
- // Trigger paused event
1330
- this.triggerEvent('paused', {
1331
- currentTime: this.getCurrentTime(),
1332
- duration: this.getDuration()
1333
- });
1334
- }
1429
+ // Trigger paused event
1430
+ this.triggerEvent('paused', {
1431
+ currentTime: this.getCurrentTime(),
1432
+ duration: this.getDuration()
1433
+ });
1434
+ }
1335
1435
 
1336
- updateVolume(value) {
1337
- if (!this.video) return;
1436
+ updateVolume(value) {
1437
+ if (!this.video) return;
1338
1438
 
1339
- const previousVolume = this.video.volume;
1340
- const previousMuted = this.video.muted;
1439
+ const previousVolume = this.video.volume;
1440
+ const previousMuted = this.video.muted;
1341
1441
 
1342
- this.video.volume = Math.max(0, Math.min(1, value / 100));
1343
- if (this.volumeSlider) this.volumeSlider.value = value;
1344
- this.updateMuteButton();
1345
- this.updateVolumeSliderVisual();
1346
- this.initVolumeTooltip();
1442
+ this.video.volume = Math.max(0, Math.min(1, value / 100));
1347
1443
 
1348
- // Triggers volumechange event if there is a significant change
1349
- if (Math.abs(previousVolume - this.video.volume) > 0.01 || previousMuted !== this.video.muted) {
1350
- this.triggerEvent('volumechange', {
1351
- volume: this.getVolume(),
1352
- muted: this.isMuted(),
1353
- previousVolume: previousVolume,
1354
- previousMuted: previousMuted
1355
- });
1356
- }
1444
+ if (this.video.volume > 0 && this.video.muted) {
1445
+ this.video.muted = false;
1357
1446
  }
1358
1447
 
1359
- changeVolume(delta) {
1360
- if (!this.video) return;
1448
+ if (this.volumeSlider) this.volumeSlider.value = value;
1449
+ this.updateMuteButton();
1450
+ this.updateVolumeSliderVisual();
1451
+ this.initVolumeTooltip();
1361
1452
 
1362
- const newVolume = Math.max(0, Math.min(1, this.video.volume + delta));
1363
- this.updateVolume(newVolume * 100);
1364
- this.updateVolumeSliderVisual();
1365
- this.initVolumeTooltip();
1453
+ // Triggers volumechange event if there is a significant change
1454
+ if (Math.abs(previousVolume - this.video.volume) > 0.01 || previousMuted !== this.video.muted) {
1455
+ this.triggerEvent('volumechange', {
1456
+ volume: this.getVolume(),
1457
+ muted: this.isMuted(),
1458
+ previousVolume: previousVolume,
1459
+ previousMuted: previousMuted
1460
+ });
1366
1461
  }
1462
+ }
1367
1463
 
1368
- updateProgress() {
1369
- if (!this.video || !this.progressFilled || !this.progressHandle || this.isUserSeeking) return;
1464
+ changeVolume(delta) {
1465
+ if (!this.video) return;
1370
1466
 
1371
- if (this.video.duration && !isNaN(this.video.duration)) {
1372
- const progress = (this.video.currentTime / this.video.duration) * 100;
1373
- this.progressFilled.style.width = progress + '%';
1374
- this.progressHandle.style.left = progress + '%';
1375
- }
1467
+ const newVolume = Math.max(0, Math.min(1, this.video.volume + delta));
1468
+ this.updateVolume(newVolume * 100);
1469
+ this.updateVolumeSliderVisual();
1470
+ this.initVolumeTooltip();
1471
+ }
1376
1472
 
1377
- this.updateTimeDisplay();
1473
+ updateProgress() {
1474
+ if (!this.video || !this.progressFilled || !this.progressHandle || this.isUserSeeking) return;
1378
1475
 
1379
- // Trigger timeupdate event (with throttling to avoid too many events)
1380
- if (!this.lastTimeUpdate || Date.now() - this.lastTimeUpdate > 250) {
1381
- this.triggerEvent('timeupdate', {
1382
- currentTime: this.getCurrentTime(),
1383
- duration: this.getDuration(),
1384
- progress: (this.getCurrentTime() / this.getDuration()) * 100 || 0
1385
- });
1386
- this.lastTimeUpdate = Date.now();
1387
- }
1476
+ if (this.video.duration && !isNaN(this.video.duration)) {
1477
+ const progress = (this.video.currentTime / this.video.duration) * 100;
1478
+ this.progressFilled.style.width = progress + '%';
1479
+ this.progressHandle.style.left = progress + '%';
1388
1480
  }
1389
1481
 
1390
- updateBuffer() {
1391
- if (!this.video || !this.progressBuffer) return;
1482
+ this.updateTimeDisplay();
1392
1483
 
1393
- try {
1394
- if (this.video.buffered && this.video.buffered.length > 0 && this.video.duration) {
1395
- const buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
1396
- this.progressBuffer.style.width = buffered + '%';
1397
- }
1398
- } catch (error) {
1399
- if (this.options.debug) console.log('Buffer update error (non-critical):', error);
1400
- }
1484
+ // Trigger timeupdate event (with throttling to avoid too many events)
1485
+ if (!this.lastTimeUpdate || Date.now() - this.lastTimeUpdate > 250) {
1486
+ this.triggerEvent('timeupdate', {
1487
+ currentTime: this.getCurrentTime(),
1488
+ duration: this.getDuration(),
1489
+ progress: (this.getCurrentTime() / this.getDuration()) * 100 || 0
1490
+ });
1491
+ this.lastTimeUpdate = Date.now();
1401
1492
  }
1493
+ }
1402
1494
 
1403
- startSeeking(e) {
1404
- if (this.isChangingQuality) return;
1405
-
1406
- this.isUserSeeking = true;
1407
- this.seek(e);
1408
- e.preventDefault();
1495
+ updateBuffer() {
1496
+ if (!this.video || !this.progressBuffer) return;
1409
1497
 
1410
- // Show controls during seeking
1411
- if (this.options.autoHide && this.autoHideInitialized) {
1412
- this.showControlsNow();
1413
- this.resetAutoHideTimer();
1498
+ try {
1499
+ if (this.video.buffered && this.video.buffered.length > 0 && this.video.duration) {
1500
+ const buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
1501
+ this.progressBuffer.style.width = buffered + '%';
1414
1502
  }
1503
+ } catch (error) {
1504
+ if (this.options.debug) console.log('Buffer update error (non-critical):', error);
1415
1505
  }
1506
+ }
1416
1507
 
1417
- continueSeeking(e) {
1418
- if (this.isUserSeeking && !this.isChangingQuality) {
1419
- this.seek(e);
1420
- }
1508
+ startSeeking(e) {
1509
+ if (this.isChangingQuality) return;
1510
+
1511
+ this.isUserSeeking = true;
1512
+ this.seek(e);
1513
+ e.preventDefault();
1514
+
1515
+ // Show controls during seeking
1516
+ if (this.options.autoHide && this.autoHideInitialized) {
1517
+ this.showControlsNow();
1518
+ this.resetAutoHideTimer();
1421
1519
  }
1520
+ }
1422
1521
 
1423
- endSeeking() {
1424
- this.isUserSeeking = false;
1522
+ continueSeeking(e) {
1523
+ if (this.isUserSeeking && !this.isChangingQuality) {
1524
+ this.seek(e);
1425
1525
  }
1526
+ }
1426
1527
 
1427
- seek(e) {
1428
- if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
1528
+ endSeeking() {
1529
+ this.isUserSeeking = false;
1530
+ }
1429
1531
 
1430
- const rect = this.progressContainer.getBoundingClientRect();
1431
- const clickX = e.clientX - rect.left;
1432
- const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1532
+ seek(e) {
1533
+ if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
1433
1534
 
1434
- if (this.video.duration && !isNaN(this.video.duration)) {
1435
- this.video.currentTime = percentage * this.video.duration;
1436
- const progress = percentage * 100;
1437
- this.progressFilled.style.width = progress + '%';
1438
- this.progressHandle.style.left = progress + '%';
1439
- }
1440
- }
1535
+ const rect = this.progressContainer.getBoundingClientRect();
1536
+ const clickX = e.clientX - rect.left;
1537
+ const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1441
1538
 
1442
- updateDuration() {
1443
- if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
1444
- this.durationEl.textContent = this.formatTime(this.video.duration);
1445
- }
1539
+ if (this.video.duration && !isNaN(this.video.duration)) {
1540
+ this.video.currentTime = percentage * this.video.duration;
1541
+ const progress = percentage * 100;
1542
+ this.progressFilled.style.width = progress + '%';
1543
+ this.progressHandle.style.left = progress + '%';
1446
1544
  }
1545
+ }
1447
1546
 
1448
- changeSpeed(e) {
1449
- if (!this.video || !e.target.classList.contains('speed-option') || this.isChangingQuality) return;
1547
+ updateDuration() {
1548
+ if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
1549
+ this.durationEl.textContent = this.formatTime(this.video.duration);
1550
+ }
1551
+ }
1450
1552
 
1451
- const speed = parseFloat(e.target.getAttribute('data-speed'));
1452
- if (speed && speed > 0) {
1453
- this.video.playbackRate = speed;
1454
- if (this.speedBtn) this.speedBtn.textContent = speed + 'x';
1553
+ changeSpeed(e) {
1554
+ if (!this.video || !e.target.classList.contains('speed-option') || this.isChangingQuality) return;
1455
1555
 
1456
- if (this.speedMenu) {
1457
- this.speedMenu.querySelectorAll('.speed-option').forEach(option => {
1458
- option.classList.remove('active');
1459
- });
1460
- e.target.classList.add('active');
1461
- }
1556
+ const speed = parseFloat(e.target.getAttribute('data-speed'));
1557
+ if (speed && speed > 0) {
1558
+ this.video.playbackRate = speed;
1559
+ if (this.speedBtn) this.speedBtn.textContent = speed + 'x';
1462
1560
 
1463
- // Trigger speedchange event
1464
- const previousSpeed = this.video.playbackRate;
1465
- this.triggerEvent('speedchange', {
1466
- speed: speed,
1467
- previousSpeed: previousSpeed
1561
+ if (this.speedMenu) {
1562
+ this.speedMenu.querySelectorAll('.speed-option').forEach(option => {
1563
+ option.classList.remove('active');
1468
1564
  });
1565
+ e.target.classList.add('active');
1469
1566
  }
1567
+
1568
+ // Trigger speedchange event
1569
+ const previousSpeed = this.video.playbackRate;
1570
+ this.triggerEvent('speedchange', {
1571
+ speed: speed,
1572
+ previousSpeed: previousSpeed
1573
+ });
1470
1574
  }
1575
+ }
1471
1576
 
1472
1577
  onVideoEnded() {
1473
1578
  if (this.playIcon) this.playIcon.classList.remove('hidden');
@@ -1493,217 +1598,270 @@ onVideoEnded() {
1493
1598
  });
1494
1599
  }
1495
1600
 
1496
- getCurrentTime() { return this.video ? this.video.currentTime || 0 : 0; }
1601
+ /**
1602
+ * Handle video loading errors (404, 503, network errors, etc.)
1603
+ * Triggers 'ended' event to allow proper cleanup and playlist continuation
1604
+ */
1605
+ onVideoError(error) {
1606
+ if (this.options.debug) {
1607
+ console.error('Video loading error detected:', {
1608
+ error: error,
1609
+ code: this.video?.error?.code,
1610
+ message: this.video?.error?.message,
1611
+ src: this.video?.currentSrc || this.video?.src
1612
+ });
1613
+ }
1614
+
1615
+ // Hide loading overlay
1616
+ this.hideLoading();
1617
+ if (this.initialLoading) {
1618
+ this.initialLoading.style.display = 'none';
1619
+ }
1497
1620
 
1498
- setCurrentTime(time) { if (this.video && typeof time === 'number' && time >= 0 && !this.isChangingQuality) { this.video.currentTime = time; } }
1621
+ // Remove quality-changing class if present
1622
+ if (this.video?.classList) {
1623
+ this.video.classList.remove('quality-changing');
1624
+ }
1499
1625
 
1500
- getDuration() { return this.video && this.video.duration ? this.video.duration : 0; }
1626
+ // Reset changing quality flag
1627
+ this.isChangingQuality = false;
1501
1628
 
1502
- getVolume() { return this.video ? this.video.volume || 0 : 0; }
1629
+ // Show controls to allow user interaction
1630
+ this.showControlsNow();
1503
1631
 
1504
- setVolume(volume) {
1505
- if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
1506
- this.updateVolume(volume * 100);
1507
- }
1632
+ // Optional: Show poster if available
1633
+ if (this.options.showPosterOnEnd && this.posterOverlay) {
1634
+ this.showPoster();
1508
1635
  }
1509
1636
 
1510
- isPaused() { return this.video ? this.video.paused : true; }
1511
-
1512
- isMuted() { return this.video ? this.video.muted : false; }
1637
+ // Trigger 'ended' event to allow proper cleanup
1638
+ // This allows playlist to continue or other error handling
1639
+ this.triggerEvent('ended', {
1640
+ currentTime: this.getCurrentTime(),
1641
+ duration: this.getDuration(),
1642
+ error: true,
1643
+ errorCode: this.video?.error?.code,
1644
+ errorMessage: this.video?.error?.message,
1645
+ playlistInfo: this.getPlaylistInfo()
1646
+ });
1513
1647
 
1514
- setMuted(muted) {
1515
- if (this.video && typeof muted === 'boolean') {
1516
- this.video.muted = muted;
1517
- this.updateMuteButton();
1518
- this.updateVolumeSliderVisual();
1519
- this.initVolumeTooltip();
1520
- }
1648
+ if (this.options.debug) {
1649
+ console.log('Video error handled - triggered ended event');
1521
1650
  }
1651
+ }
1522
1652
 
1523
- getPlaybackRate() { return this.video ? this.video.playbackRate || 1 : 1; }
1524
1653
 
1525
- setPlaybackRate(rate) { if (this.video && typeof rate === 'number' && rate > 0 && !this.isChangingQuality) { this.video.playbackRate = rate; if (this.speedBtn) this.speedBtn.textContent = rate + 'x'; } }
1654
+ getCurrentTime() { return this.video ? this.video.currentTime || 0 : 0; }
1526
1655
 
1527
- isPictureInPictureActive() { return document.pictureInPictureElement === this.video; }
1656
+ setCurrentTime(time) { if (this.video && typeof time === 'number' && time >= 0 && !this.isChangingQuality) { this.video.currentTime = time; } }
1528
1657
 
1529
- getCurrentLanguage() {
1530
- return this.isI18nAvailable() ?
1531
- VideoPlayerTranslations.getCurrentLanguage() : 'en';
1658
+ getDuration() { return this.video && this.video.duration ? this.video.duration : 0; }
1659
+
1660
+ getVolume() { return this.video ? this.video.volume || 0 : 0; }
1661
+
1662
+ setVolume(volume) {
1663
+ if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
1664
+ this.updateVolume(volume * 100);
1532
1665
  }
1666
+ }
1533
1667
 
1534
- getSupportedLanguages() {
1535
- return this.isI18nAvailable() ?
1536
- VideoPlayerTranslations.getSupportedLanguages() : ['en'];
1668
+ isPaused() { return this.video ? this.video.paused : true; }
1669
+
1670
+ isMuted() { return this.video ? this.video.muted : false; }
1671
+
1672
+ setMuted(muted) {
1673
+ if (this.video && typeof muted === 'boolean') {
1674
+ this.video.muted = muted;
1675
+ this.updateMuteButton();
1676
+ this.updateVolumeSliderVisual();
1677
+ this.initVolumeTooltip();
1537
1678
  }
1679
+ }
1538
1680
 
1539
- createBrandLogo() {
1540
- if (!this.options.brandLogoEnabled || !this.options.brandLogoUrl) return;
1681
+ getPlaybackRate() { return this.video ? this.video.playbackRate || 1 : 1; }
1541
1682
 
1542
- const controlsRight = this.controls?.querySelector('.controls-right');
1543
- if (!controlsRight) return;
1683
+ setPlaybackRate(rate) { if (this.video && typeof rate === 'number' && rate > 0 && !this.isChangingQuality) { this.video.playbackRate = rate; if (this.speedBtn) this.speedBtn.textContent = rate + 'x'; } }
1544
1684
 
1545
- // Create brand logo image
1546
- const logo = document.createElement('img');
1547
- logo.className = 'brand-logo';
1548
- logo.src = this.options.brandLogoUrl;
1549
- logo.alt = this.t('brand_logo');
1685
+ isPictureInPictureActive() { return document.pictureInPictureElement === this.video; }
1550
1686
 
1551
- // Handle loading error
1552
- logo.onerror = () => {
1553
- if (this.options.debug) console.warn('Brand logo failed to load:', this.options.brandLogoUrl);
1554
- logo.style.display = 'none';
1555
- };
1687
+ getCurrentLanguage() {
1688
+ return this.isI18nAvailable() ?
1689
+ VideoPlayerTranslations.getCurrentLanguage() : 'en';
1690
+ }
1556
1691
 
1557
- logo.onload = () => {
1558
- if (this.options.debug) console.log('Brand logo loaded successfully');
1559
- };
1692
+ getSupportedLanguages() {
1693
+ return this.isI18nAvailable() ?
1694
+ VideoPlayerTranslations.getSupportedLanguages() : ['en'];
1695
+ }
1560
1696
 
1561
- // Add click functionality if link URL is provided
1562
- if (this.options.brandLogoLinkUrl) {
1563
- logo.style.cursor = 'pointer';
1564
- logo.addEventListener('click', (e) => {
1565
- e.stopPropagation(); // Prevent video controls interference
1566
- window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1567
- if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1568
- });
1569
- } else {
1570
- logo.style.cursor = 'default';
1571
- }
1697
+ createBrandLogo() {
1698
+ if (!this.options.brandLogoEnabled || !this.options.brandLogoUrl) return;
1572
1699
 
1573
- // Position the brand logo at the right of the controlbar (at the left of the buttons)
1574
- controlsRight.insertBefore(logo, controlsRight.firstChild);
1700
+ const controlsRight = this.controls?.querySelector('.controls-right');
1701
+ if (!controlsRight) return;
1575
1702
 
1576
- if (this.options.debug) {
1577
- if (this.options.brandLogoLinkUrl) {
1578
- console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
1579
- } else {
1580
- console.log('Brand logo created (no link)');
1581
- }
1582
- }
1703
+ // Create brand logo image
1704
+ const logo = document.createElement('img');
1705
+ logo.className = 'brand-logo';
1706
+ logo.src = this.options.brandLogoUrl;
1707
+ logo.alt = this.t('brand_logo');
1708
+
1709
+ // Handle loading error
1710
+ logo.onerror = () => {
1711
+ if (this.options.debug) console.warn('Brand logo failed to load:', this.options.brandLogoUrl);
1712
+ logo.style.display = 'none';
1713
+ };
1714
+
1715
+ logo.onload = () => {
1716
+ if (this.options.debug) console.log('Brand logo loaded successfully');
1717
+ };
1718
+
1719
+ // Add click functionality if link URL is provided
1720
+ if (this.options.brandLogoLinkUrl) {
1721
+ logo.style.cursor = 'pointer';
1722
+ logo.addEventListener('click', (e) => {
1723
+ e.stopPropagation(); // Prevent video controls interference
1724
+ window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1725
+ if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1726
+ });
1727
+ } else {
1728
+ logo.style.cursor = 'default';
1583
1729
  }
1584
1730
 
1585
- setBrandLogo(enabled, url = '', linkUrl = '') {
1586
- this.options.brandLogoEnabled = enabled;
1587
- if (url) {
1588
- this.options.brandLogoUrl = url;
1589
- }
1590
- if (linkUrl !== '') {
1591
- this.options.brandLogoLinkUrl = linkUrl;
1592
- }
1731
+ // Position the brand logo at the right of the controlbar (at the left of the buttons)
1732
+ controlsRight.insertBefore(logo, controlsRight.firstChild);
1593
1733
 
1594
- // Remove existing brand logo
1595
- const existingLogo = this.controls?.querySelector('.brand-logo');
1596
- if (existingLogo) {
1597
- existingLogo.remove();
1734
+ if (this.options.debug) {
1735
+ if (this.options.brandLogoLinkUrl) {
1736
+ console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
1737
+ } else {
1738
+ console.log('Brand logo created (no link)');
1598
1739
  }
1740
+ }
1741
+ }
1599
1742
 
1600
- // Recreate the logo if enabled
1601
- if (enabled && this.options.brandLogoUrl) {
1602
- this.createBrandLogo();
1603
- }
1743
+ setBrandLogo(enabled, url = '', linkUrl = '') {
1744
+ this.options.brandLogoEnabled = enabled;
1745
+ if (url) {
1746
+ this.options.brandLogoUrl = url;
1747
+ }
1748
+ if (linkUrl !== '') {
1749
+ this.options.brandLogoLinkUrl = linkUrl;
1750
+ }
1604
1751
 
1605
- return this;
1752
+ // Remove existing brand logo
1753
+ const existingLogo = this.controls?.querySelector('.brand-logo');
1754
+ if (existingLogo) {
1755
+ existingLogo.remove();
1606
1756
  }
1607
1757
 
1608
- getBrandLogoSettings() {
1609
- return {
1610
- enabled: this.options.brandLogoEnabled,
1611
- url: this.options.brandLogoUrl,
1612
- linkUrl: this.options.brandLogoLinkUrl
1613
- };
1758
+ // Recreate the logo if enabled
1759
+ if (enabled && this.options.brandLogoUrl) {
1760
+ this.createBrandLogo();
1614
1761
  }
1615
1762
 
1616
- switchToVideo(newVideoElement, shouldPlay = false) {
1617
- if (!newVideoElement) {
1618
- if (this.options.debug) console.error('🎵 New video element not found');
1619
- return false;
1620
- }
1763
+ return this;
1764
+ }
1765
+
1766
+ getBrandLogoSettings() {
1767
+ return {
1768
+ enabled: this.options.brandLogoEnabled,
1769
+ url: this.options.brandLogoUrl,
1770
+ linkUrl: this.options.brandLogoLinkUrl
1771
+ };
1772
+ }
1621
1773
 
1622
- // Pause current video
1623
- this.video.pause();
1774
+ switchToVideo(newVideoElement, shouldPlay = false) {
1775
+ if (!newVideoElement) {
1776
+ if (this.options.debug) console.error('🎵 New video element not found');
1777
+ return false;
1778
+ }
1624
1779
 
1625
- // Get new video sources and qualities
1626
- const newSources = Array.from(newVideoElement.querySelectorAll('source')).map(source => ({
1627
- src: source.src,
1628
- quality: source.getAttribute('data-quality') || 'auto',
1629
- type: source.type || 'video/mp4'
1630
- }));
1780
+ // Pause current video
1781
+ this.video.pause();
1631
1782
 
1632
- if (newSources.length === 0) {
1633
- if (this.options.debug) console.error('🎵 New video has no sources');
1634
- return false;
1635
- }
1783
+ // Get new video sources and qualities
1784
+ const newSources = Array.from(newVideoElement.querySelectorAll('source')).map(source => ({
1785
+ src: source.src,
1786
+ quality: source.getAttribute('data-quality') || 'auto',
1787
+ type: source.type || 'video/mp4'
1788
+ }));
1789
+
1790
+ if (newSources.length === 0) {
1791
+ if (this.options.debug) console.error('🎵 New video has no sources');
1792
+ return false;
1793
+ }
1636
1794
 
1637
- // Check if new video is adaptive stream
1638
- if (this.options.adaptiveStreaming && newSources.length > 0) {
1639
- const firstSource = newSources[0];
1640
- if (this.detectStreamType(firstSource.src)) {
1641
- // Initialize adaptive streaming for new video
1642
- this.initializeAdaptiveStreaming(firstSource.src).then((initialized) => {
1643
- if (initialized && shouldPlay) {
1644
- const playPromise = this.video.play();
1645
- if (playPromise) {
1646
- playPromise.catch(error => {
1647
- if (this.options.debug) console.log('Autoplay prevented:', error);
1648
- });
1649
- }
1795
+ // Check if new video is adaptive stream
1796
+ if (this.options.adaptiveStreaming && newSources.length > 0) {
1797
+ const firstSource = newSources[0];
1798
+ if (this.detectStreamType(firstSource.src)) {
1799
+ // Initialize adaptive streaming for new video
1800
+ this.initializeAdaptiveStreaming(firstSource.src).then((initialized) => {
1801
+ if (initialized && shouldPlay) {
1802
+ const playPromise = this.video.play();
1803
+ if (playPromise) {
1804
+ playPromise.catch(error => {
1805
+ if (this.options.debug) console.log('Autoplay prevented:', error);
1806
+ });
1650
1807
  }
1651
- });
1652
- return true;
1653
- }
1808
+ }
1809
+ });
1810
+ return true;
1654
1811
  }
1812
+ }
1655
1813
 
1656
- // Update traditional video sources
1657
- this.video.innerHTML = '';
1658
- newSources.forEach(source => {
1659
- const sourceEl = document.createElement('source');
1660
- sourceEl.src = source.src;
1661
- sourceEl.type = source.type;
1662
- sourceEl.setAttribute('data-quality', source.quality);
1663
- this.video.appendChild(sourceEl);
1664
- });
1814
+ // Update traditional video sources
1815
+ this.video.innerHTML = '';
1816
+ newSources.forEach(source => {
1817
+ const sourceEl = document.createElement('source');
1818
+ sourceEl.src = source.src;
1819
+ sourceEl.type = source.type;
1820
+ sourceEl.setAttribute('data-quality', source.quality);
1821
+ this.video.appendChild(sourceEl);
1822
+ });
1665
1823
 
1666
- // Update subtitles if present
1667
- const newTracks = Array.from(newVideoElement.querySelectorAll('track'));
1668
- newTracks.forEach(track => {
1669
- const trackEl = document.createElement('track');
1670
- trackEl.kind = track.kind;
1671
- trackEl.src = track.src;
1672
- trackEl.srclang = track.srclang;
1673
- trackEl.label = track.label;
1674
- if (track.default) trackEl.default = true;
1675
- this.video.appendChild(trackEl);
1676
- });
1824
+ // Update subtitles if present
1825
+ const newTracks = Array.from(newVideoElement.querySelectorAll('track'));
1826
+ newTracks.forEach(track => {
1827
+ const trackEl = document.createElement('track');
1828
+ trackEl.kind = track.kind;
1829
+ trackEl.src = track.src;
1830
+ trackEl.srclang = track.srclang;
1831
+ trackEl.label = track.label;
1832
+ if (track.default) trackEl.default = true;
1833
+ this.video.appendChild(trackEl);
1834
+ });
1677
1835
 
1678
- // Update video title
1679
- const newTitle = newVideoElement.getAttribute('data-video-title');
1680
- if (newTitle && this.options.showTitleOverlay) {
1681
- this.options.videoTitle = newTitle;
1682
- if (this.titleText) {
1683
- this.titleText.textContent = newTitle;
1684
- }
1836
+ // Update video title
1837
+ const newTitle = newVideoElement.getAttribute('data-video-title');
1838
+ if (newTitle && this.options.showTitleOverlay) {
1839
+ this.options.videoTitle = newTitle;
1840
+ if (this.titleText) {
1841
+ this.titleText.textContent = newTitle;
1685
1842
  }
1843
+ }
1686
1844
 
1687
- // Reload video
1688
- this.video.load();
1845
+ // Reload video
1846
+ this.video.load();
1689
1847
 
1690
- // Update qualities and quality selector
1691
- this.collectVideoQualities();
1692
- this.updateQualityMenu();
1848
+ // Update qualities and quality selector
1849
+ this.collectVideoQualities();
1850
+ this.updateQualityMenu();
1693
1851
 
1694
- // Play if needed
1695
- if (shouldPlay) {
1696
- const playPromise = this.video.play();
1697
- if (playPromise) {
1698
- playPromise.catch(error => {
1699
- if (this.options.debug) console.log('🎵 Autoplay prevented:', error);
1700
- });
1701
- }
1852
+ // Play if needed
1853
+ if (shouldPlay) {
1854
+ const playPromise = this.video.play();
1855
+ if (playPromise) {
1856
+ playPromise.catch(error => {
1857
+ if (this.options.debug) console.log('🎵 Autoplay prevented:', error);
1858
+ });
1702
1859
  }
1703
-
1704
- return true;
1705
1860
  }
1706
1861
 
1862
+ return true;
1863
+ }
1864
+
1707
1865
  /**
1708
1866
  * POSTER IMAGE MANAGEMENT
1709
1867
  * Initialize and manage video poster image
@@ -1920,59 +2078,104 @@ isPosterVisible() {
1920
2078
  }
1921
2079
 
1922
2080
 
1923
- loadScript(src) {
1924
- return new Promise((resolve, reject) => {
1925
- if (document.querySelector(`script[src="${src}"]`)) {
1926
- resolve();
1927
- return;
1928
- }
2081
+ loadScript(src) {
2082
+ return new Promise((resolve, reject) => {
2083
+ if (document.querySelector(`script[src="${src}"]`)) {
2084
+ resolve();
2085
+ return;
2086
+ }
2087
+
2088
+ const script = document.createElement('script');
2089
+ script.src = src;
2090
+ script.onload = resolve;
2091
+ script.onerror = reject;
2092
+ document.head.appendChild(script);
2093
+ });
2094
+ }
2095
+
2096
+ /**
2097
+ * Set seek handle shape dynamically
2098
+ * @param {string} shape - Shape type: none, circle, square, diamond, arrow, triangle, heart, star
2099
+ * @returns {Object} this
2100
+ */
2101
+ setSeekHandleShape(shape) {
2102
+ const validShapes = ['none', 'circle', 'square', 'diamond', 'arrow', 'triangle', 'heart', 'star'];
2103
+
2104
+ if (!validShapes.includes(shape)) {
2105
+ if (this.options.debug) console.warn('Invalid seek handle shape:', shape);
2106
+ return this;
2107
+ }
2108
+
2109
+ this.options.seekHandleShape = shape;
1929
2110
 
1930
- const script = document.createElement('script');
1931
- script.src = src;
1932
- script.onload = resolve;
1933
- script.onerror = reject;
1934
- document.head.appendChild(script);
2111
+ // Update handle class
2112
+ if (this.progressHandle) {
2113
+ // Remove all shape classes
2114
+ validShapes.forEach(s => {
2115
+ this.progressHandle.classList.remove(`progress-handle-${s}`);
1935
2116
  });
2117
+ // Add new shape class
2118
+ this.progressHandle.classList.add(`progress-handle-${shape}`);
1936
2119
  }
1937
2120
 
1938
- dispose() {
1939
- if (this.qualityMonitorInterval) {
1940
- clearInterval(this.qualityMonitorInterval);
1941
- this.qualityMonitorInterval = null;
1942
- }
2121
+ if (this.options.debug) console.log('Seek handle shape changed to:', shape);
2122
+ return this;
2123
+ }
1943
2124
 
1944
- if (this.autoHideTimer) {
1945
- clearTimeout(this.autoHideTimer);
1946
- this.autoHideTimer = null;
1947
- }
2125
+ /**
2126
+ * Get current seek handle shape
2127
+ * @returns {string} Current shape
2128
+ */
2129
+ getSeekHandleShape() {
2130
+ return this.options.seekHandleShape;
2131
+ }
1948
2132
 
1949
- this.cleanupQualityChange();
1950
- this.clearControlsTimeout();
1951
- this.clearTitleTimeout();
2133
+ /**
2134
+ * Get available seek handle shapes
2135
+ * @returns {Array} Array of available shapes
2136
+ */
2137
+ getAvailableSeekHandleShapes() {
2138
+ return ['none', 'circle', 'square', 'diamond', 'arrow', 'triangle', 'heart', 'star'];
2139
+ }
1952
2140
 
1953
- // Destroy adaptive streaming players
1954
- this.destroyAdaptivePlayer();
2141
+ dispose() {
2142
+ if (this.qualityMonitorInterval) {
2143
+ clearInterval(this.qualityMonitorInterval);
2144
+ this.qualityMonitorInterval = null;
2145
+ }
1955
2146
 
1956
- if (this.controls) {
1957
- this.controls.remove();
1958
- }
1959
- if (this.loadingOverlay) {
1960
- this.loadingOverlay.remove();
1961
- }
1962
- if (this.titleOverlay) {
1963
- this.titleOverlay.remove();
1964
- }
1965
- if (this.initialLoading) {
1966
- this.initialLoading.remove();
1967
- }
2147
+ if (this.autoHideTimer) {
2148
+ clearTimeout(this.autoHideTimer);
2149
+ this.autoHideTimer = null;
2150
+ }
1968
2151
 
1969
- if (this.video) {
1970
- this.video.classList.remove('video-player');
1971
- this.video.controls = true;
1972
- this.video.style.visibility = '';
1973
- this.video.style.opacity = '';
1974
- this.video.style.pointerEvents = '';
1975
- }
2152
+ this.cleanupQualityChange();
2153
+ this.clearControlsTimeout();
2154
+ this.clearTitleTimeout();
2155
+
2156
+ // Destroy adaptive streaming players
2157
+ this.destroyAdaptivePlayer();
2158
+
2159
+ if (this.controls) {
2160
+ this.controls.remove();
2161
+ }
2162
+ if (this.loadingOverlay) {
2163
+ this.loadingOverlay.remove();
2164
+ }
2165
+ if (this.titleOverlay) {
2166
+ this.titleOverlay.remove();
2167
+ }
2168
+ if (this.initialLoading) {
2169
+ this.initialLoading.remove();
2170
+ }
2171
+
2172
+ if (this.video) {
2173
+ this.video.classList.remove('video-player');
2174
+ this.video.controls = true;
2175
+ this.video.style.visibility = '';
2176
+ this.video.style.opacity = '';
2177
+ this.video.style.pointerEvents = '';
2178
+ }
1976
2179
  if (this.chapterMarkersContainer) {
1977
2180
  this.chapterMarkersContainer.remove();
1978
2181
  }
@@ -1984,37 +2187,37 @@ isPosterVisible() {
1984
2187
  }
1985
2188
  this.disposeAllPlugins();
1986
2189
 
1987
- }
2190
+ }
1988
2191
 
1989
- /**
2192
+ /**
1990
2193
 
1991
- * Apply specified resolution mode to video
2194
+ * Apply specified resolution mode to video
1992
2195
 
1993
- * @param {string} resolution - The resolution mode to apply
2196
+ * @param {string} resolution - The resolution mode to apply
1994
2197
 
1995
- */
2198
+ */
1996
2199
 
1997
- /**
2200
+ /**
1998
2201
 
1999
- * Get currently set resolution
2202
+ * Get currently set resolution
2000
2203
 
2001
- * @returns {string} Current resolution
2204
+ * @returns {string} Current resolution
2002
2205
 
2003
- */
2206
+ */
2004
2207
 
2005
- /**
2208
+ /**
2006
2209
 
2007
- * Initialize resolution from options value
2210
+ * Initialize resolution from options value
2008
2211
 
2009
- */
2212
+ */
2010
2213
 
2011
- /**
2214
+ /**
2012
2215
 
2013
- * Restore resolution after quality change - internal method
2216
+ * Restore resolution after quality change - internal method
2014
2217
 
2015
- * @private
2218
+ * @private
2016
2219
 
2017
- */
2220
+ */
2018
2221
 
2019
2222
  addEventListener(eventType, callback) {
2020
2223
  if (typeof callback !== 'function') {
@@ -2400,6 +2603,16 @@ initAutoHide() {
2400
2603
  this.controls.addEventListener('mouseleave', (e) => {
2401
2604
  if (this.autoHideDebug) {
2402
2605
  if (this.options.debug) console.log('Mouse EXITS controls - restart timer');
2606
+
2607
+ // Touch events for mobile devices
2608
+ this.container.addEventListener('touchstart', () => {
2609
+ this.showControlsNow();
2610
+ this.resetAutoHideTimer();
2611
+ });
2612
+
2613
+ this.container.addEventListener('touchend', () => {
2614
+ this.resetAutoHideTimer();
2615
+ });
2403
2616
  }
2404
2617
  this.onMouseLeaveControls(e);
2405
2618
  });
@@ -2443,7 +2656,8 @@ resetAutoHideTimer() {
2443
2656
  this.autoHideTimer = null;
2444
2657
  }
2445
2658
 
2446
- if (this.mouseOverControls) {
2659
+ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
2660
+ if (this.mouseOverControls && !isTouchDevice) {
2447
2661
  if (this.autoHideDebug) {
2448
2662
  if (this.options.debug) console.log('Not starting timer - mouse on controls');
2449
2663
  }
@@ -2488,8 +2702,9 @@ showControlsNow() {
2488
2702
  }
2489
2703
 
2490
2704
  hideControlsNow() {
2491
- // Don't hide if mouse is still over controls
2492
- if (this.mouseOverControls) {
2705
+ // Don't hide if mouse is still over controls (allow hiding on touch devices)
2706
+ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
2707
+ if (this.mouseOverControls && !isTouchDevice) {
2493
2708
  if (this.autoHideDebug && this.options.debug) console.log('⏸️ Not hiding - mouse still over controls');
2494
2709
  return;
2495
2710
  }
@@ -2683,7 +2898,7 @@ createControls() {
2683
2898
  <div class="progress-bar">
2684
2899
  <div class="progress-buffer"></div>
2685
2900
  <div class="progress-filled"></div>
2686
- <div class="progress-handle"></div>
2901
+ <div class="progress-handle progress-handle-${this.options.seekHandleShape}"></div>
2687
2902
  </div>
2688
2903
  ${this.options.showSeekTooltip ? '<div class="seek-tooltip">0:00</div>' : ''}
2689
2904
  </div>
@@ -2700,7 +2915,7 @@ createControls() {
2700
2915
  <span class="icon mute-icon hidden">🔇</span>
2701
2916
  </button>
2702
2917
 
2703
- <div class="volume-container" data-orientation="${this.options.volumeSlider}">
2918
+ <div class="volume-container" data-mobile-slider="${this.options.volumeSlider}">
2704
2919
  <input type="range" class="volume-slider" min="0" max="100" value="100" data-tooltip="volume">
2705
2920
  </div>
2706
2921
 
@@ -3346,140 +3561,160 @@ optimizeButtonsForSmallHeight() {
3346
3561
  /* Controls methods for main class - All original functionality preserved exactly */
3347
3562
 
3348
3563
  initializeQualityMonitoring() {
3349
- this.qualityMonitorInterval = setInterval(() => {
3564
+ this.qualityMonitorInterval = setInterval(() => {
3565
+ if (!this.isChangingQuality) {
3566
+ this.updateCurrentPlayingQuality();
3567
+ }
3568
+ }, 3000);
3569
+
3570
+ if (this.video) {
3571
+ this.video.addEventListener('loadedmetadata', () => {
3572
+ setTimeout(() => {
3573
+ if (!this.isChangingQuality) {
3574
+ this.updateCurrentPlayingQuality();
3575
+ }
3576
+ }, 100);
3577
+ });
3578
+
3579
+ this.video.addEventListener('resize', () => {
3350
3580
  if (!this.isChangingQuality) {
3351
3581
  this.updateCurrentPlayingQuality();
3352
3582
  }
3353
- }, 3000);
3354
-
3355
- if (this.video) {
3356
- this.video.addEventListener('loadedmetadata', () => {
3357
- setTimeout(() => {
3358
- if (!this.isChangingQuality) {
3359
- this.updateCurrentPlayingQuality();
3360
- }
3361
- }, 100);
3362
- });
3583
+ });
3363
3584
 
3364
- this.video.addEventListener('resize', () => {
3585
+ this.video.addEventListener('loadeddata', () => {
3586
+ setTimeout(() => {
3365
3587
  if (!this.isChangingQuality) {
3366
3588
  this.updateCurrentPlayingQuality();
3367
3589
  }
3368
- });
3369
-
3370
- this.video.addEventListener('loadeddata', () => {
3371
- setTimeout(() => {
3372
- if (!this.isChangingQuality) {
3373
- this.updateCurrentPlayingQuality();
3374
- }
3375
- }, 1000);
3376
- });
3377
- }
3590
+ }, 1000);
3591
+ });
3378
3592
  }
3593
+ }
3379
3594
 
3380
- getCurrentPlayingQuality() {
3381
- if (!this.video) return null;
3382
-
3383
- if (this.video.currentSrc && this.qualities && this.qualities.length > 0) {
3384
- const currentSource = this.qualities.find(q => {
3385
- const currentUrl = this.video.currentSrc.toLowerCase();
3386
- const qualityUrl = q.src.toLowerCase();
3387
-
3388
- if (this.debugQuality) {
3389
- if (this.options.debug) console.log('Quality comparison:', {
3390
- current: currentUrl,
3391
- quality: qualityUrl,
3392
- qualityName: q.quality,
3393
- match: currentUrl === qualityUrl || currentUrl.includes(qualityUrl) || qualityUrl.includes(currentUrl)
3394
- });
3395
- }
3595
+ getCurrentPlayingQuality() {
3596
+ if (!this.video) return null;
3396
3597
 
3397
- return currentUrl === qualityUrl ||
3398
- currentUrl.includes(qualityUrl) ||
3399
- qualityUrl.includes(currentUrl);
3400
- });
3598
+ if (this.video.currentSrc && this.qualities && this.qualities.length > 0) {
3599
+ const currentSource = this.qualities.find(q => {
3600
+ const currentUrl = this.video.currentSrc.toLowerCase();
3601
+ const qualityUrl = q.src.toLowerCase();
3401
3602
 
3402
- if (currentSource) {
3403
- if (this.debugQuality) {
3404
- if (this.options.debug) console.log('Quality found from source:', currentSource.quality);
3405
- }
3406
- return currentSource.quality;
3603
+ if (this.debugQuality) {
3604
+ if (this.options.debug) console.log('Quality comparison:', {
3605
+ current: currentUrl,
3606
+ quality: qualityUrl,
3607
+ qualityName: q.quality,
3608
+ match: currentUrl === qualityUrl || currentUrl.includes(qualityUrl) || qualityUrl.includes(currentUrl)
3609
+ });
3407
3610
  }
3408
- }
3409
3611
 
3410
- if (this.video.videoHeight && this.video.videoWidth) {
3411
- const height = this.video.videoHeight;
3412
- const width = this.video.videoWidth;
3612
+ return currentUrl === qualityUrl ||
3613
+ currentUrl.includes(qualityUrl) ||
3614
+ qualityUrl.includes(currentUrl);
3615
+ });
3413
3616
 
3617
+ if (currentSource) {
3414
3618
  if (this.debugQuality) {
3415
- if (this.options.debug) console.log('Risoluzione video:', { height, width });
3619
+ if (this.options.debug) console.log('Quality found from source:', currentSource.quality);
3416
3620
  }
3417
-
3418
- if (height >= 2160) return '4K';
3419
- if (height >= 1440) return '1440p';
3420
- if (height >= 1080) return '1080p';
3421
- if (height >= 720) return '720p';
3422
- if (height >= 480) return '480p';
3423
- if (height >= 360) return '360p';
3424
- if (height >= 240) return '240p';
3425
-
3426
- return `${height}p`;
3621
+ return currentSource.quality;
3427
3622
  }
3623
+ }
3624
+
3625
+ if (this.video.videoHeight && this.video.videoWidth) {
3626
+ const height = this.video.videoHeight;
3627
+ const width = this.video.videoWidth;
3428
3628
 
3429
3629
  if (this.debugQuality) {
3430
- if (this.options.debug) console.log('No quality detected:', {
3431
- currentSrc: this.video.currentSrc,
3432
- videoHeight: this.video.videoHeight,
3433
- videoWidth: this.video.videoWidth,
3434
- qualities: this.qualities
3435
- });
3630
+ if (this.options.debug) console.log('Risoluzione video:', { height, width });
3436
3631
  }
3437
3632
 
3438
- return null;
3633
+ if (height >= 2160) return '4K';
3634
+ if (height >= 1440) return '1440p';
3635
+ if (height >= 1080) return '1080p';
3636
+ if (height >= 720) return '720p';
3637
+ if (height >= 480) return '480p';
3638
+ if (height >= 360) return '360p';
3639
+ if (height >= 240) return '240p';
3640
+
3641
+ return `${height}p`;
3439
3642
  }
3440
3643
 
3441
- updateCurrentPlayingQuality() {
3442
- const newPlayingQuality = this.getCurrentPlayingQuality();
3644
+ if (this.debugQuality) {
3645
+ if (this.options.debug) console.log('No quality detected:', {
3646
+ currentSrc: this.video.currentSrc,
3647
+ videoHeight: this.video.videoHeight,
3648
+ videoWidth: this.video.videoWidth,
3649
+ qualities: this.qualities
3650
+ });
3651
+ }
3652
+
3653
+ return null;
3654
+ }
3655
+
3656
+ updateCurrentPlayingQuality() {
3657
+ const newPlayingQuality = this.getCurrentPlayingQuality();
3443
3658
 
3444
- if (newPlayingQuality && newPlayingQuality !== this.currentPlayingQuality) {
3445
- if (this.options.debug) console.log(`Quality changed: ${this.currentPlayingQuality} → ${newPlayingQuality}`);
3446
- this.currentPlayingQuality = newPlayingQuality;
3447
- this.updateQualityDisplay();
3448
- }
3659
+ if (newPlayingQuality && newPlayingQuality !== this.currentPlayingQuality) {
3660
+ if (this.options.debug) console.log(`Quality changed: ${this.currentPlayingQuality} → ${newPlayingQuality}`);
3661
+ this.currentPlayingQuality = newPlayingQuality;
3662
+ this.updateQualityDisplay();
3449
3663
  }
3664
+ }
3450
3665
 
3451
- updateQualityDisplay() {
3452
- this.updateQualityButton();
3453
- this.updateQualityMenu();
3454
- }
3666
+ updateQualityDisplay() {
3667
+ this.updateQualityButton();
3668
+ this.updateQualityMenu();
3669
+ }
3455
3670
 
3456
- updateQualityButton() {
3457
- const qualityBtn = this.controls?.querySelector('.quality-btn');
3458
- if (!qualityBtn) return;
3671
+ updateQualityButton() {
3672
+ const qualityBtn = this.controls?.querySelector('.quality-btn');
3673
+ if (!qualityBtn) return;
3459
3674
 
3460
- let btnText = qualityBtn.querySelector('.quality-btn-text');
3461
- if (!btnText) {
3462
- qualityBtn.innerHTML = `
3463
- <span class="icon">⚙</span>
3464
- <div class="quality-btn-text">
3465
- <div class="selected-quality">${this.selectedQuality === 'auto' ? this.t('auto') : this.selectedQuality}</div>
3466
- <div class="current-quality">${this.currentPlayingQuality || ''}</div>
3467
- </div>
3468
- `;
3469
- } else {
3470
- const selectedEl = btnText.querySelector('.selected-quality');
3471
- const currentEl = btnText.querySelector('.current-quality');
3675
+ let btnText = qualityBtn.querySelector('.quality-btn-text');
3676
+ if (!btnText) {
3677
+ // SECURITY: Use DOM methods instead of innerHTML to prevent XSS
3678
+ qualityBtn.textContent = ''; // Clear existing content
3679
+
3680
+ // Create icon element
3681
+ const iconSpan = document.createElement('span');
3682
+ iconSpan.className = 'icon';
3683
+ iconSpan.textContent = '⚙';
3684
+ qualityBtn.appendChild(iconSpan);
3685
+
3686
+ // Create text container
3687
+ btnText = document.createElement('div');
3688
+ btnText.className = 'quality-btn-text';
3689
+
3690
+ // Create selected quality element
3691
+ const selectedQualityDiv = document.createElement('div');
3692
+ selectedQualityDiv.className = 'selected-quality';
3693
+ selectedQualityDiv.textContent = this.selectedQuality === 'auto' ? this.t('auto') : this.selectedQuality;
3694
+ btnText.appendChild(selectedQualityDiv);
3695
+
3696
+ // Create current quality element
3697
+ const currentQualityDiv = document.createElement('div');
3698
+ currentQualityDiv.className = 'current-quality';
3699
+ currentQualityDiv.textContent = this.currentPlayingQuality || '';
3700
+ btnText.appendChild(currentQualityDiv);
3701
+
3702
+ // Append to button
3703
+ qualityBtn.appendChild(btnText);
3704
+ } else {
3705
+ // SECURITY: Update existing elements using textContent (not innerHTML)
3706
+ const selectedEl = btnText.querySelector('.selected-quality');
3707
+ const currentEl = btnText.querySelector('.current-quality');
3472
3708
 
3473
- if (selectedEl) {
3474
- selectedEl.textContent = this.selectedQuality === 'auto' ? this.t('auto') : this.selectedQuality;
3475
- }
3709
+ if (selectedEl) {
3710
+ selectedEl.textContent = this.selectedQuality === 'auto' ? this.t('auto') : this.selectedQuality;
3711
+ }
3476
3712
 
3477
- if (currentEl) {
3478
- currentEl.textContent = this.currentPlayingQuality || '';
3479
- currentEl.style.display = this.currentPlayingQuality ? 'block' : 'none';
3480
- }
3713
+ if (currentEl) {
3714
+ currentEl.textContent = this.currentPlayingQuality || '';
3481
3715
  }
3482
3716
  }
3717
+ }
3483
3718
 
3484
3719
  updateQualityMenu() {
3485
3720
  const qualityMenu = this.controls?.querySelector('.quality-menu');
@@ -3533,213 +3768,217 @@ updateQualityMenu() {
3533
3768
  qualityMenu.innerHTML = menuHTML;
3534
3769
  }
3535
3770
 
3536
- getQualityStatus() {
3537
- return {
3538
- selected: this.selectedQuality,
3539
- playing: this.currentPlayingQuality,
3540
- isAuto: this.selectedQuality === 'auto',
3541
- isChanging: this.isChangingQuality
3542
- };
3543
- }
3771
+ getQualityStatus() {
3772
+ return {
3773
+ selected: this.selectedQuality,
3774
+ playing: this.currentPlayingQuality,
3775
+ isAuto: this.selectedQuality === 'auto',
3776
+ isChanging: this.isChangingQuality
3777
+ };
3778
+ }
3544
3779
 
3545
- getSelectedQuality() {
3546
- return this.selectedQuality;
3547
- }
3780
+ getSelectedQuality() {
3781
+ return this.selectedQuality;
3782
+ }
3548
3783
 
3549
- isAutoQualityActive() {
3550
- return this.selectedQuality === 'auto';
3551
- }
3784
+ isAutoQualityActive() {
3785
+ return this.selectedQuality === 'auto';
3786
+ }
3552
3787
 
3553
- enableQualityDebug() {
3554
- this.debugQuality = true;
3555
- this.enableAutoHideDebug(); // Abilita anche debug auto-hide
3556
- if (this.options.debug) console.log('Quality AND auto-hide debug enabled');
3557
- this.updateCurrentPlayingQuality();
3558
- }
3788
+ enableQualityDebug() {
3789
+ this.debugQuality = true;
3790
+ this.enableAutoHideDebug(); // Abilita anche debug auto-hide
3791
+ if (this.options.debug) console.log('Quality AND auto-hide debug enabled');
3792
+ this.updateCurrentPlayingQuality();
3793
+ }
3559
3794
 
3560
- disableQualityDebug() {
3561
- this.debugQuality = false;
3562
- this.disableAutoHideDebug();
3563
- if (this.options.debug) console.log('Quality AND auto-hide debug disabled');
3795
+ disableQualityDebug() {
3796
+ this.debugQuality = false;
3797
+ this.disableAutoHideDebug();
3798
+ if (this.options.debug) console.log('Quality AND auto-hide debug disabled');
3799
+ }
3800
+
3801
+ changeQuality(e) {
3802
+ if (!e.target.classList.contains('quality-option')) return;
3803
+ if (this.isChangingQuality) return;
3804
+
3805
+ // Handle adaptive streaming quality change
3806
+ const adaptiveQuality = e.target.getAttribute('data-adaptive-quality');
3807
+ if (adaptiveQuality !== null && this.isAdaptiveStream) {
3808
+ const qualityIndex = adaptiveQuality === 'auto' ? -1 : parseInt(adaptiveQuality);
3809
+ this.setAdaptiveQuality(qualityIndex);
3810
+ this.updateAdaptiveQualityMenu();
3811
+ return;
3564
3812
  }
3565
3813
 
3566
- changeQuality(e) {
3567
- if (!e.target.classList.contains('quality-option')) return;
3568
- if (this.isChangingQuality) return;
3814
+ const quality = e.target.getAttribute('data-quality');
3815
+ if (!quality || quality === this.selectedQuality) return;
3569
3816
 
3570
- // Handle adaptive streaming quality change
3571
- const adaptiveQuality = e.target.getAttribute('data-adaptive-quality');
3572
- if (adaptiveQuality !== null && this.isAdaptiveStream) {
3573
- const qualityIndex = adaptiveQuality === 'auto' ? -1 : parseInt(adaptiveQuality);
3574
- this.setAdaptiveQuality(qualityIndex);
3575
- this.updateAdaptiveQualityMenu();
3576
- return;
3577
- }
3817
+ if (this.options.debug) console.log(`Quality change requested: ${this.selectedQuality} → ${quality}`);
3578
3818
 
3579
- const quality = e.target.getAttribute('data-quality');
3580
- if (!quality || quality === this.selectedQuality) return;
3819
+ this.selectedQuality = quality;
3581
3820
 
3582
- if (this.options.debug) console.log(`Quality change requested: ${this.selectedQuality} → ${quality}`);
3821
+ if (quality === 'auto') {
3822
+ this.enableAutoQuality();
3823
+ } else {
3824
+ this.setQuality(quality);
3825
+ }
3583
3826
 
3584
- this.selectedQuality = quality;
3827
+ this.updateQualityDisplay();
3828
+ }
3585
3829
 
3586
- if (quality === 'auto') {
3587
- this.enableAutoQuality();
3588
- } else {
3589
- this.setQuality(quality);
3590
- }
3830
+ setQuality(targetQuality) {
3831
+ if (this.options.debug) console.log(`setQuality("${targetQuality}") called`);
3591
3832
 
3592
- this.updateQualityDisplay();
3833
+ if (!targetQuality) {
3834
+ if (this.options.debug) console.error('targetQuality is empty!');
3835
+ return;
3593
3836
  }
3594
3837
 
3595
- setQuality(targetQuality) {
3596
- if (this.options.debug) console.log(`setQuality("${targetQuality}") called`);
3838
+ if (!this.video || !this.qualities || this.qualities.length === 0) return;
3839
+ if (this.isChangingQuality) return;
3597
3840
 
3598
- if (!targetQuality) {
3599
- if (this.options.debug) console.error('targetQuality is empty!');
3600
- return;
3601
- }
3841
+ const newSource = this.qualities.find(q => q.quality === targetQuality);
3842
+ if (!newSource || !newSource.src) {
3843
+ if (this.options.debug) console.error(`Quality "${targetQuality}" not found`);
3844
+ return;
3845
+ }
3602
3846
 
3603
- if (!this.video || !this.qualities || this.qualities.length === 0) return;
3604
- if (this.isChangingQuality) return;
3847
+ const currentTime = this.video.currentTime || 0;
3848
+ const wasPlaying = !this.video.paused;
3605
3849
 
3606
- const newSource = this.qualities.find(q => q.quality === targetQuality);
3607
- if (!newSource || !newSource.src) {
3608
- if (this.options.debug) console.error(`Quality "${targetQuality}" not found`);
3609
- return;
3610
- }
3850
+ this.isChangingQuality = true;
3851
+ this.selectedQuality = targetQuality;
3852
+ this.video.pause();
3611
3853
 
3612
- const currentTime = this.video.currentTime || 0;
3613
- const wasPlaying = !this.video.paused;
3854
+ // Show loading state during quality change
3855
+ this.showLoading();
3856
+ if (this.video.classList) {
3857
+ this.video.classList.add('quality-changing');
3858
+ }
3614
3859
 
3615
- this.isChangingQuality = true;
3616
- this.selectedQuality = targetQuality;
3617
- this.video.pause();
3860
+ const onLoadedData = () => {
3861
+ if (this.options.debug) console.log(`Quality ${targetQuality} applied!`);
3862
+ this.video.currentTime = currentTime;
3618
3863
 
3619
- // Show loading state during quality change
3620
- this.showLoading();
3621
- if (this.video.classList) {
3622
- this.video.classList.add('quality-changing');
3864
+ if (wasPlaying) {
3865
+ this.video.play().catch(e => {
3866
+ if (this.options.debug) console.log('Play error:', e);
3867
+ });
3623
3868
  }
3624
3869
 
3625
- const onLoadedData = () => {
3626
- if (this.options.debug) console.log(`Quality ${targetQuality} applied!`);
3627
- this.video.currentTime = currentTime;
3628
-
3629
- if (wasPlaying) {
3630
- this.video.play().catch(e => {
3631
- if (this.options.debug) console.log('Play error:', e);
3632
- });
3633
- }
3634
-
3635
- this.currentPlayingQuality = targetQuality;
3636
- this.updateQualityDisplay();
3637
- this.isChangingQuality = false;
3870
+ this.currentPlayingQuality = targetQuality;
3871
+ this.updateQualityDisplay();
3872
+ this.isChangingQuality = false;
3638
3873
 
3639
- // Restore resolution settings after quality change
3640
- this.restoreResolutionAfterQualityChange();
3641
- cleanup();
3642
- };
3874
+ // Restore resolution settings after quality change
3875
+ this.restoreResolutionAfterQualityChange();
3876
+ cleanup();
3877
+ };
3643
3878
 
3644
- const onError = (error) => {
3645
- if (this.options.debug) console.error(`Loading error ${targetQuality}:`, error);
3646
- this.isChangingQuality = false;
3647
- cleanup();
3648
- };
3879
+ const onError = (error) => {
3880
+ if (this.options.debug) console.error(`Loading error ${targetQuality}:`, error);
3881
+ this.isChangingQuality = false;
3649
3882
 
3650
- const cleanup = () => {
3651
- this.video.removeEventListener('loadeddata', onLoadedData);
3652
- this.video.removeEventListener('error', onError);
3653
- };
3883
+ // Trigger ended event for error handling
3884
+ this.onVideoError(error);
3654
3885
 
3655
- this.video.addEventListener('loadeddata', onLoadedData, { once: true });
3656
- this.video.addEventListener('error', onError, { once: true });
3886
+ cleanup();
3887
+ };
3657
3888
 
3658
- this.video.src = newSource.src;
3659
- this.video.load();
3660
- }
3889
+ const cleanup = () => {
3890
+ this.video.removeEventListener('loadeddata', onLoadedData);
3891
+ this.video.removeEventListener('error', onError);
3892
+ };
3661
3893
 
3662
- finishQualityChange(success, wasPlaying, currentTime, currentVolume, wasMuted, targetQuality) {
3663
- if (this.options.debug) console.log(`Quality change completion: success=${success}, target=${targetQuality}`);
3894
+ this.video.addEventListener('loadeddata', onLoadedData, { once: true });
3895
+ this.video.addEventListener('error', onError, { once: true });
3664
3896
 
3665
- if (this.qualityChangeTimeout) {
3666
- clearTimeout(this.qualityChangeTimeout);
3667
- this.qualityChangeTimeout = null;
3668
- }
3897
+ this.video.src = newSource.src;
3898
+ this.video.load();
3899
+ }
3669
3900
 
3670
- if (this.video) {
3671
- try {
3672
- if (success && currentTime > 0 && this.video.duration) {
3673
- this.video.currentTime = Math.min(currentTime, this.video.duration);
3674
- }
3901
+ finishQualityChange(success, wasPlaying, currentTime, currentVolume, wasMuted, targetQuality) {
3902
+ if (this.options.debug) console.log(`Quality change completion: success=${success}, target=${targetQuality}`);
3675
3903
 
3676
- this.video.volume = currentVolume;
3677
- this.video.muted = wasMuted;
3904
+ if (this.qualityChangeTimeout) {
3905
+ clearTimeout(this.qualityChangeTimeout);
3906
+ this.qualityChangeTimeout = null;
3907
+ }
3678
3908
 
3679
- if (success && wasPlaying) {
3680
- this.video.play().catch(err => {
3681
- if (this.options.debug) console.warn('Play after quality change failed:', err);
3682
- });
3683
- }
3684
- } catch (error) {
3685
- if (this.options.debug) console.error('Errore ripristino stato:', error);
3909
+ if (this.video) {
3910
+ try {
3911
+ if (success && currentTime > 0 && this.video.duration) {
3912
+ this.video.currentTime = Math.min(currentTime, this.video.duration);
3686
3913
  }
3687
3914
 
3688
- if (this.video.classList) {
3689
- this.video.classList.remove('quality-changing');
3915
+ this.video.volume = currentVolume;
3916
+ this.video.muted = wasMuted;
3917
+
3918
+ if (success && wasPlaying) {
3919
+ this.video.play().catch(err => {
3920
+ if (this.options.debug) console.warn('Play after quality change failed:', err);
3921
+ });
3690
3922
  }
3923
+ } catch (error) {
3924
+ if (this.options.debug) console.error('Errore ripristino stato:', error);
3691
3925
  }
3692
3926
 
3693
- this.hideLoading();
3694
- this.isChangingQuality = false;
3695
-
3696
- if (success) {
3697
- if (this.options.debug) console.log('Quality change completed successfully');
3698
- setTimeout(() => {
3699
- this.currentPlayingQuality = targetQuality;
3700
- this.updateQualityDisplay();
3701
- if (this.options.debug) console.log(`🎯 Quality confirmed active: ${targetQuality}`);
3702
- }, 100);
3703
- } else {
3704
- if (this.options.debug) console.warn('Quality change failed or timeout');
3927
+ if (this.video.classList) {
3928
+ this.video.classList.remove('quality-changing');
3705
3929
  }
3930
+ }
3706
3931
 
3932
+ this.hideLoading();
3933
+ this.isChangingQuality = false;
3934
+
3935
+ if (success) {
3936
+ if (this.options.debug) console.log('Quality change completed successfully');
3707
3937
  setTimeout(() => {
3708
- this.updateCurrentPlayingQuality();
3709
- }, 2000);
3938
+ this.currentPlayingQuality = targetQuality;
3939
+ this.updateQualityDisplay();
3940
+ if (this.options.debug) console.log(`🎯 Quality confirmed active: ${targetQuality}`);
3941
+ }, 100);
3942
+ } else {
3943
+ if (this.options.debug) console.warn('Quality change failed or timeout');
3710
3944
  }
3711
3945
 
3712
- cleanupQualityChange() {
3713
- if (this.qualityChangeTimeout) {
3714
- clearTimeout(this.qualityChangeTimeout);
3715
- this.qualityChangeTimeout = null;
3716
- }
3717
- }
3946
+ setTimeout(() => {
3947
+ this.updateCurrentPlayingQuality();
3948
+ }, 2000);
3949
+ }
3718
3950
 
3719
- enableAutoQuality() {
3720
- if (this.options.debug) console.log('🔄 enableAutoQuality - keeping selectedQuality as "auto"');
3951
+ cleanupQualityChange() {
3952
+ if (this.qualityChangeTimeout) {
3953
+ clearTimeout(this.qualityChangeTimeout);
3954
+ this.qualityChangeTimeout = null;
3955
+ }
3956
+ }
3721
3957
 
3722
- // IMPORTANT: Keep selectedQuality as 'auto' for proper UI display
3723
- this.selectedQuality = 'auto';
3958
+ enableAutoQuality() {
3959
+ if (this.options.debug) console.log('🔄 enableAutoQuality - keeping selectedQuality as "auto"');
3724
3960
 
3725
- if (!this.qualities || this.qualities.length === 0) {
3726
- if (this.options.debug) console.warn('⚠️ No qualities available for auto selection');
3727
- this.updateQualityDisplay();
3728
- return;
3729
- }
3961
+ // IMPORTANT: Keep selectedQuality as 'auto' for proper UI display
3962
+ this.selectedQuality = 'auto';
3730
3963
 
3731
- // Smart connection-based quality selection
3732
- let autoSelectedQuality = this.getAutoQualityBasedOnConnection();
3964
+ if (!this.qualities || this.qualities.length === 0) {
3965
+ if (this.options.debug) console.warn('⚠️ No qualities available for auto selection');
3966
+ this.updateQualityDisplay();
3967
+ return;
3968
+ }
3733
3969
 
3734
- if (this.options.debug) {
3735
- console.log('🎯 Auto quality selected:', autoSelectedQuality);
3736
- console.log('📊 selectedQuality remains: "auto" (for UI)');
3737
- }
3970
+ // Smart connection-based quality selection
3971
+ let autoSelectedQuality = this.getAutoQualityBasedOnConnection();
3738
3972
 
3739
- // Apply the auto-selected quality but keep UI showing "auto"
3740
- this.applyAutoQuality(autoSelectedQuality);
3973
+ if (this.options.debug) {
3974
+ console.log('🎯 Auto quality selected:', autoSelectedQuality);
3975
+ console.log('📊 selectedQuality remains: "auto" (for UI)');
3741
3976
  }
3742
3977
 
3978
+ // Apply the auto-selected quality but keep UI showing "auto"
3979
+ this.applyAutoQuality(autoSelectedQuality);
3980
+ }
3981
+
3743
3982
  // ENHANCED CONNECTION DETECTION - Uses RTT + downlink heuristics
3744
3983
  // Handles both Ethernet and real mobile 4G intelligently
3745
3984
 
@@ -4006,102 +4245,120 @@ getAutoQualityBasedOnConnection() {
4006
4245
  return maxQuality.quality;
4007
4246
  }
4008
4247
 
4009
- applyAutoQuality(targetQuality) {
4010
- if (!targetQuality || !this.video || !this.qualities || this.qualities.length === 0) {
4011
- return;
4012
- }
4248
+ applyAutoQuality(targetQuality) {
4249
+ if (!targetQuality || !this.video || !this.qualities || this.qualities.length === 0) {
4250
+ return;
4251
+ }
4013
4252
 
4014
- if (this.isChangingQuality) return;
4253
+ if (this.isChangingQuality) return;
4015
4254
 
4016
- const newSource = this.qualities.find(q => q.quality === targetQuality);
4017
- if (!newSource || !newSource.src) {
4018
- if (this.options.debug) console.error('Auto quality', targetQuality, 'not found');
4019
- return;
4020
- }
4255
+ const newSource = this.qualities.find(q => q.quality === targetQuality);
4256
+ if (!newSource || !newSource.src) {
4257
+ if (this.options.debug) console.error('Auto quality', targetQuality, 'not found');
4258
+ return;
4259
+ }
4021
4260
 
4022
- // Store current resolution to restore after quality change
4023
- const currentResolution = this.getCurrentResolution();
4261
+ // Store current resolution to restore after quality change
4262
+ const currentResolution = this.getCurrentResolution();
4024
4263
 
4025
- const currentTime = this.video.currentTime || 0;
4026
- const wasPlaying = !this.video.paused;
4264
+ const currentTime = this.video.currentTime || 0;
4265
+ const wasPlaying = !this.video.paused;
4027
4266
 
4028
- this.isChangingQuality = true;
4029
- this.video.pause();
4267
+ this.isChangingQuality = true;
4268
+ this.video.pause();
4030
4269
 
4031
- const onLoadedData = () => {
4032
- if (this.options.debug) console.log('Auto quality', targetQuality, 'applied');
4033
- this.video.currentTime = currentTime;
4034
- if (wasPlaying) {
4035
- this.video.play().catch(e => {
4036
- if (this.options.debug) console.log('Autoplay prevented:', e);
4037
- });
4038
- }
4039
- this.currentPlayingQuality = targetQuality;
4040
- // Keep selectedQuality as 'auto' for UI display
4041
- this.updateQualityDisplay();
4042
- this.isChangingQuality = false;
4043
- cleanup();
4044
- };
4270
+ // Show loading overlay
4271
+ this.showLoading();
4272
+ if (this.video.classList) {
4273
+ this.video.classList.add('quality-changing');
4274
+ }
4045
4275
 
4046
- const onError = (error) => {
4047
- if (this.options.debug) console.error('Auto quality loading error:', error);
4048
- this.isChangingQuality = false;
4049
- cleanup();
4050
- };
4051
4276
 
4052
- const cleanup = () => {
4053
- this.video.removeEventListener('loadeddata', onLoadedData);
4054
- this.video.removeEventListener('error', onError);
4055
- };
4277
+ const onLoadedData = () => {
4278
+ if (this.options.debug) console.log('Auto quality', targetQuality, 'applied');
4279
+ this.video.currentTime = currentTime;
4280
+ if (wasPlaying) {
4281
+ this.video.play().catch(e => {
4282
+ if (this.options.debug) console.log('Autoplay prevented:', e);
4283
+ });
4284
+ }
4285
+ this.currentPlayingQuality = targetQuality;
4286
+ // Keep selectedQuality as 'auto' for UI display
4287
+ this.updateQualityDisplay();
4056
4288
 
4057
- this.video.addEventListener('loadeddata', onLoadedData, { once: true });
4058
- this.video.addEventListener('error', onError, { once: true });
4059
- this.video.src = newSource.src;
4060
- this.video.load();
4061
- }
4289
+ // Hide loading overlay
4290
+ this.hideLoading();
4291
+ if (this.video.classList) {
4292
+ this.video.classList.remove('quality-changing');
4293
+ }
4062
4294
 
4063
- setDefaultQuality(quality) {
4064
- if (this.options.debug) console.log(`🔧 Setting defaultQuality: "${quality}"`);
4065
- this.options.defaultQuality = quality;
4066
- this.selectedQuality = quality;
4295
+ this.isChangingQuality = false;
4296
+ cleanup();
4297
+ };
4067
4298
 
4068
- if (quality === 'auto') {
4069
- this.enableAutoQuality();
4070
- } else {
4071
- this.setQuality(quality);
4072
- }
4299
+ const onError = (error) => {
4300
+ if (this.options.debug) console.error('Auto quality loading error:', error);
4301
+ this.isChangingQuality = false;
4073
4302
 
4074
- return this;
4075
- }
4303
+ // Trigger ended event for error handling
4304
+ this.onVideoError(error);
4076
4305
 
4077
- getDefaultQuality() {
4078
- return this.options.defaultQuality;
4079
- }
4306
+ cleanup();
4307
+ };
4080
4308
 
4081
- getQualityLabel(height, width) {
4082
- if (height >= 2160) return '4K';
4083
- if (height >= 1440) return '1440p';
4084
- if (height >= 1080) return '1080p';
4085
- if (height >= 720) return '720p';
4086
- if (height >= 480) return '480p';
4087
- if (height >= 360) return '360p';
4088
- if (height >= 240) return '240p';
4089
- return `${height}p`;
4309
+ const cleanup = () => {
4310
+ this.video.removeEventListener('loadeddata', onLoadedData);
4311
+ this.video.removeEventListener('error', onError);
4312
+ };
4313
+
4314
+ this.video.addEventListener('loadeddata', onLoadedData, { once: true });
4315
+ this.video.addEventListener('error', onError, { once: true });
4316
+ this.video.src = newSource.src;
4317
+ this.video.load();
4318
+ }
4319
+
4320
+ setDefaultQuality(quality) {
4321
+ if (this.options.debug) console.log(`🔧 Setting defaultQuality: "${quality}"`);
4322
+ this.options.defaultQuality = quality;
4323
+ this.selectedQuality = quality;
4324
+
4325
+ if (quality === 'auto') {
4326
+ this.enableAutoQuality();
4327
+ } else {
4328
+ this.setQuality(quality);
4090
4329
  }
4091
4330
 
4092
- updateAdaptiveQualityMenu() {
4093
- const qualityMenu = this.controls?.querySelector('.quality-menu');
4094
- if (!qualityMenu || !this.isAdaptiveStream) return;
4331
+ return this;
4332
+ }
4095
4333
 
4096
- let menuHTML = `<div class="quality-option ${this.isAutoQuality() ? 'active' : ''}" data-adaptive-quality="auto">Auto</div>`;
4334
+ getDefaultQuality() {
4335
+ return this.options.defaultQuality;
4336
+ }
4097
4337
 
4098
- this.adaptiveQualities.forEach(quality => {
4099
- const isActive = this.getCurrentAdaptiveQuality() === quality.index;
4100
- menuHTML += `<div class="quality-option ${isActive ? 'active' : ''}" data-adaptive-quality="${quality.index}">${quality.label}</div>`;
4101
- });
4338
+ getQualityLabel(height, width) {
4339
+ if (height >= 2160) return '4K';
4340
+ if (height >= 1440) return '1440p';
4341
+ if (height >= 1080) return '1080p';
4342
+ if (height >= 720) return '720p';
4343
+ if (height >= 480) return '480p';
4344
+ if (height >= 360) return '360p';
4345
+ if (height >= 240) return '240p';
4346
+ return `${height}p`;
4347
+ }
4102
4348
 
4103
- qualityMenu.innerHTML = menuHTML;
4104
- }
4349
+ updateAdaptiveQualityMenu() {
4350
+ const qualityMenu = this.controls?.querySelector('.quality-menu');
4351
+ if (!qualityMenu || !this.isAdaptiveStream) return;
4352
+
4353
+ let menuHTML = `<div class="quality-option ${this.isAutoQuality() ? 'active' : ''}" data-adaptive-quality="auto">Auto</div>`;
4354
+
4355
+ this.adaptiveQualities.forEach(quality => {
4356
+ const isActive = this.getCurrentAdaptiveQuality() === quality.index;
4357
+ menuHTML += `<div class="quality-option ${isActive ? 'active' : ''}" data-adaptive-quality="${quality.index}">${quality.label}</div>`;
4358
+ });
4359
+
4360
+ qualityMenu.innerHTML = menuHTML;
4361
+ }
4105
4362
 
4106
4363
  updateAdaptiveQualityDisplay() {
4107
4364
  if (!this.isAdaptiveStream) return;
@@ -4127,61 +4384,61 @@ updateAdaptiveQualityDisplay() {
4127
4384
  }
4128
4385
  }
4129
4386
 
4130
- setAdaptiveQuality(qualityIndex) {
4131
- if (!this.isAdaptiveStream) return;
4387
+ setAdaptiveQuality(qualityIndex) {
4388
+ if (!this.isAdaptiveStream) return;
4132
4389
 
4133
- try {
4134
- if (qualityIndex === 'auto' || qualityIndex === -1) {
4135
- // Enable auto quality
4136
- if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
4137
- this.dashPlayer.updateSettings({
4138
- streaming: {
4139
- abr: { autoSwitchBitrate: { video: true } }
4140
- }
4141
- });
4142
- } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
4143
- this.hlsPlayer.currentLevel = -1; // Auto level selection
4144
- }
4145
- this.selectedQuality = 'auto';
4146
- } else {
4147
- // Set specific quality
4148
- if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
4149
- this.dashPlayer.updateSettings({
4150
- streaming: {
4151
- abr: { autoSwitchBitrate: { video: false } }
4152
- }
4153
- });
4154
- this.dashPlayer.setQualityFor('video', qualityIndex);
4155
- } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
4156
- this.hlsPlayer.currentLevel = qualityIndex;
4157
- }
4158
- this.selectedQuality = this.adaptiveQualities[qualityIndex]?.label || 'Unknown';
4390
+ try {
4391
+ if (qualityIndex === 'auto' || qualityIndex === -1) {
4392
+ // Enable auto quality
4393
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
4394
+ this.dashPlayer.updateSettings({
4395
+ streaming: {
4396
+ abr: { autoSwitchBitrate: { video: true } }
4397
+ }
4398
+ });
4399
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
4400
+ this.hlsPlayer.currentLevel = -1; // Auto level selection
4401
+ }
4402
+ this.selectedQuality = 'auto';
4403
+ } else {
4404
+ // Set specific quality
4405
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
4406
+ this.dashPlayer.updateSettings({
4407
+ streaming: {
4408
+ abr: { autoSwitchBitrate: { video: false } }
4409
+ }
4410
+ });
4411
+ this.dashPlayer.setQualityFor('video', qualityIndex);
4412
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
4413
+ this.hlsPlayer.currentLevel = qualityIndex;
4159
4414
  }
4415
+ this.selectedQuality = this.adaptiveQualities[qualityIndex]?.label || 'Unknown';
4416
+ }
4160
4417
 
4161
- this.updateAdaptiveQualityDisplay();
4162
- if (this.options.debug) console.log('📡 Adaptive quality set to:', qualityIndex);
4418
+ this.updateAdaptiveQualityDisplay();
4419
+ if (this.options.debug) console.log('📡 Adaptive quality set to:', qualityIndex);
4163
4420
 
4164
- } catch (error) {
4165
- if (this.options.debug) console.error('📡 Error setting adaptive quality:', error);
4166
- }
4421
+ } catch (error) {
4422
+ if (this.options.debug) console.error('📡 Error setting adaptive quality:', error);
4167
4423
  }
4424
+ }
4168
4425
 
4169
- getCurrentAdaptiveQuality() {
4170
- if (!this.isAdaptiveStream) return null;
4426
+ getCurrentAdaptiveQuality() {
4427
+ if (!this.isAdaptiveStream) return null;
4171
4428
 
4172
- try {
4173
- if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
4174
- return this.dashPlayer.getQualityFor('video');
4175
- } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
4176
- return this.hlsPlayer.currentLevel;
4177
- }
4178
- } catch (error) {
4179
- if (this.options.debug) console.error('📡 Error getting current quality:', error);
4429
+ try {
4430
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
4431
+ return this.dashPlayer.getQualityFor('video');
4432
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
4433
+ return this.hlsPlayer.currentLevel;
4180
4434
  }
4181
-
4182
- return null;
4435
+ } catch (error) {
4436
+ if (this.options.debug) console.error('📡 Error getting current quality:', error);
4183
4437
  }
4184
4438
 
4439
+ return null;
4440
+ }
4441
+
4185
4442
  getCurrentAdaptiveQualityLabel() {
4186
4443
  const currentIndex = this.getCurrentAdaptiveQuality();
4187
4444
  if (currentIndex === null || currentIndex === -1) {
@@ -4190,75 +4447,75 @@ getCurrentAdaptiveQualityLabel() {
4190
4447
  return this.adaptiveQualities[currentIndex]?.label || this.tauto;
4191
4448
  }
4192
4449
 
4193
- isAutoQuality() {
4194
- if (this.isAdaptiveStream) {
4195
- const currentQuality = this.getCurrentAdaptiveQuality();
4196
- return currentQuality === null || currentQuality === -1 || this.selectedQuality === 'auto';
4197
- }
4198
- return this.selectedQuality === 'auto';
4450
+ isAutoQuality() {
4451
+ if (this.isAdaptiveStream) {
4452
+ const currentQuality = this.getCurrentAdaptiveQuality();
4453
+ return currentQuality === null || currentQuality === -1 || this.selectedQuality === 'auto';
4199
4454
  }
4455
+ return this.selectedQuality === 'auto';
4456
+ }
4200
4457
 
4201
- setResolution(resolution) {
4202
- if (!this.video || !this.container) {
4203
- if (this.options.debug) console.warn("Video or container not available for setResolution");
4204
- return;
4205
- }
4458
+ setResolution(resolution) {
4459
+ if (!this.video || !this.container) {
4460
+ if (this.options.debug) console.warn("Video or container not available for setResolution");
4461
+ return;
4462
+ }
4206
4463
 
4207
- // Supported values including new scale-to-fit mode
4208
- const supportedResolutions = ["normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"];
4464
+ // Supported values including new scale-to-fit mode
4465
+ const supportedResolutions = ["normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"];
4209
4466
 
4210
- if (!supportedResolutions.includes(resolution)) {
4211
- if (this.options.debug) console.warn(`Resolution "${resolution}" not supported. Supported values: ${supportedResolutions.join(", ")}`);
4212
- return;
4213
- }
4467
+ if (!supportedResolutions.includes(resolution)) {
4468
+ if (this.options.debug) console.warn(`Resolution "${resolution}" not supported. Supported values: ${supportedResolutions.join(", ")}`);
4469
+ return;
4470
+ }
4214
4471
 
4215
- // Remove all previous resolution classes
4216
- const allResolutionClasses = [
4217
- "resolution-normal", "resolution-4-3", "resolution-16-9",
4218
- "resolution-stretched", "resolution-fit-to-screen", "resolution-scale-to-fit"
4219
- ];
4472
+ // Remove all previous resolution classes
4473
+ const allResolutionClasses = [
4474
+ "resolution-normal", "resolution-4-3", "resolution-16-9",
4475
+ "resolution-stretched", "resolution-fit-to-screen", "resolution-scale-to-fit"
4476
+ ];
4220
4477
 
4221
- this.video.classList.remove(...allResolutionClasses);
4222
- if (this.container) {
4223
- this.container.classList.remove(...allResolutionClasses);
4224
- }
4478
+ this.video.classList.remove(...allResolutionClasses);
4479
+ if (this.container) {
4480
+ this.container.classList.remove(...allResolutionClasses);
4481
+ }
4225
4482
 
4226
- // Apply new resolution class
4227
- const cssClass = `resolution-${resolution.replace(":", "-")}`;
4228
- this.video.classList.add(cssClass);
4229
- if (this.container) {
4230
- this.container.classList.add(cssClass);
4231
- }
4483
+ // Apply new resolution class
4484
+ const cssClass = `resolution-${resolution.replace(":", "-")}`;
4485
+ this.video.classList.add(cssClass);
4486
+ if (this.container) {
4487
+ this.container.classList.add(cssClass);
4488
+ }
4232
4489
 
4233
- // Update option
4234
- this.options.resolution = resolution;
4490
+ // Update option
4491
+ this.options.resolution = resolution;
4235
4492
 
4236
- if (this.options.debug) {
4237
- console.log(`Resolution applied: ${resolution} (CSS class: ${cssClass})`);
4238
- }
4493
+ if (this.options.debug) {
4494
+ console.log(`Resolution applied: ${resolution} (CSS class: ${cssClass})`);
4239
4495
  }
4496
+ }
4240
4497
 
4241
- getCurrentResolution() {
4242
- return this.options.resolution || "normal";
4243
- }
4498
+ getCurrentResolution() {
4499
+ return this.options.resolution || "normal";
4500
+ }
4244
4501
 
4245
- initializeResolution() {
4246
- if (this.options.resolution && this.options.resolution !== "normal") {
4247
- this.setResolution(this.options.resolution);
4248
- }
4502
+ initializeResolution() {
4503
+ if (this.options.resolution && this.options.resolution !== "normal") {
4504
+ this.setResolution(this.options.resolution);
4249
4505
  }
4506
+ }
4250
4507
 
4251
- restoreResolutionAfterQualityChange() {
4252
- if (this.options.resolution && this.options.resolution !== "normal") {
4253
- if (this.options.debug) {
4254
- console.log(`Restoring resolution "${this.options.resolution}" after quality change`);
4255
- }
4256
- // Small delay to ensure video element is ready
4257
- setTimeout(() => {
4258
- this.setResolution(this.options.resolution);
4259
- }, 150);
4508
+ restoreResolutionAfterQualityChange() {
4509
+ if (this.options.resolution && this.options.resolution !== "normal") {
4510
+ if (this.options.debug) {
4511
+ console.log(`Restoring resolution "${this.options.resolution}" after quality change`);
4260
4512
  }
4513
+ // Small delay to ensure video element is ready
4514
+ setTimeout(() => {
4515
+ this.setResolution(this.options.resolution);
4516
+ }, 150);
4261
4517
  }
4518
+ }
4262
4519
 
4263
4520
  /* Subtitles Module for MYETV Video Player
4264
4521
  * Conservative modularization - original code preserved exactly
@@ -4303,7 +4560,7 @@ createCustomSubtitleOverlay() {
4303
4560
  'bottom: 80px;' +
4304
4561
  'left: 50%;' +
4305
4562
  'transform: translateX(-50%);' +
4306
- 'z-index: 5;' +
4563
+ 'z-index: 5;' +
4307
4564
  'color: white;' +
4308
4565
  'font-family: Arial, sans-serif;' +
4309
4566
  'font-size: clamp(12px, 4vw, 18px);' + // RESPONSIVE font-size
@@ -4397,7 +4654,7 @@ parseCustomSRT(srtText) {
4397
4654
  if (timeMatch) {
4398
4655
  var startTime = this.customTimeToSeconds(timeMatch[1]);
4399
4656
  var endTime = this.customTimeToSeconds(timeMatch[2]);
4400
- var text = lines.slice(2).join('\n').trim().replace(/<[^>]*>/g, '');
4657
+ var text = this.sanitizeSubtitleText(lines.slice(2).join('\n').trim());
4401
4658
 
4402
4659
  if (text.length > 0 && startTime < endTime) {
4403
4660
  subtitles.push({