myetv-player 1.0.0 → 1.0.6

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