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
package/src/core.js CHANGED
@@ -2,433 +2,434 @@
2
2
  // Conservative modularization - original code preserved exactly
3
3
  // Created by https://www.myetv.tv https://oskarcosimo.com
4
4
 
5
- constructor(videoElement, options = {}) {
6
- this.video = typeof videoElement === 'string'
7
- ? document.getElementById(videoElement)
8
- : videoElement;
5
+ constructor(videoElement, options = {}) {
6
+ this.video = typeof videoElement === 'string'
7
+ ? document.getElementById(videoElement)
8
+ : videoElement;
9
9
 
10
- if (!this.video) {
11
- throw new Error('Video element not found: ' + videoElement);
12
- }
10
+ if (!this.video) {
11
+ throw new Error('Video element not found: ' + videoElement);
12
+ }
13
+
14
+ this.options = {
15
+ showQualitySelector: true,
16
+ showSpeedControl: true,
17
+ showFullscreen: true,
18
+ showPictureInPicture: true,
19
+ showSubtitles: true,
20
+ subtitlesEnabled: false,
21
+ autoHide: true,
22
+ autoHideDelay: 3000,
23
+ poster: null, // URL of poster image
24
+ showPosterOnEnd: false, // Show poster again when video ends
25
+ keyboardControls: true,
26
+ showSeekTooltip: true,
27
+ showTitleOverlay: false,
28
+ videoTitle: '',
29
+ persistentTitle: false,
30
+ debug: false, // Enable/disable debug logging
31
+ autoplay: false, // if video should autoplay at start
32
+ defaultQuality: 'auto', // 'auto', '1080p', '720p', '480p', etc.
33
+ language: null, // language of the player (default english)
34
+ pauseClick: true, // the player should be paused when click over the video area
35
+ doubleTapPause: true, // first tap (or click) show the controlbar, second tap (or click) pause
36
+ brandLogoEnabled: false, // Enable/disable brand logo
37
+ brandLogoUrl: '', // URL for brand logo image
38
+ brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
39
+ playlistEnabled: true, // Enable/disable playlist detection
40
+ playlistAutoPlay: true, // Auto-play next video when current ends
41
+ playlistLoop: false, // Loop playlist when reaching the end
42
+ loop: false, // Loop video when it ends (restart from beginning)
43
+ volumeSlider: 'show', // Mobile volume slider: 'show' (horizontal popup) or 'hide' (no slider on mobile)
44
+ // WATERMARK OVERLAY
45
+ watermarkUrl: '', // URL of watermark image
46
+ watermarkLink: '', // Optional URL to open when clicking watermark
47
+ watermarkPosition: 'bottomright', // Position: topleft, topright, bottomleft, bottomright
48
+ watermarkTitle: '', // Optional tooltip title
49
+ hideWatermark: true, // Hide watermark with controls (default: true)
50
+ // ADAPTIVE STREAMING SUPPORT
51
+ adaptiveStreaming: false, // Enable DASH/HLS adaptive streaming
52
+ dashLibUrl: 'https://cdn.dashjs.org/latest/dash.all.min.js', // Dash.js library URL
53
+ hlsLibUrl: 'https://cdn.jsdelivr.net/npm/hls.js@latest', // HLS.js library URL
54
+ adaptiveQualityControl: true, // Show quality control for adaptive streams
55
+ // AUDIO PLAYER
56
+ audiofile: false,
57
+ audiowave: false,
58
+ // RESOLUTION CONTROL
59
+ resolution: "normal", // "normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"
60
+ ...options
61
+ };
62
+
63
+ this.isUserSeeking = false;
64
+ this.controlsTimeout = null;
65
+ this.titleTimeout = null;
66
+ this.currentQualityIndex = 0;
67
+ this.qualities = [];
68
+ this.originalSources = [];
69
+ this.setupMenuToggles(); // Initialize menu toggle system
70
+ this.isPiPSupported = this.checkPiPSupport();
71
+ this.seekTooltip = null;
72
+ this.titleOverlay = null;
73
+ this.isPlayerReady = false;
74
+
75
+ // Subtitle management
76
+ this.textTracks = [];
77
+ this.currentSubtitleTrack = null;
78
+ this.subtitlesEnabled = false;
79
+ this.customSubtitleRenderer = null;
13
80
 
14
- this.options = {
15
- showQualitySelector: true,
16
- showSpeedControl: true,
17
- showFullscreen: true,
18
- showPictureInPicture: true,
19
- showSubtitles: true,
20
- subtitlesEnabled: false,
21
- autoHide: true,
22
- autoHideDelay: 3000,
23
- poster: null, // URL of poster image
24
- showPosterOnEnd: false, // Show poster again when video ends
25
- keyboardControls: true,
26
- showSeekTooltip: true,
27
- showTitleOverlay: false,
28
- videoTitle: '',
29
- persistentTitle: false,
30
- debug: false, // Enable/disable debug logging
31
- autoplay: false, // if video should autoplay at start
32
- defaultQuality: 'auto', // 'auto', '1080p', '720p', '480p', etc.
33
- language: null, // language of the player (default english)
34
- pauseClick: true, // the player should be paused when click over the video area
35
- doubleTapPause: true, // first tap (or click) show the controlbar, second tap (or click) pause
36
- brandLogoEnabled: false, // Enable/disable brand logo
37
- brandLogoUrl: '', // URL for brand logo image
38
- brandLogoLinkUrl: '', // Optional URL to open when clicking the logo
39
- playlistEnabled: true, // Enable/disable playlist detection
40
- playlistAutoPlay: true, // Auto-play next video when current ends
41
- playlistLoop: false, // Loop playlist when reaching the end
42
- loop: false, // Loop video when it ends (restart from beginning)
43
- volumeSlider: 'horizontal', //volume slider type: 'horizontal' or 'vertical'
44
- // WATERMARK OVERLAY
45
- watermarkUrl: '', // URL of watermark image
46
- watermarkLink: '', // Optional URL to open when clicking watermark
47
- watermarkPosition: 'bottomright', // Position: topleft, topright, bottomleft, bottomright
48
- watermarkTitle: '', // Optional tooltip title
49
- hideWatermark: true, // Hide watermark with controls (default: true)
50
- // ADAPTIVE STREAMING SUPPORT
51
- adaptiveStreaming: false, // Enable DASH/HLS adaptive streaming
52
- dashLibUrl: 'https://cdn.dashjs.org/latest/dash.all.min.js', // Dash.js library URL
53
- hlsLibUrl: 'https://cdn.jsdelivr.net/npm/hls.js@latest', // HLS.js library URL
54
- adaptiveQualityControl: true, // Show quality control for adaptive streams
55
- // AUDIO PLAYER
56
- audiofile: false,
57
- audiowave: false,
58
- // RESOLUTION CONTROL
59
- resolution: "normal", // "normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"
60
- ...options
61
- };
81
+ // Chapter management
82
+ this.chapters = [];
83
+ this.chapterMarkersContainer = null;
84
+ this.chapterTooltip = null;
62
85
 
63
- this.isUserSeeking = false;
64
- this.controlsTimeout = null;
65
- this.titleTimeout = null;
66
- this.currentQualityIndex = 0;
67
- this.qualities = [];
68
- this.originalSources = [];
69
- this.isPiPSupported = this.checkPiPSupport();
70
- this.seekTooltip = null;
71
- this.titleOverlay = null;
72
- this.isPlayerReady = false;
73
-
74
- // Subtitle management
75
- this.textTracks = [];
76
- this.currentSubtitleTrack = null;
77
- this.subtitlesEnabled = false;
78
- this.customSubtitleRenderer = null;
79
-
80
- // Chapter management
81
- this.chapters = [];
82
- this.chapterMarkersContainer = null;
83
- this.chapterTooltip = null;
84
-
85
- // Dual quality indicator management
86
- this.selectedQuality = this.options.defaultQuality || 'auto';
87
- this.currentPlayingQuality = null;
88
- this.qualityMonitorInterval = null;
86
+ // Dual quality indicator management
87
+ this.selectedQuality = this.options.defaultQuality || 'auto';
88
+ this.currentPlayingQuality = null;
89
+ this.qualityMonitorInterval = null;
89
90
 
90
- // Quality change management
91
- this.qualityChangeTimeout = null;
92
- this.isChangingQuality = false;
91
+ // Quality change management
92
+ this.qualityChangeTimeout = null;
93
+ this.isChangingQuality = false;
93
94
 
94
- // Quality debug
95
- this.debugQuality = false;
95
+ // Quality debug
96
+ this.debugQuality = false;
96
97
 
97
- // Auto-hide system
98
- this.autoHideTimer = null;
99
- this.mouseOverControls = false;
100
- this.autoHideDebug = false;
101
- this.autoHideInitialized = false;
98
+ // Auto-hide system
99
+ this.autoHideTimer = null;
100
+ this.mouseOverControls = false;
101
+ this.autoHideDebug = false;
102
+ this.autoHideInitialized = false;
102
103
 
103
- // Poster management
104
- this.posterOverlay = null;
104
+ // Poster management
105
+ this.posterOverlay = null;
105
106
 
106
107
  // Watermark overlay
107
108
  this.watermarkElement = null;
108
109
 
109
- // Custom event system
110
- this.eventCallbacks = {
111
- 'played': [],
112
- 'paused': [],
113
- 'subtitlechange': [],
114
- 'chapterchange': [],
115
- 'pipchange': [],
116
- 'fullscreenchange': [],
117
- 'speedchange': [],
118
- 'timeupdate': [],
119
- 'volumechange': [],
120
- 'qualitychange': [],
121
- 'playlistchange': [],
122
- 'ended': []
110
+ // Custom event system
111
+ this.eventCallbacks = {
112
+ 'played': [],
113
+ 'paused': [],
114
+ 'subtitlechange': [],
115
+ 'chapterchange': [],
116
+ 'pipchange': [],
117
+ 'fullscreenchange': [],
118
+ 'speedchange': [],
119
+ 'timeupdate': [],
120
+ 'volumechange': [],
121
+ 'qualitychange': [],
122
+ 'playlistchange': [],
123
+ 'ended': []
124
+ };
125
+
126
+ // Playlist management
127
+ this.playlist = [];
128
+ this.currentPlaylistIndex = -1;
129
+ this.playlistId = null;
130
+ this.isPlaylistActive = false;
131
+
132
+ // Adaptive streaming management
133
+ this.dashPlayer = null;
134
+ this.hlsPlayer = null;
135
+ this.adaptiveStreamingType = null; // 'dash', 'hls', or null
136
+ this.isAdaptiveStream = false;
137
+ this.adaptiveQualities = [];
138
+ this.librariesLoaded = {
139
+ dash: false,
140
+ hls: false
141
+ };
142
+
143
+ this.lastTimeUpdate = 0; // For throttling timeupdate events
144
+
145
+ if (this.options.language && this.isI18nAvailable()) {
146
+ VideoPlayerTranslations.setLanguage(this.options.language);
147
+ }
148
+ // Apply autoplay if enabled
149
+ if (options.autoplay) {
150
+ this.video.autoplay = true;
151
+ }
152
+
153
+ try {
154
+ this.interceptAutoLoading();
155
+ this.createPlayerStructure();
156
+ this.initializeElements();
157
+ // audio player adaptation
158
+ this.adaptToAudioFile = function () {
159
+ if (this.options.audiofile) {
160
+ // Nascondere video
161
+ if (this.video) {
162
+ this.video.style.display = 'none';
163
+ }
164
+ if (this.container) {
165
+ this.container.classList.add('audio-player');
166
+ }
167
+ if (this.options.audiowave) {
168
+ this.initAudioWave();
169
+ }
170
+ }
123
171
  };
172
+ // Audio wave with Web Audio API
173
+ this.initAudioWave = function () {
174
+ if (!this.video) return;
175
+
176
+ this.audioWaveCanvas = document.createElement('canvas');
177
+ this.audioWaveCanvas.className = 'audio-wave-canvas';
178
+ this.container.appendChild(this.audioWaveCanvas);
179
+
180
+ const canvasCtx = this.audioWaveCanvas.getContext('2d');
181
+ const WIDTH = this.audioWaveCanvas.width = this.container.clientWidth;
182
+ const HEIGHT = this.audioWaveCanvas.height = 60; // altezza onda audio
183
+
184
+ // Setup Web Audio API
185
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
186
+ this.audioCtx = new AudioContext();
187
+ this.analyser = this.audioCtx.createAnalyser();
188
+ this.source = this.audioCtx.createMediaElementSource(this.video);
189
+ this.source.connect(this.analyser);
190
+ this.analyser.connect(this.audioCtx.destination);
191
+
192
+ this.analyser.fftSize = 2048;
193
+ const bufferLength = this.analyser.fftSize;
194
+ const dataArray = new Uint8Array(bufferLength);
195
+
196
+ // canvas
197
+ const draw = () => {
198
+ requestAnimationFrame(draw);
199
+ this.analyser.getByteTimeDomainData(dataArray);
200
+
201
+ canvasCtx.fillStyle = '#222';
202
+ canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
203
+
204
+ canvasCtx.lineWidth = 2;
205
+ canvasCtx.strokeStyle = '#33ccff';
206
+ canvasCtx.beginPath();
207
+
208
+ const sliceWidth = WIDTH / bufferLength;
209
+ let x = 0;
210
+
211
+ for (let i = 0; i < bufferLength; i++) {
212
+ const v = dataArray[i] / 128.0;
213
+ const y = v * HEIGHT / 2;
214
+
215
+ if (i === 0) {
216
+ canvasCtx.moveTo(x, y);
217
+ } else {
218
+ canvasCtx.lineTo(x, y);
219
+ }
124
220
 
125
- // Playlist management
126
- this.playlist = [];
127
- this.currentPlaylistIndex = -1;
128
- this.playlistId = null;
129
- this.isPlaylistActive = false;
130
-
131
- // Adaptive streaming management
132
- this.dashPlayer = null;
133
- this.hlsPlayer = null;
134
- this.adaptiveStreamingType = null; // 'dash', 'hls', or null
135
- this.isAdaptiveStream = false;
136
- this.adaptiveQualities = [];
137
- this.librariesLoaded = {
138
- dash: false,
139
- hls: false
140
- };
221
+ x += sliceWidth;
222
+ }
223
+ canvasCtx.lineTo(WIDTH, HEIGHT / 2);
224
+ canvasCtx.stroke();
225
+ };
141
226
 
142
- this.lastTimeUpdate = 0; // For throttling timeupdate events
227
+ draw();
228
+ };
229
+ this.adaptToAudioFile();
230
+ this.bindEvents();
143
231
 
144
- if (this.options.language && this.isI18nAvailable()) {
145
- VideoPlayerTranslations.setLanguage(this.options.language);
146
- }
147
- // Apply autoplay if enabled
148
- if (options.autoplay) {
149
- this.video.autoplay = true;
232
+ if (this.options.keyboardControls) {
233
+ this.setupKeyboardControls();
150
234
  }
151
235
 
152
- try {
153
- this.interceptAutoLoading();
154
- this.createPlayerStructure();
155
- this.initializeElements();
156
- // audio player adaptation
157
- this.adaptToAudioFile = function () {
158
- if (this.options.audiofile) {
159
- // Nascondere video
160
- if (this.video) {
161
- this.video.style.display = 'none';
162
- }
163
- if (this.container) {
164
- this.container.classList.add('audio-player');
165
- }
166
- if (this.options.audiowave) {
167
- this.initAudioWave();
168
- }
169
- }
170
- };
171
- // Audio wave with Web Audio API
172
- this.initAudioWave = function () {
173
- if (!this.video) return;
174
-
175
- this.audioWaveCanvas = document.createElement('canvas');
176
- this.audioWaveCanvas.className = 'audio-wave-canvas';
177
- this.container.appendChild(this.audioWaveCanvas);
178
-
179
- const canvasCtx = this.audioWaveCanvas.getContext('2d');
180
- const WIDTH = this.audioWaveCanvas.width = this.container.clientWidth;
181
- const HEIGHT = this.audioWaveCanvas.height = 60; // altezza onda audio
182
-
183
- // Setup Web Audio API
184
- const AudioContext = window.AudioContext || window.webkitAudioContext;
185
- this.audioCtx = new AudioContext();
186
- this.analyser = this.audioCtx.createAnalyser();
187
- this.source = this.audioCtx.createMediaElementSource(this.video);
188
- this.source.connect(this.analyser);
189
- this.analyser.connect(this.audioCtx.destination);
190
-
191
- this.analyser.fftSize = 2048;
192
- const bufferLength = this.analyser.fftSize;
193
- const dataArray = new Uint8Array(bufferLength);
194
-
195
- // canvas
196
- const draw = () => {
197
- requestAnimationFrame(draw);
198
- this.analyser.getByteTimeDomainData(dataArray);
199
-
200
- canvasCtx.fillStyle = '#222';
201
- canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
202
-
203
- canvasCtx.lineWidth = 2;
204
- canvasCtx.strokeStyle = '#33ccff';
205
- canvasCtx.beginPath();
206
-
207
- const sliceWidth = WIDTH / bufferLength;
208
- let x = 0;
209
-
210
- for (let i = 0; i < bufferLength; i++) {
211
- const v = dataArray[i] / 128.0;
212
- const y = v * HEIGHT / 2;
213
-
214
- if (i === 0) {
215
- canvasCtx.moveTo(x, y);
216
- } else {
217
- canvasCtx.lineTo(x, y);
218
- }
219
-
220
- x += sliceWidth;
221
- }
222
- canvasCtx.lineTo(WIDTH, HEIGHT / 2);
223
- canvasCtx.stroke();
224
- };
236
+ this.updateVolumeSliderVisual();
237
+ this.initVolumeTooltip();
238
+ this.updateTooltips();
239
+ this.markPlayerReady();
240
+ this.initializePluginSystem();
241
+ this.restoreSourcesAsync();
225
242
 
226
- draw();
227
- };
228
- this.adaptToAudioFile();
229
- this.bindEvents();
243
+ this.initializeSubtitles();
244
+ this.initializeQualityMonitoring();
230
245
 
231
- if (this.options.keyboardControls) {
232
- this.setupKeyboardControls();
233
- }
246
+ this.initializeResolution();
247
+ this.initializeChapters();
248
+ this.initializePoster();
249
+ this.initializeWatermark();
234
250
 
235
- this.updateVolumeSliderVisual();
236
- this.initVolumeTooltip();
237
- this.updateTooltips();
238
- this.markPlayerReady();
239
- this.initializePluginSystem();
240
- this.restoreSourcesAsync();
251
+ } catch (error) {
252
+ if (this.options.debug) console.error('Video player initialization error:', error);
253
+ }
254
+ }
241
255
 
242
- this.initializeSubtitles();
243
- this.initializeQualityMonitoring();
256
+ getPlayerState() {
257
+ return {
258
+ isPlaying: !this.isPaused(),
259
+ isPaused: this.isPaused(),
260
+ currentTime: this.getCurrentTime(),
261
+ duration: this.getDuration(),
262
+ volume: this.getVolume(),
263
+ isMuted: this.isMuted(),
264
+ playbackRate: this.getPlaybackRate(),
265
+ isFullscreen: this.isFullscreenActive(),
266
+ isPictureInPicture: this.isPictureInPictureActive(),
267
+ subtitlesEnabled: this.isSubtitlesEnabled(),
268
+ currentSubtitle: this.getCurrentSubtitleTrack(),
269
+ selectedQuality: this.getSelectedQuality(),
270
+ currentQuality: this.getCurrentPlayingQuality(),
271
+ isAutoQuality: this.isAutoQualityActive()
272
+ };
273
+ }
244
274
 
245
- this.initializeResolution();
246
- this.initializeChapters();
247
- this.initializePoster();
248
- this.initializeWatermark();
275
+ isI18nAvailable() {
276
+ return typeof VideoPlayerTranslations !== 'undefined' &&
277
+ VideoPlayerTranslations !== null &&
278
+ typeof VideoPlayerTranslations.t === 'function';
279
+ }
249
280
 
281
+ t(key) {
282
+ if (this.isI18nAvailable()) {
283
+ try {
284
+ return VideoPlayerTranslations.t(key);
250
285
  } catch (error) {
251
- if (this.options.debug) console.error('Video player initialization error:', error);
286
+ if (this.options.debug) console.warn('Translation error:', error);
252
287
  }
253
288
  }
254
289
 
255
- getPlayerState() {
256
- return {
257
- isPlaying: !this.isPaused(),
258
- isPaused: this.isPaused(),
259
- currentTime: this.getCurrentTime(),
260
- duration: this.getDuration(),
261
- volume: this.getVolume(),
262
- isMuted: this.isMuted(),
263
- playbackRate: this.getPlaybackRate(),
264
- isFullscreen: this.isFullscreenActive(),
265
- isPictureInPicture: this.isPictureInPictureActive(),
266
- subtitlesEnabled: this.isSubtitlesEnabled(),
267
- currentSubtitle: this.getCurrentSubtitleTrack(),
268
- selectedQuality: this.getSelectedQuality(),
269
- currentQuality: this.getCurrentPlayingQuality(),
270
- isAutoQuality: this.isAutoQualityActive()
271
- };
272
- }
290
+ const fallback = {
291
+ 'play_pause': 'Play/Pause (Space)',
292
+ 'mute_unmute': 'Mute/Unmute (M)',
293
+ 'volume': 'Volume',
294
+ 'playback_speed': 'Playback speed',
295
+ 'video_quality': 'Video quality',
296
+ 'picture_in_picture': 'Picture-in-Picture (P)',
297
+ 'fullscreen': 'Fullscreen (F)',
298
+ 'subtitles': 'Subtitles (S)',
299
+ 'subtitles_enable': 'Enable subtitles',
300
+ 'subtitles_disable': 'Disable subtitles',
301
+ 'subtitles_off': 'Off',
302
+ 'auto': 'Auto',
303
+ 'brand_logo': 'Brand logo',
304
+ 'next_video': 'Next video (N)',
305
+ 'prev_video': 'Previous video (P)',
306
+ 'playlist_next': 'Next',
307
+ 'playlist_prev': 'Previous'
308
+ };
309
+
310
+ return fallback[key] || key;
311
+ }
273
312
 
274
- isI18nAvailable() {
275
- return typeof VideoPlayerTranslations !== 'undefined' &&
276
- VideoPlayerTranslations !== null &&
277
- typeof VideoPlayerTranslations.t === 'function';
278
- }
313
+ interceptAutoLoading() {
314
+ this.saveOriginalSources();
315
+ this.disableSources();
279
316
 
280
- t(key) {
281
- if (this.isI18nAvailable()) {
282
- try {
283
- return VideoPlayerTranslations.t(key);
284
- } catch (error) {
285
- if (this.options.debug) console.warn('Translation error:', error);
286
- }
287
- }
317
+ this.video.preload = 'none';
318
+ this.video.controls = false;
319
+ this.video.autoplay = false;
288
320
 
289
- const fallback = {
290
- 'play_pause': 'Play/Pause (Space)',
291
- 'mute_unmute': 'Mute/Unmute (M)',
292
- 'volume': 'Volume',
293
- 'playback_speed': 'Playback speed',
294
- 'video_quality': 'Video quality',
295
- 'picture_in_picture': 'Picture-in-Picture (P)',
296
- 'fullscreen': 'Fullscreen (F)',
297
- 'subtitles': 'Subtitles (S)',
298
- 'subtitles_enable': 'Enable subtitles',
299
- 'subtitles_disable': 'Disable subtitles',
300
- 'subtitles_off': 'Off',
301
- 'auto': 'Auto',
302
- 'brand_logo': 'Brand logo',
303
- 'next_video': 'Next video (N)',
304
- 'prev_video': 'Previous video (P)',
305
- 'playlist_next': 'Next',
306
- 'playlist_prev': 'Previous'
307
- };
308
-
309
- return fallback[key] || key;
321
+ if (this.video.src && this.video.src !== window.location.href) {
322
+ this.originalSrc = this.video.src;
323
+ this.video.removeAttribute('src');
324
+ this.video.src = '';
310
325
  }
311
326
 
312
- interceptAutoLoading() {
313
- this.saveOriginalSources();
314
- this.disableSources();
327
+ this.hideNativePlayer();
315
328
 
316
- this.video.preload = 'none';
317
- this.video.controls = false;
318
- this.video.autoplay = false;
329
+ if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
330
+ }
319
331
 
320
- if (this.video.src && this.video.src !== window.location.href) {
321
- this.originalSrc = this.video.src;
322
- this.video.removeAttribute('src');
323
- this.video.src = '';
332
+ saveOriginalSources() {
333
+ const sources = this.video.querySelectorAll('source');
334
+ this.originalSources = [];
335
+
336
+ sources.forEach((source, index) => {
337
+ if (source.src) {
338
+ this.originalSources.push({
339
+ element: source,
340
+ src: source.src,
341
+ type: source.type || 'video/mp4',
342
+ quality: source.getAttribute('data-quality') || `quality-${index}`,
343
+ index: index
344
+ });
324
345
  }
346
+ });
325
347
 
326
- this.hideNativePlayer();
348
+ if (this.options.debug) console.log(`📁 Saved ${this.originalSources.length} sources originali:`, this.originalSources);
349
+ }
327
350
 
328
- if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
329
- }
351
+ disableSources() {
352
+ const sources = this.video.querySelectorAll('source');
353
+ sources.forEach(source => {
354
+ if (source.src) {
355
+ source.removeAttribute('src');
356
+ }
357
+ });
358
+ }
330
359
 
331
- saveOriginalSources() {
332
- const sources = this.video.querySelectorAll('source');
333
- this.originalSources = [];
360
+ restoreSourcesAsync() {
361
+ setTimeout(() => {
362
+ this.restoreSources();
363
+ }, 200);
364
+ }
334
365
 
335
- sources.forEach((source, index) => {
336
- if (source.src) {
337
- this.originalSources.push({
338
- element: source,
339
- src: source.src,
340
- type: source.type || 'video/mp4',
341
- quality: source.getAttribute('data-quality') || `quality-${index}`,
342
- index: index
343
- });
366
+ async restoreSources() {
367
+ try {
368
+ // Check for adaptive streaming first
369
+ let adaptiveSource = null;
370
+
371
+ if (this.originalSrc) {
372
+ adaptiveSource = this.originalSrc;
373
+ } else if (this.originalSources.length > 0) {
374
+ // Check if any source is adaptive
375
+ const firstSource = this.originalSources[0];
376
+ if (firstSource.src && this.detectStreamType(firstSource.src)) {
377
+ adaptiveSource = firstSource.src;
344
378
  }
345
- });
346
-
347
- if (this.options.debug) console.log(`📁 Saved ${this.originalSources.length} sources originali:`, this.originalSources);
348
- }
379
+ }
349
380
 
350
- disableSources() {
351
- const sources = this.video.querySelectorAll('source');
352
- sources.forEach(source => {
353
- if (source.src) {
354
- source.removeAttribute('src');
381
+ // Initialize adaptive streaming if detected
382
+ if (adaptiveSource && this.options.adaptiveStreaming) {
383
+ const adaptiveInitialized = await this.initializeAdaptiveStreaming(adaptiveSource);
384
+ if (adaptiveInitialized) {
385
+ if (this.options.debug) console.log('📡 Adaptive streaming initialized');
386
+ return;
355
387
  }
356
- });
357
- }
358
-
359
- restoreSourcesAsync() {
360
- setTimeout(() => {
361
- this.restoreSources();
362
- }, 200);
363
- }
388
+ }
364
389
 
365
- async restoreSources() {
366
- try {
367
- // Check for adaptive streaming first
368
- let adaptiveSource = null;
369
-
370
- if (this.originalSrc) {
371
- adaptiveSource = this.originalSrc;
372
- } else if (this.originalSources.length > 0) {
373
- // Check if any source is adaptive
374
- const firstSource = this.originalSources[0];
375
- if (firstSource.src && this.detectStreamType(firstSource.src)) {
376
- adaptiveSource = firstSource.src;
377
- }
378
- }
390
+ // Fallback to traditional sources
391
+ if (this.originalSrc) {
392
+ this.video.src = this.originalSrc;
393
+ }
379
394
 
380
- // Initialize adaptive streaming if detected
381
- if (adaptiveSource && this.options.adaptiveStreaming) {
382
- const adaptiveInitialized = await this.initializeAdaptiveStreaming(adaptiveSource);
383
- if (adaptiveInitialized) {
384
- if (this.options.debug) console.log('📡 Adaptive streaming initialized');
385
- return;
386
- }
395
+ this.originalSources.forEach(sourceData => {
396
+ if (sourceData.element && sourceData.src) {
397
+ sourceData.element.src = sourceData.src;
387
398
  }
399
+ });
388
400
 
389
- // Fallback to traditional sources
390
- if (this.originalSrc) {
391
- this.video.src = this.originalSrc;
392
- }
401
+ this.qualities = this.originalSources.map(s => ({
402
+ src: s.src,
403
+ quality: s.quality,
404
+ type: s.type
405
+ }));
393
406
 
394
- this.originalSources.forEach(sourceData => {
395
- if (sourceData.element && sourceData.src) {
396
- sourceData.element.src = sourceData.src;
397
- }
407
+ if (this.originalSrc && this.qualities.length === 0) {
408
+ this.qualities.push({
409
+ src: this.originalSrc,
410
+ quality: 'default',
411
+ type: 'video/mp4'
398
412
  });
413
+ }
399
414
 
400
- this.qualities = this.originalSources.map(s => ({
401
- src: s.src,
402
- quality: s.quality,
403
- type: s.type
404
- }));
405
-
406
- if (this.originalSrc && this.qualities.length === 0) {
407
- this.qualities.push({
408
- src: this.originalSrc,
409
- quality: 'default',
410
- type: 'video/mp4'
411
- });
412
- }
413
-
414
- if (this.qualities.length > 0) {
415
- this.video.load();
415
+ if (this.qualities.length > 0) {
416
+ this.video.load();
416
417
 
417
- // CRITICAL: Re-initialize subtitles AFTER video.load() completes
418
- this.video.addEventListener('loadedmetadata', () => {
419
- setTimeout(() => {
420
- this.reinitializeSubtitles();
421
- if (this.options.debug) console.log('🔄 Subtitles re-initialized after video load');
422
- }, 300);
423
- }, { once: true });
424
- }
418
+ // CRITICAL: Re-initialize subtitles AFTER video.load() completes
419
+ this.video.addEventListener('loadedmetadata', () => {
420
+ setTimeout(() => {
421
+ this.reinitializeSubtitles();
422
+ if (this.options.debug) console.log('🔄 Subtitles re-initialized after video load');
423
+ }, 300);
424
+ }, { once: true });
425
+ }
425
426
 
426
- if (this.options.debug) console.log('✅ Sources ripristinate:', this.qualities);
427
+ if (this.options.debug) console.log('✅ Sources ripristinate:', this.qualities);
427
428
 
428
- } catch (error) {
429
- if (this.options.debug) console.error('❌ Errore ripristino sources:', error);
430
- }
429
+ } catch (error) {
430
+ if (this.options.debug) console.error('❌ Errore ripristino sources:', error);
431
431
  }
432
+ }
432
433
 
433
434
  reinitializeSubtitles() {
434
435
  if (this.options.debug) console.log('🔄 Re-initializing subtitles...');
@@ -474,630 +475,706 @@ getDefaultSubtitleTrack() {
474
475
  return -1;
475
476
  }
476
477
 
477
- markPlayerReady() {
478
+ markPlayerReady() {
479
+ setTimeout(() => {
480
+ this.isPlayerReady = true;
481
+ if (this.container) {
482
+ this.container.classList.add('player-initialized');
483
+ }
484
+
485
+ if (this.video) {
486
+ this.video.style.visibility = '';
487
+ this.video.style.opacity = '';
488
+ this.video.style.pointerEvents = '';
489
+ }
490
+
491
+ // INITIALIZE AUTO-HIDE AFTER EVERYTHING IS READY
478
492
  setTimeout(() => {
479
- this.isPlayerReady = true;
480
- if (this.container) {
481
- this.container.classList.add('player-initialized');
493
+ if (this.options.autoHide && !this.autoHideInitialized) {
494
+ this.initAutoHide();
482
495
  }
483
496
 
484
- if (this.video) {
485
- this.video.style.visibility = '';
486
- this.video.style.opacity = '';
487
- this.video.style.pointerEvents = '';
497
+ // Fix: Apply default quality (auto or specific)
498
+ if (this.selectedQuality && this.qualities && this.qualities.length > 0) {
499
+ if (this.options.debug) console.log(`🎯 Applying defaultQuality: "${this.selectedQuality}"`);
500
+
501
+ if (this.selectedQuality === 'auto') {
502
+ this.enableAutoQuality();
503
+ } else {
504
+ // Check if requested quality is available
505
+ const requestedQuality = this.qualities.find(q => q.quality === this.selectedQuality);
506
+ if (requestedQuality) {
507
+ if (this.options.debug) console.log(`✅ Quality "${this.selectedQuality}" available`);
508
+ this.setQuality(this.selectedQuality);
509
+ } else {
510
+ if (this.options.debug) console.warn(`⚠️ Quality "${this.selectedQuality}" not available - fallback to auto`);
511
+ if (this.options.debug) console.log('📋 Available qualities:', this.qualities.map(q => q.quality));
512
+ this.enableAutoQuality();
513
+ }
514
+ }
488
515
  }
489
516
 
490
- // INITIALIZE AUTO-HIDE AFTER EVERYTHING IS READY
491
- setTimeout(() => {
492
- if (this.options.autoHide && !this.autoHideInitialized) {
493
- this.initAutoHide();
494
- }
517
+ // Autoplay
518
+ if (this.options.autoplay) {
519
+ if (this.options.debug) console.log('🎬 Autoplay enabled');
520
+ setTimeout(() => {
521
+ this.video.play().catch(error => {
522
+ if (this.options.debug) console.warn('⚠️ Autoplay blocked:', error);
523
+ });
524
+ }, 100);
525
+ }
526
+ }, 200);
495
527
 
496
- // Fix: Apply default quality (auto or specific)
497
- if (this.selectedQuality && this.qualities && this.qualities.length > 0) {
498
- if (this.options.debug) console.log(`🎯 Applying defaultQuality: "${this.selectedQuality}"`);
528
+ }, 100);
529
+ }
499
530
 
500
- if (this.selectedQuality === 'auto') {
501
- this.enableAutoQuality();
502
- } else {
503
- // Check if requested quality is available
504
- const requestedQuality = this.qualities.find(q => q.quality === this.selectedQuality);
505
- if (requestedQuality) {
506
- if (this.options.debug) console.log(`✅ Quality "${this.selectedQuality}" available`);
507
- this.setQuality(this.selectedQuality);
508
- } else {
509
- if (this.options.debug) console.warn(`⚠️ Quality "${this.selectedQuality}" not available - fallback to auto`);
510
- if (this.options.debug) console.log('📋 Available qualities:', this.qualities.map(q => q.quality));
511
- this.enableAutoQuality();
512
- }
513
- }
514
- }
531
+ createPlayerStructure() {
532
+ let wrapper = this.video.closest('.video-wrapper');
533
+ if (!wrapper) {
534
+ wrapper = document.createElement('div');
535
+ wrapper.className = 'video-wrapper';
536
+ this.video.parentNode.insertBefore(wrapper, this.video);
537
+ wrapper.appendChild(this.video);
538
+ }
515
539
 
516
- // Autoplay
517
- if (this.options.autoplay) {
518
- if (this.options.debug) console.log('🎬 Autoplay enabled');
519
- setTimeout(() => {
520
- this.video.play().catch(error => {
521
- if (this.options.debug) console.warn('⚠️ Autoplay blocked:', error);
522
- });
523
- }, 100);
524
- }
525
- }, 200);
540
+ this.container = wrapper;
526
541
 
527
- }, 100);
542
+ this.createInitialLoading();
543
+ this.createLoadingOverlay();
544
+ this.collectVideoQualities();
545
+ this.createControls();
546
+ this.createBrandLogo();
547
+ this.detectPlaylist();
548
+
549
+ if (this.options.showTitleOverlay) {
550
+ this.createTitleOverlay();
528
551
  }
552
+ }
529
553
 
530
- createPlayerStructure() {
531
- let wrapper = this.video.closest('.video-wrapper');
532
- if (!wrapper) {
533
- wrapper = document.createElement('div');
534
- wrapper.className = 'video-wrapper';
535
- this.video.parentNode.insertBefore(wrapper, this.video);
536
- wrapper.appendChild(this.video);
537
- }
554
+ createInitialLoading() {
555
+ const initialLoader = document.createElement('div');
556
+ initialLoader.className = 'initial-loading';
557
+ initialLoader.innerHTML = '<div class="loading-spinner"></div>';
558
+ this.container.appendChild(initialLoader);
559
+ this.initialLoading = initialLoader;
560
+ }
538
561
 
539
- this.container = wrapper;
562
+ collectVideoQualities() {
563
+ if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
564
+ }
540
565
 
541
- this.createInitialLoading();
542
- this.createLoadingOverlay();
543
- this.collectVideoQualities();
544
- this.createControls();
545
- this.createBrandLogo();
546
- this.detectPlaylist();
566
+ createLoadingOverlay() {
567
+ const overlay = document.createElement('div');
568
+ overlay.className = 'loading-overlay';
569
+ overlay.id = 'loadingOverlay-' + this.getUniqueId();
570
+ overlay.innerHTML = '<div class="loading-spinner"></div>';
571
+ this.container.appendChild(overlay);
572
+ this.loadingOverlay = overlay;
573
+ }
547
574
 
548
- if (this.options.showTitleOverlay) {
549
- this.createTitleOverlay();
550
- }
551
- }
575
+ createTitleOverlay() {
576
+ const overlay = document.createElement('div');
577
+ overlay.className = 'title-overlay';
578
+ overlay.id = 'titleOverlay-' + this.getUniqueId();
552
579
 
553
- createInitialLoading() {
554
- const initialLoader = document.createElement('div');
555
- initialLoader.className = 'initial-loading';
556
- initialLoader.innerHTML = '<div class="loading-spinner"></div>';
557
- this.container.appendChild(initialLoader);
558
- this.initialLoading = initialLoader;
559
- }
580
+ const titleText = document.createElement('h2');
581
+ titleText.className = 'title-text';
582
+ titleText.textContent = this.options.videoTitle || '';
560
583
 
561
- collectVideoQualities() {
562
- if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
563
- }
584
+ overlay.appendChild(titleText);
564
585
 
565
- createLoadingOverlay() {
566
- const overlay = document.createElement('div');
567
- overlay.className = 'loading-overlay';
568
- overlay.id = 'loadingOverlay-' + this.getUniqueId();
569
- overlay.innerHTML = '<div class="loading-spinner"></div>';
586
+ if (this.controls) {
587
+ this.container.insertBefore(overlay, this.controls);
588
+ } else {
570
589
  this.container.appendChild(overlay);
571
- this.loadingOverlay = overlay;
572
590
  }
573
591
 
574
- createTitleOverlay() {
575
- const overlay = document.createElement('div');
576
- overlay.className = 'title-overlay';
577
- overlay.id = 'titleOverlay-' + this.getUniqueId();
578
-
579
- const titleText = document.createElement('h2');
580
- titleText.className = 'title-text';
581
- titleText.textContent = this.options.videoTitle || '';
592
+ this.titleOverlay = overlay;
582
593
 
583
- overlay.appendChild(titleText);
594
+ if (this.options.persistentTitle && this.options.videoTitle) {
595
+ this.showTitleOverlay();
596
+ }
597
+ }
584
598
 
585
- if (this.controls) {
586
- this.container.insertBefore(overlay, this.controls);
587
- } else {
588
- this.container.appendChild(overlay);
589
- }
599
+ updateTooltips() {
600
+ if (!this.controls) return;
590
601
 
591
- this.titleOverlay = overlay;
602
+ try {
603
+ this.controls.querySelectorAll('[data-tooltip]').forEach(element => {
604
+ const key = element.getAttribute('data-tooltip');
605
+ element.title = this.t(key);
606
+ });
592
607
 
593
- if (this.options.persistentTitle && this.options.videoTitle) {
594
- this.showTitleOverlay();
608
+ const autoOption = this.controls.querySelector('.quality-option[data-quality="auto"]');
609
+ if (autoOption) {
610
+ autoOption.textContent = this.t('auto');
595
611
  }
612
+ } catch (error) {
613
+ if (this.options.debug) console.warn('Errore aggiornamento tooltip:', error);
596
614
  }
615
+ }
597
616
 
598
- updateTooltips() {
599
- if (!this.controls) return;
600
-
617
+ setLanguage(lang) {
618
+ if (this.isI18nAvailable()) {
601
619
  try {
602
- this.controls.querySelectorAll('[data-tooltip]').forEach(element => {
603
- const key = element.getAttribute('data-tooltip');
604
- element.title = this.t(key);
605
- });
606
-
607
- const autoOption = this.controls.querySelector('.quality-option[data-quality="auto"]');
608
- if (autoOption) {
609
- autoOption.textContent = this.t('auto');
620
+ if (VideoPlayerTranslations.setLanguage(lang)) {
621
+ this.updateTooltips();
622
+ return true;
610
623
  }
611
624
  } catch (error) {
612
- if (this.options.debug) console.warn('Errore aggiornamento tooltip:', error);
625
+ if (this.options.debug) console.warn('Errore cambio lingua:', error);
613
626
  }
614
627
  }
628
+ return false;
629
+ }
615
630
 
616
- setLanguage(lang) {
617
- if (this.isI18nAvailable()) {
618
- try {
619
- if (VideoPlayerTranslations.setLanguage(lang)) {
620
- this.updateTooltips();
621
- return true;
622
- }
623
- } catch (error) {
624
- if (this.options.debug) console.warn('Errore cambio lingua:', error);
631
+ setVideoTitle(title) {
632
+ this.options.videoTitle = title || '';
633
+
634
+ if (this.titleOverlay) {
635
+ const titleElement = this.titleOverlay.querySelector('.title-text');
636
+ if (titleElement) {
637
+ titleElement.textContent = this.options.videoTitle;
638
+ }
639
+
640
+ if (title) {
641
+ this.showTitleOverlay();
642
+
643
+ if (!this.options.persistentTitle) {
644
+ this.clearTitleTimeout();
645
+ this.titleTimeout = setTimeout(() => {
646
+ this.hideTitleOverlay();
647
+ }, 3000);
625
648
  }
626
649
  }
627
- return false;
628
650
  }
629
651
 
630
- setVideoTitle(title) {
631
- this.options.videoTitle = title || '';
652
+ return this;
653
+ }
632
654
 
633
- if (this.titleOverlay) {
634
- const titleElement = this.titleOverlay.querySelector('.title-text');
635
- if (titleElement) {
636
- titleElement.textContent = this.options.videoTitle;
637
- }
655
+ getVideoTitle() {
656
+ return this.options.videoTitle;
657
+ }
638
658
 
639
- if (title) {
640
- this.showTitleOverlay();
659
+ setPersistentTitle(persistent) {
660
+ this.options.persistentTitle = persistent;
641
661
 
642
- if (!this.options.persistentTitle) {
643
- this.clearTitleTimeout();
644
- this.titleTimeout = setTimeout(() => {
645
- this.hideTitleOverlay();
646
- }, 3000);
647
- }
662
+ if (this.titleOverlay && this.options.videoTitle) {
663
+ if (persistent) {
664
+ this.showTitleOverlay();
665
+ this.clearTitleTimeout();
666
+ } else {
667
+ this.titleOverlay.classList.remove('persistent');
668
+ if (this.titleOverlay.classList.contains('show')) {
669
+ this.clearTitleTimeout();
670
+ this.titleTimeout = setTimeout(() => {
671
+ this.hideTitleOverlay();
672
+ }, 3000);
648
673
  }
649
674
  }
675
+ }
650
676
 
651
- return this;
677
+ return this;
678
+ }
679
+
680
+ enableTitleOverlay() {
681
+ if (!this.titleOverlay && !this.options.showTitleOverlay) {
682
+ this.options.showTitleOverlay = true;
683
+ this.createTitleOverlay();
652
684
  }
685
+ return this;
686
+ }
653
687
 
654
- getVideoTitle() {
655
- return this.options.videoTitle;
688
+ disableTitleOverlay() {
689
+ if (this.titleOverlay) {
690
+ this.titleOverlay.remove();
691
+ this.titleOverlay = null;
656
692
  }
693
+ this.options.showTitleOverlay = false;
694
+ return this;
695
+ }
657
696
 
658
- setPersistentTitle(persistent) {
659
- this.options.persistentTitle = persistent;
697
+ getUniqueId() {
698
+ return Math.random().toString(36).substr(2, 9);
699
+ }
660
700
 
661
- if (this.titleOverlay && this.options.videoTitle) {
662
- if (persistent) {
663
- this.showTitleOverlay();
664
- this.clearTitleTimeout();
665
- } else {
666
- this.titleOverlay.classList.remove('persistent');
667
- if (this.titleOverlay.classList.contains('show')) {
668
- this.clearTitleTimeout();
669
- this.titleTimeout = setTimeout(() => {
670
- this.hideTitleOverlay();
671
- }, 3000);
672
- }
673
- }
674
- }
701
+ initializeElements() {
702
+ this.progressContainer = this.controls?.querySelector('.progress-container');
703
+ this.progressFilled = this.controls?.querySelector('.progress-filled');
704
+ this.progressBuffer = this.controls?.querySelector('.progress-buffer');
705
+ this.progressHandle = this.controls?.querySelector('.progress-handle');
706
+ this.seekTooltip = this.controls?.querySelector('.seek-tooltip');
707
+
708
+ this.playPauseBtn = this.controls?.querySelector('.play-pause-btn');
709
+ this.muteBtn = this.controls?.querySelector('.mute-btn');
710
+ this.fullscreenBtn = this.controls?.querySelector('.fullscreen-btn');
711
+ this.speedBtn = this.controls?.querySelector('.speed-btn');
712
+ this.qualityBtn = this.controls?.querySelector('.quality-btn');
713
+ this.pipBtn = this.controls?.querySelector('.pip-btn');
714
+ this.subtitlesBtn = this.controls?.querySelector('.subtitles-btn');
715
+ this.playlistPrevBtn = this.controls?.querySelector('.playlist-prev-btn');
716
+ this.playlistNextBtn = this.controls?.querySelector('.playlist-next-btn');
717
+
718
+ this.playIcon = this.controls?.querySelector('.play-icon');
719
+ this.pauseIcon = this.controls?.querySelector('.pause-icon');
720
+ this.volumeIcon = this.controls?.querySelector('.volume-icon');
721
+ this.muteIcon = this.controls?.querySelector('.mute-icon');
722
+ this.fullscreenIcon = this.controls?.querySelector('.fullscreen-icon');
723
+ this.exitFullscreenIcon = this.controls?.querySelector('.exit-fullscreen-icon');
724
+ this.pipIcon = this.controls?.querySelector('.pip-icon');
725
+ this.pipExitIcon = this.controls?.querySelector('.pip-exit-icon');
726
+
727
+ this.volumeSlider = this.controls?.querySelector('.volume-slider');
728
+ this.currentTimeEl = this.controls?.querySelector('.current-time');
729
+ this.durationEl = this.controls?.querySelector('.duration');
730
+ this.speedMenu = this.controls?.querySelector('.speed-menu');
731
+ this.qualityMenu = this.controls?.querySelector('.quality-menu');
732
+ this.subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
733
+ }
675
734
 
676
- return this;
677
- }
735
+ // Generic method to close all active menus (works with plugins too)
736
+ closeAllMenus() {
737
+ // Find all elements with class ending in '-menu' that have 'active' class
738
+ const allMenus = this.controls?.querySelectorAll('[class*="-menu"].active');
739
+ allMenus?.forEach(menu => {
740
+ menu.classList.remove('active');
741
+ });
678
742
 
679
- enableTitleOverlay() {
680
- if (!this.titleOverlay && !this.options.showTitleOverlay) {
681
- this.options.showTitleOverlay = true;
682
- this.createTitleOverlay();
683
- }
684
- return this;
685
- }
743
+ // Remove active state from all control buttons
744
+ const allButtons = this.controls?.querySelectorAll('.control-btn.active');
745
+ allButtons?.forEach(btn => {
746
+ btn.classList.remove('active');
747
+ });
748
+ }
686
749
 
687
- disableTitleOverlay() {
688
- if (this.titleOverlay) {
689
- this.titleOverlay.remove();
690
- this.titleOverlay = null;
691
- }
692
- this.options.showTitleOverlay = false;
693
- return this;
694
- }
750
+ // Generic menu toggle setup (works with core menus and plugin menus)
751
+ setupMenuToggles() {
752
+ // Delegate click events to control bar for any button with associated menu
753
+ if (this.controls) {
754
+ this.controls.addEventListener('click', (e) => {
755
+ // Find if clicked element is a control button or inside one
756
+ const button = e.target.closest('.control-btn');
757
+
758
+ if (!button) return;
759
+
760
+ // Get button classes to find associated menu
761
+ const buttonClasses = button.className.split(' ');
762
+ let menuClass = null;
763
+
764
+ // Find if this button has an associated menu (e.g., speed-btn -> speed-menu)
765
+ for (const cls of buttonClasses) {
766
+ if (cls.endsWith('-btn')) {
767
+ const menuName = cls.replace('-btn', '-menu');
768
+ const menu = this.controls.querySelector('.' + menuName);
769
+ if (menu) {
770
+ menuClass = menuName;
771
+ break;
772
+ }
773
+ }
774
+ }
695
775
 
696
- getUniqueId() {
697
- return Math.random().toString(36).substr(2, 9);
698
- }
776
+ if (!menuClass) return;
699
777
 
700
- initializeElements() {
701
- this.progressContainer = this.controls?.querySelector('.progress-container');
702
- this.progressFilled = this.controls?.querySelector('.progress-filled');
703
- this.progressBuffer = this.controls?.querySelector('.progress-buffer');
704
- this.progressHandle = this.controls?.querySelector('.progress-handle');
705
- this.seekTooltip = this.controls?.querySelector('.seek-tooltip');
778
+ e.stopPropagation();
706
779
 
707
- this.playPauseBtn = this.controls?.querySelector('.play-pause-btn');
708
- this.muteBtn = this.controls?.querySelector('.mute-btn');
709
- this.fullscreenBtn = this.controls?.querySelector('.fullscreen-btn');
710
- this.speedBtn = this.controls?.querySelector('.speed-btn');
711
- this.qualityBtn = this.controls?.querySelector('.quality-btn');
712
- this.pipBtn = this.controls?.querySelector('.pip-btn');
713
- this.subtitlesBtn = this.controls?.querySelector('.subtitles-btn');
714
- this.playlistPrevBtn = this.controls?.querySelector('.playlist-prev-btn');
715
- this.playlistNextBtn = this.controls?.querySelector('.playlist-next-btn');
780
+ // Get the menu element
781
+ const menu = this.controls.querySelector('.' + menuClass);
782
+ const isOpen = menu.classList.contains('active');
716
783
 
717
- this.playIcon = this.controls?.querySelector('.play-icon');
718
- this.pauseIcon = this.controls?.querySelector('.pause-icon');
719
- this.volumeIcon = this.controls?.querySelector('.volume-icon');
720
- this.muteIcon = this.controls?.querySelector('.mute-icon');
721
- this.fullscreenIcon = this.controls?.querySelector('.fullscreen-icon');
722
- this.exitFullscreenIcon = this.controls?.querySelector('.exit-fullscreen-icon');
723
- this.pipIcon = this.controls?.querySelector('.pip-icon');
724
- this.pipExitIcon = this.controls?.querySelector('.pip-exit-icon');
784
+ // Close all menus first
785
+ this.closeAllMenus();
725
786
 
726
- this.volumeSlider = this.controls?.querySelector('.volume-slider');
727
- this.currentTimeEl = this.controls?.querySelector('.current-time');
728
- this.durationEl = this.controls?.querySelector('.duration');
729
- this.speedMenu = this.controls?.querySelector('.speed-menu');
730
- this.qualityMenu = this.controls?.querySelector('.quality-menu');
731
- this.subtitlesMenu = this.controls?.querySelector('.subtitles-menu');
787
+ // If menu was closed, open it
788
+ if (!isOpen) {
789
+ menu.classList.add('active');
790
+ button.classList.add('active');
791
+ }
792
+ });
732
793
  }
733
794
 
734
- updateVolumeSliderVisual() {
735
- if (!this.video || !this.container) return;
795
+ // Close menus when clicking outside controls
796
+ document.addEventListener('click', (e) => {
797
+ if (!this.controls?.contains(e.target)) {
798
+ this.closeAllMenus();
799
+ }
800
+ });
801
+ }
736
802
 
737
- const volume = this.video.muted ? 0 : this.video.volume;
738
- const percentage = Math.round(volume * 100);
803
+ updateVolumeSliderVisual() {
804
+ if (!this.video || !this.container) return;
739
805
 
740
- this.container.style.setProperty('--player-volume-fill', percentage + '%');
806
+ const volume = this.video.muted ? 0 : this.video.volume;
807
+ const percentage = Math.round(volume * 100);
741
808
 
742
- if (this.volumeSlider) {
743
- this.volumeSlider.value = percentage;
744
- }
809
+ this.container.style.setProperty('--player-volume-fill', percentage + '%');
810
+
811
+ if (this.volumeSlider) {
812
+ this.volumeSlider.value = percentage;
745
813
  }
814
+ }
746
815
 
747
- createVolumeTooltip() {
748
- const volumeContainer = this.controls?.querySelector('.volume-container');
749
- if (!volumeContainer || volumeContainer.querySelector('.volume-tooltip')) {
750
- return; // Tooltip already present
751
- }
816
+ createVolumeTooltip() {
817
+ const volumeContainer = this.controls?.querySelector('.volume-container');
818
+ if (!volumeContainer || volumeContainer.querySelector('.volume-tooltip')) {
819
+ return; // Tooltip already present
820
+ }
752
821
 
753
- const tooltip = document.createElement('div');
754
- tooltip.className = 'volume-tooltip';
755
- tooltip.textContent = '50%';
756
- volumeContainer.appendChild(tooltip);
822
+ const tooltip = document.createElement('div');
823
+ tooltip.className = 'volume-tooltip';
824
+ tooltip.textContent = '50%';
825
+ volumeContainer.appendChild(tooltip);
757
826
 
758
- this.volumeTooltip = tooltip;
827
+ this.volumeTooltip = tooltip;
759
828
 
760
- if (this.options.debug) {
761
- console.log('Dynamic volume tooltip created');
762
- }
829
+ if (this.options.debug) {
830
+ console.log('Dynamic volume tooltip created');
763
831
  }
832
+ }
764
833
 
765
- updateVolumeTooltip() {
766
- if (!this.volumeTooltip || !this.video) return;
834
+ updateVolumeTooltip() {
835
+ if (!this.volumeTooltip || !this.video) return;
767
836
 
768
- const volume = Math.round(this.video.volume * 100);
769
- this.volumeTooltip.textContent = volume + '%';
837
+ const volume = Math.round(this.video.volume * 100);
838
+ this.volumeTooltip.textContent = volume + '%';
770
839
 
771
- // Aggiorna la posizione del tooltip
772
- this.updateVolumeTooltipPosition(this.video.volume);
840
+ // Aggiorna la posizione del tooltip
841
+ this.updateVolumeTooltipPosition(this.video.volume);
773
842
 
774
- if (this.options.debug) {
775
- console.log('Volume tooltip updated:', volume + '%');
776
- }
843
+ if (this.options.debug) {
844
+ console.log('Volume tooltip updated:', volume + '%');
777
845
  }
846
+ }
778
847
 
779
- updateVolumeTooltipPosition(volumeValue = null) {
780
- if (!this.volumeTooltip || !this.video) return;
848
+ updateVolumeTooltipPosition(volumeValue = null) {
849
+ if (!this.volumeTooltip || !this.video) return;
781
850
 
782
- const volumeSlider = this.controls?.querySelector('.volume-slider');
783
- if (!volumeSlider) return;
851
+ const volumeSlider = this.controls?.querySelector('.volume-slider');
852
+ if (!volumeSlider) return;
784
853
 
785
- // If no volume provided, use current volume
786
- if (volumeValue === null) {
787
- volumeValue = this.video.volume;
788
- }
854
+ // If no volume provided, use current volume
855
+ if (volumeValue === null) {
856
+ volumeValue = this.video.volume;
857
+ }
789
858
 
790
- // Calcola la posizione esatta del thumb
791
- const sliderRect = volumeSlider.getBoundingClientRect();
792
- const sliderWidth = sliderRect.width;
859
+ // Calcola la posizione esatta del thumb
860
+ const sliderRect = volumeSlider.getBoundingClientRect();
861
+ const sliderWidth = sliderRect.width;
793
862
 
794
- // Thumb size is typically 14px (as defined in CSS)
795
- const thumbSize = 14; // var(--player-volume-handle-size)
863
+ // Thumb size is typically 14px (as defined in CSS)
864
+ const thumbSize = 14; // var(--player-volume-handle-size)
796
865
 
797
- // Calcola la posizione del centro del thumb
798
- // Il thumb si muove da thumbSize/2 a (sliderWidth - thumbSize/2)
799
- const availableWidth = sliderWidth - thumbSize;
800
- const thumbCenterPosition = (thumbSize / 2) + (availableWidth * volumeValue);
866
+ // Calcola la posizione del centro del thumb
867
+ // Il thumb si muove da thumbSize/2 a (sliderWidth - thumbSize/2)
868
+ const availableWidth = sliderWidth - thumbSize;
869
+ const thumbCenterPosition = (thumbSize / 2) + (availableWidth * volumeValue);
801
870
 
802
- // Converti in percentuale relativa al container dello slider
803
- const percentage = (thumbCenterPosition / sliderWidth) * 100;
871
+ // Converti in percentuale relativa al container dello slider
872
+ const percentage = (thumbCenterPosition / sliderWidth) * 100;
804
873
 
805
- // Posiziona il tooltip
806
- this.volumeTooltip.style.left = percentage + '%';
874
+ // Posiziona il tooltip
875
+ this.volumeTooltip.style.left = percentage + '%';
807
876
 
808
- if (this.options.debug) {
809
- console.log('Volume tooltip position updated:', {
810
- volumeValue: volumeValue,
811
- percentage: percentage + '%',
812
- thumbCenter: thumbCenterPosition,
813
- sliderWidth: sliderWidth
814
- });
815
- }
877
+ if (this.options.debug) {
878
+ console.log('Volume tooltip position updated:', {
879
+ volumeValue: volumeValue,
880
+ percentage: percentage + '%',
881
+ thumbCenter: thumbCenterPosition,
882
+ sliderWidth: sliderWidth
883
+ });
816
884
  }
885
+ }
817
886
 
818
- initVolumeTooltip() {
819
- this.createVolumeTooltip();
820
- this.setupVolumeTooltipEvents();
887
+ initVolumeTooltip() {
888
+ this.createVolumeTooltip();
821
889
 
822
- // Set initial position immediately
823
- setTimeout(() => {
824
- if (this.volumeTooltip && this.video) {
825
- this.updateVolumeTooltipPosition(this.video.volume);
826
- this.updateVolumeTooltip();
827
- }
828
- }, 50); // Shorter delay for faster initialization
829
- }
890
+ // Set initial position immediately
891
+ setTimeout(() => {
892
+ if (this.volumeTooltip && this.video) {
893
+ this.updateVolumeTooltipPosition(this.video.volume);
894
+ this.updateVolumeTooltip();
895
+ }
896
+ }, 50); // Shorter delay for faster initialization
897
+ }
830
898
 
831
- updateVolumeSliderVisualWithTooltip() {
832
- const volumeSlider = this.controls?.querySelector('.volume-slider');
833
- if (!volumeSlider || !this.video) return;
899
+ updateVolumeSliderVisualWithTooltip() {
900
+ const volumeSlider = this.controls?.querySelector('.volume-slider');
901
+ if (!volumeSlider || !this.video) return;
834
902
 
835
- const volume = this.video.volume || 0;
836
- const percentage = Math.round(volume * 100);
903
+ const volume = this.video.volume || 0;
904
+ const percentage = Math.round(volume * 100);
837
905
 
838
- volumeSlider.value = volume;
906
+ volumeSlider.value = volume;
839
907
 
840
- // Update CSS custom property per il riempimento visuale
841
- const volumeFillPercentage = percentage + '%';
842
- volumeSlider.style.setProperty('--player-volume-fill', volumeFillPercentage);
908
+ // Update CSS custom property per il riempimento visuale
909
+ const volumeFillPercentage = percentage + '%';
910
+ volumeSlider.style.setProperty('--player-volume-fill', volumeFillPercentage);
843
911
 
844
- // Aggiorna anche il tooltip se presente (testo e posizione)
845
- this.updateVolumeTooltip();
912
+ // Aggiorna anche il tooltip se presente (testo e posizione)
913
+ this.updateVolumeTooltip();
846
914
 
847
- if (this.options.debug) {
848
- console.log('Volume slider aggiornato:', {
849
- volume: volume,
850
- percentage: percentage,
851
- fillPercentage: volumeFillPercentage
852
- });
853
- }
915
+ if (this.options.debug) {
916
+ console.log('Volume slider aggiornato:', {
917
+ volume: volume,
918
+ percentage: percentage,
919
+ fillPercentage: volumeFillPercentage
920
+ });
854
921
  }
922
+ }
855
923
 
856
- // Volume slider type: 'horizontal' or 'vertical'
857
- setVolumeSliderOrientation(orientation) {
858
- if (!['horizontal', 'vertical'].includes(orientation)) {
859
- if (this.options.debug) console.warn('Invalid volume slider orientation:', orientation);
924
+ /**
925
+ * Set mobile volume slider visibility
926
+ * @param {String} mode - 'show' (horizontal popup) or 'hide' (no slider on mobile)
927
+ * @returns {Object} this
928
+ */
929
+ setMobileVolumeSlider(mode) {
930
+ if (!['show', 'hide'].includes(mode)) {
931
+ if (this.options.debug) console.warn('Invalid mobile volume slider mode:', mode);
860
932
  return this;
861
933
  }
862
934
 
863
- this.options.volumeSlider = orientation;
935
+ this.options.mobileVolumeSlider = mode;
864
936
  const volumeContainer = this.controls?.querySelector('.volume-container');
865
937
  if (volumeContainer) {
866
- volumeContainer.setAttribute('data-orientation', orientation);
938
+ // Set data attribute for CSS to use
939
+ volumeContainer.setAttribute('data-mobile-slider', mode);
940
+ if (this.options.debug) console.log('Mobile volume slider set to:', mode);
867
941
  }
868
-
869
- if (this.options.debug) console.log('Volume slider orientation set to:', orientation);
870
942
  return this;
871
943
  }
872
944
 
873
- getVolumeSliderOrientation() {
874
- return this.options.volumeSlider;
945
+ /**
946
+ * Get mobile volume slider mode
947
+ * @returns {String} Current mobile volume slider mode
948
+ */
949
+ getMobileVolumeSlider() {
950
+ return this.options.mobileVolumeSlider;
875
951
  }
876
952
 
953
+ initVolumeTooltip() {
877
954
 
878
- initVolumeTooltip() {
955
+ this.createVolumeTooltip();
879
956
 
880
- this.createVolumeTooltip();
957
+ setTimeout(() => {
958
+ this.updateVolumeTooltip();
959
+ }, 200);
881
960
 
882
- // Setup events
883
- this.setupVolumeTooltipEvents();
961
+ if (this.options.debug) {
962
+ console.log('Dynamic volume tooltip inizializzation');
963
+ }
964
+ }
884
965
 
885
- setTimeout(() => {
886
- this.updateVolumeTooltip();
887
- }, 200);
966
+ setupSeekTooltip() {
967
+ if (!this.options.showSeekTooltip || !this.progressContainer || !this.seekTooltip) return;
888
968
 
889
- if (this.options.debug) {
890
- console.log('Dynamic volume tooltip inizializzato');
969
+ this.progressContainer.addEventListener('mouseenter', () => {
970
+ if (this.seekTooltip) {
971
+ this.seekTooltip.classList.add('visible');
891
972
  }
892
- }
973
+ });
893
974
 
894
- setupSeekTooltip() {
895
- if (!this.options.showSeekTooltip || !this.progressContainer || !this.seekTooltip) return;
975
+ this.progressContainer.addEventListener('mouseleave', () => {
976
+ if (this.seekTooltip) {
977
+ this.seekTooltip.classList.remove('visible');
978
+ }
979
+ });
896
980
 
897
- this.progressContainer.addEventListener('mouseenter', () => {
898
- if (this.seekTooltip) {
899
- this.seekTooltip.classList.add('visible');
900
- }
901
- });
981
+ this.progressContainer.addEventListener('mousemove', (e) => {
982
+ this.updateSeekTooltip(e);
983
+ });
984
+ }
902
985
 
903
- this.progressContainer.addEventListener('mouseleave', () => {
904
- if (this.seekTooltip) {
905
- this.seekTooltip.classList.remove('visible');
906
- }
907
- });
986
+ updateSeekTooltip(e) {
987
+ if (!this.seekTooltip || !this.progressContainer || !this.video || !this.video.duration) return;
908
988
 
909
- this.progressContainer.addEventListener('mousemove', (e) => {
910
- this.updateSeekTooltip(e);
911
- });
912
- }
989
+ const rect = this.progressContainer.getBoundingClientRect();
990
+ const clickX = e.clientX - rect.left;
991
+ const percentage = Math.max(0, Math.min(1, clickX / rect.width));
992
+ const targetTime = percentage * this.video.duration;
913
993
 
914
- updateSeekTooltip(e) {
915
- if (!this.seekTooltip || !this.progressContainer || !this.video || !this.video.duration) return;
994
+ this.seekTooltip.textContent = this.formatTime(targetTime);
916
995
 
917
- const rect = this.progressContainer.getBoundingClientRect();
918
- const clickX = e.clientX - rect.left;
919
- const percentage = Math.max(0, Math.min(1, clickX / rect.width));
920
- const targetTime = percentage * this.video.duration;
996
+ const tooltipRect = this.seekTooltip.getBoundingClientRect();
997
+ let leftPosition = clickX;
921
998
 
922
- this.seekTooltip.textContent = this.formatTime(targetTime);
999
+ const tooltipWidth = tooltipRect.width || 50;
1000
+ const containerWidth = rect.width;
923
1001
 
924
- const tooltipRect = this.seekTooltip.getBoundingClientRect();
925
- let leftPosition = clickX;
1002
+ leftPosition = Math.max(tooltipWidth / 2, Math.min(containerWidth - tooltipWidth / 2, clickX));
926
1003
 
927
- const tooltipWidth = tooltipRect.width || 50;
928
- const containerWidth = rect.width;
1004
+ this.seekTooltip.style.left = leftPosition + 'px';
1005
+ }
929
1006
 
930
- leftPosition = Math.max(tooltipWidth / 2, Math.min(containerWidth - tooltipWidth / 2, clickX));
1007
+ play() {
1008
+ if (!this.video || this.isChangingQuality) return;
931
1009
 
932
- this.seekTooltip.style.left = leftPosition + 'px';
933
- }
1010
+ this.video.play().catch(err => {
1011
+ if (this.options.debug) console.log('Play failed:', err);
1012
+ });
934
1013
 
935
- play() {
936
- if (!this.video || this.isChangingQuality) return;
1014
+ if (this.playIcon) this.playIcon.classList.add('hidden');
1015
+ if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
937
1016
 
938
- this.video.play().catch(err => {
939
- if (this.options.debug) console.log('Play failed:', err);
940
- });
1017
+ // Trigger event played
1018
+ this.triggerEvent('played', {
1019
+ currentTime: this.getCurrentTime(),
1020
+ duration: this.getDuration()
1021
+ });
1022
+ }
941
1023
 
942
- if (this.playIcon) this.playIcon.classList.add('hidden');
943
- if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
1024
+ pause() {
1025
+ if (!this.video) return;
944
1026
 
945
- // Trigger event played
946
- this.triggerEvent('played', {
947
- currentTime: this.getCurrentTime(),
948
- duration: this.getDuration()
949
- });
950
- }
1027
+ this.video.pause();
1028
+ if (this.playIcon) this.playIcon.classList.remove('hidden');
1029
+ if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
951
1030
 
952
- pause() {
953
- if (!this.video) return;
1031
+ // Trigger paused event
1032
+ this.triggerEvent('paused', {
1033
+ currentTime: this.getCurrentTime(),
1034
+ duration: this.getDuration()
1035
+ });
1036
+ }
954
1037
 
955
- this.video.pause();
956
- if (this.playIcon) this.playIcon.classList.remove('hidden');
957
- if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
1038
+ updateVolume(value) {
1039
+ if (!this.video) return;
958
1040
 
959
- // Trigger paused event
960
- this.triggerEvent('paused', {
961
- currentTime: this.getCurrentTime(),
962
- duration: this.getDuration()
963
- });
964
- }
1041
+ const previousVolume = this.video.volume;
1042
+ const previousMuted = this.video.muted;
965
1043
 
966
- updateVolume(value) {
967
- if (!this.video) return;
1044
+ this.video.volume = Math.max(0, Math.min(1, value / 100));
968
1045
 
969
- const previousVolume = this.video.volume;
970
- const previousMuted = this.video.muted;
1046
+ if (this.video.volume > 0 && this.video.muted) {
1047
+ this.video.muted = false;
1048
+ }
971
1049
 
972
- this.video.volume = Math.max(0, Math.min(1, value / 100));
973
- if (this.volumeSlider) this.volumeSlider.value = value;
974
- this.updateMuteButton();
975
- this.updateVolumeSliderVisual();
976
- this.initVolumeTooltip();
1050
+ if (this.volumeSlider) this.volumeSlider.value = value;
1051
+ this.updateMuteButton();
1052
+ this.updateVolumeSliderVisual();
1053
+ this.initVolumeTooltip();
977
1054
 
978
- // Triggers volumechange event if there is a significant change
979
- if (Math.abs(previousVolume - this.video.volume) > 0.01 || previousMuted !== this.video.muted) {
980
- this.triggerEvent('volumechange', {
981
- volume: this.getVolume(),
982
- muted: this.isMuted(),
983
- previousVolume: previousVolume,
984
- previousMuted: previousMuted
985
- });
986
- }
1055
+ // Triggers volumechange event if there is a significant change
1056
+ if (Math.abs(previousVolume - this.video.volume) > 0.01 || previousMuted !== this.video.muted) {
1057
+ this.triggerEvent('volumechange', {
1058
+ volume: this.getVolume(),
1059
+ muted: this.isMuted(),
1060
+ previousVolume: previousVolume,
1061
+ previousMuted: previousMuted
1062
+ });
987
1063
  }
1064
+ }
988
1065
 
989
- changeVolume(delta) {
990
- if (!this.video) return;
1066
+ changeVolume(delta) {
1067
+ if (!this.video) return;
991
1068
 
992
- const newVolume = Math.max(0, Math.min(1, this.video.volume + delta));
993
- this.updateVolume(newVolume * 100);
994
- this.updateVolumeSliderVisual();
995
- this.initVolumeTooltip();
996
- }
1069
+ const newVolume = Math.max(0, Math.min(1, this.video.volume + delta));
1070
+ this.updateVolume(newVolume * 100);
1071
+ this.updateVolumeSliderVisual();
1072
+ this.initVolumeTooltip();
1073
+ }
997
1074
 
998
- updateProgress() {
999
- if (!this.video || !this.progressFilled || !this.progressHandle || this.isUserSeeking) return;
1075
+ updateProgress() {
1076
+ if (!this.video || !this.progressFilled || !this.progressHandle || this.isUserSeeking) return;
1000
1077
 
1001
- if (this.video.duration && !isNaN(this.video.duration)) {
1002
- const progress = (this.video.currentTime / this.video.duration) * 100;
1003
- this.progressFilled.style.width = progress + '%';
1004
- this.progressHandle.style.left = progress + '%';
1005
- }
1078
+ if (this.video.duration && !isNaN(this.video.duration)) {
1079
+ const progress = (this.video.currentTime / this.video.duration) * 100;
1080
+ this.progressFilled.style.width = progress + '%';
1081
+ this.progressHandle.style.left = progress + '%';
1082
+ }
1006
1083
 
1007
- this.updateTimeDisplay();
1084
+ this.updateTimeDisplay();
1008
1085
 
1009
- // Trigger timeupdate event (with throttling to avoid too many events)
1010
- if (!this.lastTimeUpdate || Date.now() - this.lastTimeUpdate > 250) {
1011
- this.triggerEvent('timeupdate', {
1012
- currentTime: this.getCurrentTime(),
1013
- duration: this.getDuration(),
1014
- progress: (this.getCurrentTime() / this.getDuration()) * 100 || 0
1015
- });
1016
- this.lastTimeUpdate = Date.now();
1017
- }
1086
+ // Trigger timeupdate event (with throttling to avoid too many events)
1087
+ if (!this.lastTimeUpdate || Date.now() - this.lastTimeUpdate > 250) {
1088
+ this.triggerEvent('timeupdate', {
1089
+ currentTime: this.getCurrentTime(),
1090
+ duration: this.getDuration(),
1091
+ progress: (this.getCurrentTime() / this.getDuration()) * 100 || 0
1092
+ });
1093
+ this.lastTimeUpdate = Date.now();
1018
1094
  }
1095
+ }
1019
1096
 
1020
- updateBuffer() {
1021
- if (!this.video || !this.progressBuffer) return;
1097
+ updateBuffer() {
1098
+ if (!this.video || !this.progressBuffer) return;
1022
1099
 
1023
- try {
1024
- if (this.video.buffered && this.video.buffered.length > 0 && this.video.duration) {
1025
- const buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
1026
- this.progressBuffer.style.width = buffered + '%';
1027
- }
1028
- } catch (error) {
1029
- if (this.options.debug) console.log('Buffer update error (non-critical):', error);
1100
+ try {
1101
+ if (this.video.buffered && this.video.buffered.length > 0 && this.video.duration) {
1102
+ const buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
1103
+ this.progressBuffer.style.width = buffered + '%';
1030
1104
  }
1105
+ } catch (error) {
1106
+ if (this.options.debug) console.log('Buffer update error (non-critical):', error);
1031
1107
  }
1108
+ }
1032
1109
 
1033
- startSeeking(e) {
1034
- if (this.isChangingQuality) return;
1110
+ startSeeking(e) {
1111
+ if (this.isChangingQuality) return;
1035
1112
 
1036
- this.isUserSeeking = true;
1037
- this.seek(e);
1038
- e.preventDefault();
1113
+ this.isUserSeeking = true;
1114
+ this.seek(e);
1115
+ e.preventDefault();
1039
1116
 
1040
- // Show controls during seeking
1041
- if (this.options.autoHide && this.autoHideInitialized) {
1042
- this.showControlsNow();
1043
- this.resetAutoHideTimer();
1044
- }
1117
+ // Show controls during seeking
1118
+ if (this.options.autoHide && this.autoHideInitialized) {
1119
+ this.showControlsNow();
1120
+ this.resetAutoHideTimer();
1045
1121
  }
1122
+ }
1046
1123
 
1047
- continueSeeking(e) {
1048
- if (this.isUserSeeking && !this.isChangingQuality) {
1049
- this.seek(e);
1050
- }
1124
+ continueSeeking(e) {
1125
+ if (this.isUserSeeking && !this.isChangingQuality) {
1126
+ this.seek(e);
1051
1127
  }
1128
+ }
1052
1129
 
1053
- endSeeking() {
1054
- this.isUserSeeking = false;
1055
- }
1130
+ endSeeking() {
1131
+ this.isUserSeeking = false;
1132
+ }
1056
1133
 
1057
- seek(e) {
1058
- if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
1134
+ seek(e) {
1135
+ if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
1059
1136
 
1060
- const rect = this.progressContainer.getBoundingClientRect();
1061
- const clickX = e.clientX - rect.left;
1062
- const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1137
+ const rect = this.progressContainer.getBoundingClientRect();
1138
+ const clickX = e.clientX - rect.left;
1139
+ const percentage = Math.max(0, Math.min(1, clickX / rect.width));
1063
1140
 
1064
- if (this.video.duration && !isNaN(this.video.duration)) {
1065
- this.video.currentTime = percentage * this.video.duration;
1066
- const progress = percentage * 100;
1067
- this.progressFilled.style.width = progress + '%';
1068
- this.progressHandle.style.left = progress + '%';
1069
- }
1141
+ if (this.video.duration && !isNaN(this.video.duration)) {
1142
+ this.video.currentTime = percentage * this.video.duration;
1143
+ const progress = percentage * 100;
1144
+ this.progressFilled.style.width = progress + '%';
1145
+ this.progressHandle.style.left = progress + '%';
1070
1146
  }
1147
+ }
1071
1148
 
1072
- updateDuration() {
1073
- if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
1074
- this.durationEl.textContent = this.formatTime(this.video.duration);
1075
- }
1149
+ updateDuration() {
1150
+ if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
1151
+ this.durationEl.textContent = this.formatTime(this.video.duration);
1076
1152
  }
1153
+ }
1077
1154
 
1078
- changeSpeed(e) {
1079
- if (!this.video || !e.target.classList.contains('speed-option') || this.isChangingQuality) return;
1080
-
1081
- const speed = parseFloat(e.target.getAttribute('data-speed'));
1082
- if (speed && speed > 0) {
1083
- this.video.playbackRate = speed;
1084
- if (this.speedBtn) this.speedBtn.textContent = speed + 'x';
1155
+ changeSpeed(e) {
1156
+ if (!this.video || !e.target.classList.contains('speed-option') || this.isChangingQuality) return;
1085
1157
 
1086
- if (this.speedMenu) {
1087
- this.speedMenu.querySelectorAll('.speed-option').forEach(option => {
1088
- option.classList.remove('active');
1089
- });
1090
- e.target.classList.add('active');
1091
- }
1158
+ const speed = parseFloat(e.target.getAttribute('data-speed'));
1159
+ if (speed && speed > 0) {
1160
+ this.video.playbackRate = speed;
1161
+ if (this.speedBtn) this.speedBtn.textContent = speed + 'x';
1092
1162
 
1093
- // Trigger speedchange event
1094
- const previousSpeed = this.video.playbackRate;
1095
- this.triggerEvent('speedchange', {
1096
- speed: speed,
1097
- previousSpeed: previousSpeed
1163
+ if (this.speedMenu) {
1164
+ this.speedMenu.querySelectorAll('.speed-option').forEach(option => {
1165
+ option.classList.remove('active');
1098
1166
  });
1167
+ e.target.classList.add('active');
1099
1168
  }
1169
+
1170
+ // Trigger speedchange event
1171
+ const previousSpeed = this.video.playbackRate;
1172
+ this.triggerEvent('speedchange', {
1173
+ speed: speed,
1174
+ previousSpeed: previousSpeed
1175
+ });
1100
1176
  }
1177
+ }
1101
1178
 
1102
1179
  onVideoEnded() {
1103
1180
  if (this.playIcon) this.playIcon.classList.remove('hidden');
@@ -1123,217 +1200,270 @@ onVideoEnded() {
1123
1200
  });
1124
1201
  }
1125
1202
 
1126
- getCurrentTime() { return this.video ? this.video.currentTime || 0 : 0; }
1203
+ /**
1204
+ * Handle video loading errors (404, 503, network errors, etc.)
1205
+ * Triggers 'ended' event to allow proper cleanup and playlist continuation
1206
+ */
1207
+ onVideoError(error) {
1208
+ if (this.options.debug) {
1209
+ console.error('Video loading error detected:', {
1210
+ error: error,
1211
+ code: this.video?.error?.code,
1212
+ message: this.video?.error?.message,
1213
+ src: this.video?.currentSrc || this.video?.src
1214
+ });
1215
+ }
1127
1216
 
1128
- setCurrentTime(time) { if (this.video && typeof time === 'number' && time >= 0 && !this.isChangingQuality) { this.video.currentTime = time; } }
1217
+ // Hide loading overlay
1218
+ this.hideLoading();
1219
+ if (this.initialLoading) {
1220
+ this.initialLoading.style.display = 'none';
1221
+ }
1129
1222
 
1130
- getDuration() { return this.video && this.video.duration ? this.video.duration : 0; }
1223
+ // Remove quality-changing class if present
1224
+ if (this.video?.classList) {
1225
+ this.video.classList.remove('quality-changing');
1226
+ }
1131
1227
 
1132
- getVolume() { return this.video ? this.video.volume || 0 : 0; }
1228
+ // Reset changing quality flag
1229
+ this.isChangingQuality = false;
1133
1230
 
1134
- setVolume(volume) {
1135
- if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
1136
- this.updateVolume(volume * 100);
1137
- }
1138
- }
1231
+ // Show controls to allow user interaction
1232
+ this.showControlsNow();
1139
1233
 
1140
- isPaused() { return this.video ? this.video.paused : true; }
1234
+ // Optional: Show poster if available
1235
+ if (this.options.showPosterOnEnd && this.posterOverlay) {
1236
+ this.showPoster();
1237
+ }
1141
1238
 
1142
- isMuted() { return this.video ? this.video.muted : false; }
1239
+ // Trigger 'ended' event to allow proper cleanup
1240
+ // This allows playlist to continue or other error handling
1241
+ this.triggerEvent('ended', {
1242
+ currentTime: this.getCurrentTime(),
1243
+ duration: this.getDuration(),
1244
+ error: true,
1245
+ errorCode: this.video?.error?.code,
1246
+ errorMessage: this.video?.error?.message,
1247
+ playlistInfo: this.getPlaylistInfo()
1248
+ });
1143
1249
 
1144
- setMuted(muted) {
1145
- if (this.video && typeof muted === 'boolean') {
1146
- this.video.muted = muted;
1147
- this.updateMuteButton();
1148
- this.updateVolumeSliderVisual();
1149
- this.initVolumeTooltip();
1150
- }
1250
+ if (this.options.debug) {
1251
+ console.log('Video error handled - triggered ended event');
1151
1252
  }
1253
+ }
1152
1254
 
1153
- getPlaybackRate() { return this.video ? this.video.playbackRate || 1 : 1; }
1154
1255
 
1155
- setPlaybackRate(rate) { if (this.video && typeof rate === 'number' && rate > 0 && !this.isChangingQuality) { this.video.playbackRate = rate; if (this.speedBtn) this.speedBtn.textContent = rate + 'x'; } }
1256
+ getCurrentTime() { return this.video ? this.video.currentTime || 0 : 0; }
1156
1257
 
1157
- isPictureInPictureActive() { return document.pictureInPictureElement === this.video; }
1258
+ setCurrentTime(time) { if (this.video && typeof time === 'number' && time >= 0 && !this.isChangingQuality) { this.video.currentTime = time; } }
1158
1259
 
1159
- getCurrentLanguage() {
1160
- return this.isI18nAvailable() ?
1161
- VideoPlayerTranslations.getCurrentLanguage() : 'en';
1162
- }
1260
+ getDuration() { return this.video && this.video.duration ? this.video.duration : 0; }
1261
+
1262
+ getVolume() { return this.video ? this.video.volume || 0 : 0; }
1163
1263
 
1164
- getSupportedLanguages() {
1165
- return this.isI18nAvailable() ?
1166
- VideoPlayerTranslations.getSupportedLanguages() : ['en'];
1264
+ setVolume(volume) {
1265
+ if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
1266
+ this.updateVolume(volume * 100);
1167
1267
  }
1268
+ }
1168
1269
 
1169
- createBrandLogo() {
1170
- if (!this.options.brandLogoEnabled || !this.options.brandLogoUrl) return;
1270
+ isPaused() { return this.video ? this.video.paused : true; }
1171
1271
 
1172
- const controlsRight = this.controls?.querySelector('.controls-right');
1173
- if (!controlsRight) return;
1272
+ isMuted() { return this.video ? this.video.muted : false; }
1174
1273
 
1175
- // Create brand logo image
1176
- const logo = document.createElement('img');
1177
- logo.className = 'brand-logo';
1178
- logo.src = this.options.brandLogoUrl;
1179
- logo.alt = this.t('brand_logo');
1274
+ setMuted(muted) {
1275
+ if (this.video && typeof muted === 'boolean') {
1276
+ this.video.muted = muted;
1277
+ this.updateMuteButton();
1278
+ this.updateVolumeSliderVisual();
1279
+ this.initVolumeTooltip();
1280
+ }
1281
+ }
1180
1282
 
1181
- // Handle loading error
1182
- logo.onerror = () => {
1183
- if (this.options.debug) console.warn('Brand logo failed to load:', this.options.brandLogoUrl);
1184
- logo.style.display = 'none';
1185
- };
1283
+ getPlaybackRate() { return this.video ? this.video.playbackRate || 1 : 1; }
1186
1284
 
1187
- logo.onload = () => {
1188
- if (this.options.debug) console.log('Brand logo loaded successfully');
1189
- };
1285
+ setPlaybackRate(rate) { if (this.video && typeof rate === 'number' && rate > 0 && !this.isChangingQuality) { this.video.playbackRate = rate; if (this.speedBtn) this.speedBtn.textContent = rate + 'x'; } }
1190
1286
 
1191
- // Add click functionality if link URL is provided
1192
- if (this.options.brandLogoLinkUrl) {
1193
- logo.style.cursor = 'pointer';
1194
- logo.addEventListener('click', (e) => {
1195
- e.stopPropagation(); // Prevent video controls interference
1196
- window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1197
- if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1198
- });
1199
- } else {
1200
- logo.style.cursor = 'default';
1201
- }
1287
+ isPictureInPictureActive() { return document.pictureInPictureElement === this.video; }
1202
1288
 
1203
- // Position the brand logo at the right of the controlbar (at the left of the buttons)
1204
- controlsRight.insertBefore(logo, controlsRight.firstChild);
1289
+ getCurrentLanguage() {
1290
+ return this.isI18nAvailable() ?
1291
+ VideoPlayerTranslations.getCurrentLanguage() : 'en';
1292
+ }
1205
1293
 
1206
- if (this.options.debug) {
1207
- if (this.options.brandLogoLinkUrl) {
1208
- console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
1209
- } else {
1210
- console.log('Brand logo created (no link)');
1211
- }
1212
- }
1294
+ getSupportedLanguages() {
1295
+ return this.isI18nAvailable() ?
1296
+ VideoPlayerTranslations.getSupportedLanguages() : ['en'];
1297
+ }
1298
+
1299
+ createBrandLogo() {
1300
+ if (!this.options.brandLogoEnabled || !this.options.brandLogoUrl) return;
1301
+
1302
+ const controlsRight = this.controls?.querySelector('.controls-right');
1303
+ if (!controlsRight) return;
1304
+
1305
+ // Create brand logo image
1306
+ const logo = document.createElement('img');
1307
+ logo.className = 'brand-logo';
1308
+ logo.src = this.options.brandLogoUrl;
1309
+ logo.alt = this.t('brand_logo');
1310
+
1311
+ // Handle loading error
1312
+ logo.onerror = () => {
1313
+ if (this.options.debug) console.warn('Brand logo failed to load:', this.options.brandLogoUrl);
1314
+ logo.style.display = 'none';
1315
+ };
1316
+
1317
+ logo.onload = () => {
1318
+ if (this.options.debug) console.log('Brand logo loaded successfully');
1319
+ };
1320
+
1321
+ // Add click functionality if link URL is provided
1322
+ if (this.options.brandLogoLinkUrl) {
1323
+ logo.style.cursor = 'pointer';
1324
+ logo.addEventListener('click', (e) => {
1325
+ e.stopPropagation(); // Prevent video controls interference
1326
+ window.open(this.options.brandLogoLinkUrl, '_blank', 'noopener,noreferrer');
1327
+ if (this.options.debug) console.log('Brand logo clicked, opening:', this.options.brandLogoLinkUrl);
1328
+ });
1329
+ } else {
1330
+ logo.style.cursor = 'default';
1213
1331
  }
1214
1332
 
1215
- setBrandLogo(enabled, url = '', linkUrl = '') {
1216
- this.options.brandLogoEnabled = enabled;
1217
- if (url) {
1218
- this.options.brandLogoUrl = url;
1219
- }
1220
- if (linkUrl !== '') {
1221
- this.options.brandLogoLinkUrl = linkUrl;
1222
- }
1333
+ // Position the brand logo at the right of the controlbar (at the left of the buttons)
1334
+ controlsRight.insertBefore(logo, controlsRight.firstChild);
1223
1335
 
1224
- // Remove existing brand logo
1225
- const existingLogo = this.controls?.querySelector('.brand-logo');
1226
- if (existingLogo) {
1227
- existingLogo.remove();
1336
+ if (this.options.debug) {
1337
+ if (this.options.brandLogoLinkUrl) {
1338
+ console.log('Brand logo with click handler created for:', this.options.brandLogoLinkUrl);
1339
+ } else {
1340
+ console.log('Brand logo created (no link)');
1228
1341
  }
1342
+ }
1343
+ }
1229
1344
 
1230
- // Recreate the logo if enabled
1231
- if (enabled && this.options.brandLogoUrl) {
1232
- this.createBrandLogo();
1233
- }
1345
+ setBrandLogo(enabled, url = '', linkUrl = '') {
1346
+ this.options.brandLogoEnabled = enabled;
1347
+ if (url) {
1348
+ this.options.brandLogoUrl = url;
1349
+ }
1350
+ if (linkUrl !== '') {
1351
+ this.options.brandLogoLinkUrl = linkUrl;
1352
+ }
1234
1353
 
1235
- return this;
1354
+ // Remove existing brand logo
1355
+ const existingLogo = this.controls?.querySelector('.brand-logo');
1356
+ if (existingLogo) {
1357
+ existingLogo.remove();
1236
1358
  }
1237
1359
 
1238
- getBrandLogoSettings() {
1239
- return {
1240
- enabled: this.options.brandLogoEnabled,
1241
- url: this.options.brandLogoUrl,
1242
- linkUrl: this.options.brandLogoLinkUrl
1243
- };
1360
+ // Recreate the logo if enabled
1361
+ if (enabled && this.options.brandLogoUrl) {
1362
+ this.createBrandLogo();
1244
1363
  }
1245
1364
 
1246
- switchToVideo(newVideoElement, shouldPlay = false) {
1247
- if (!newVideoElement) {
1248
- if (this.options.debug) console.error('🎵 New video element not found');
1249
- return false;
1250
- }
1365
+ return this;
1366
+ }
1251
1367
 
1252
- // Pause current video
1253
- this.video.pause();
1368
+ getBrandLogoSettings() {
1369
+ return {
1370
+ enabled: this.options.brandLogoEnabled,
1371
+ url: this.options.brandLogoUrl,
1372
+ linkUrl: this.options.brandLogoLinkUrl
1373
+ };
1374
+ }
1254
1375
 
1255
- // Get new video sources and qualities
1256
- const newSources = Array.from(newVideoElement.querySelectorAll('source')).map(source => ({
1257
- src: source.src,
1258
- quality: source.getAttribute('data-quality') || 'auto',
1259
- type: source.type || 'video/mp4'
1260
- }));
1376
+ switchToVideo(newVideoElement, shouldPlay = false) {
1377
+ if (!newVideoElement) {
1378
+ if (this.options.debug) console.error('🎵 New video element not found');
1379
+ return false;
1380
+ }
1261
1381
 
1262
- if (newSources.length === 0) {
1263
- if (this.options.debug) console.error('🎵 New video has no sources');
1264
- return false;
1265
- }
1382
+ // Pause current video
1383
+ this.video.pause();
1384
+
1385
+ // Get new video sources and qualities
1386
+ const newSources = Array.from(newVideoElement.querySelectorAll('source')).map(source => ({
1387
+ src: source.src,
1388
+ quality: source.getAttribute('data-quality') || 'auto',
1389
+ type: source.type || 'video/mp4'
1390
+ }));
1266
1391
 
1267
- // Check if new video is adaptive stream
1268
- if (this.options.adaptiveStreaming && newSources.length > 0) {
1269
- const firstSource = newSources[0];
1270
- if (this.detectStreamType(firstSource.src)) {
1271
- // Initialize adaptive streaming for new video
1272
- this.initializeAdaptiveStreaming(firstSource.src).then((initialized) => {
1273
- if (initialized && shouldPlay) {
1274
- const playPromise = this.video.play();
1275
- if (playPromise) {
1276
- playPromise.catch(error => {
1277
- if (this.options.debug) console.log('Autoplay prevented:', error);
1278
- });
1279
- }
1392
+ if (newSources.length === 0) {
1393
+ if (this.options.debug) console.error('🎵 New video has no sources');
1394
+ return false;
1395
+ }
1396
+
1397
+ // Check if new video is adaptive stream
1398
+ if (this.options.adaptiveStreaming && newSources.length > 0) {
1399
+ const firstSource = newSources[0];
1400
+ if (this.detectStreamType(firstSource.src)) {
1401
+ // Initialize adaptive streaming for new video
1402
+ this.initializeAdaptiveStreaming(firstSource.src).then((initialized) => {
1403
+ if (initialized && shouldPlay) {
1404
+ const playPromise = this.video.play();
1405
+ if (playPromise) {
1406
+ playPromise.catch(error => {
1407
+ if (this.options.debug) console.log('Autoplay prevented:', error);
1408
+ });
1280
1409
  }
1281
- });
1282
- return true;
1283
- }
1410
+ }
1411
+ });
1412
+ return true;
1284
1413
  }
1414
+ }
1285
1415
 
1286
- // Update traditional video sources
1287
- this.video.innerHTML = '';
1288
- newSources.forEach(source => {
1289
- const sourceEl = document.createElement('source');
1290
- sourceEl.src = source.src;
1291
- sourceEl.type = source.type;
1292
- sourceEl.setAttribute('data-quality', source.quality);
1293
- this.video.appendChild(sourceEl);
1294
- });
1416
+ // Update traditional video sources
1417
+ this.video.innerHTML = '';
1418
+ newSources.forEach(source => {
1419
+ const sourceEl = document.createElement('source');
1420
+ sourceEl.src = source.src;
1421
+ sourceEl.type = source.type;
1422
+ sourceEl.setAttribute('data-quality', source.quality);
1423
+ this.video.appendChild(sourceEl);
1424
+ });
1295
1425
 
1296
- // Update subtitles if present
1297
- const newTracks = Array.from(newVideoElement.querySelectorAll('track'));
1298
- newTracks.forEach(track => {
1299
- const trackEl = document.createElement('track');
1300
- trackEl.kind = track.kind;
1301
- trackEl.src = track.src;
1302
- trackEl.srclang = track.srclang;
1303
- trackEl.label = track.label;
1304
- if (track.default) trackEl.default = true;
1305
- this.video.appendChild(trackEl);
1306
- });
1426
+ // Update subtitles if present
1427
+ const newTracks = Array.from(newVideoElement.querySelectorAll('track'));
1428
+ newTracks.forEach(track => {
1429
+ const trackEl = document.createElement('track');
1430
+ trackEl.kind = track.kind;
1431
+ trackEl.src = track.src;
1432
+ trackEl.srclang = track.srclang;
1433
+ trackEl.label = track.label;
1434
+ if (track.default) trackEl.default = true;
1435
+ this.video.appendChild(trackEl);
1436
+ });
1307
1437
 
1308
- // Update video title
1309
- const newTitle = newVideoElement.getAttribute('data-video-title');
1310
- if (newTitle && this.options.showTitleOverlay) {
1311
- this.options.videoTitle = newTitle;
1312
- if (this.titleText) {
1313
- this.titleText.textContent = newTitle;
1314
- }
1438
+ // Update video title
1439
+ const newTitle = newVideoElement.getAttribute('data-video-title');
1440
+ if (newTitle && this.options.showTitleOverlay) {
1441
+ this.options.videoTitle = newTitle;
1442
+ if (this.titleText) {
1443
+ this.titleText.textContent = newTitle;
1315
1444
  }
1445
+ }
1316
1446
 
1317
- // Reload video
1318
- this.video.load();
1447
+ // Reload video
1448
+ this.video.load();
1319
1449
 
1320
- // Update qualities and quality selector
1321
- this.collectVideoQualities();
1322
- this.updateQualityMenu();
1450
+ // Update qualities and quality selector
1451
+ this.collectVideoQualities();
1452
+ this.updateQualityMenu();
1323
1453
 
1324
- // Play if needed
1325
- if (shouldPlay) {
1326
- const playPromise = this.video.play();
1327
- if (playPromise) {
1328
- playPromise.catch(error => {
1329
- if (this.options.debug) console.log('🎵 Autoplay prevented:', error);
1330
- });
1331
- }
1454
+ // Play if needed
1455
+ if (shouldPlay) {
1456
+ const playPromise = this.video.play();
1457
+ if (playPromise) {
1458
+ playPromise.catch(error => {
1459
+ if (this.options.debug) console.log('🎵 Autoplay prevented:', error);
1460
+ });
1332
1461
  }
1333
-
1334
- return true;
1335
1462
  }
1336
1463
 
1464
+ return true;
1465
+ }
1466
+
1337
1467
  /**
1338
1468
  * POSTER IMAGE MANAGEMENT
1339
1469
  * Initialize and manage video poster image
@@ -1550,59 +1680,59 @@ isPosterVisible() {
1550
1680
  }
1551
1681
 
1552
1682
 
1553
- loadScript(src) {
1554
- return new Promise((resolve, reject) => {
1555
- if (document.querySelector(`script[src="${src}"]`)) {
1556
- resolve();
1557
- return;
1558
- }
1683
+ loadScript(src) {
1684
+ return new Promise((resolve, reject) => {
1685
+ if (document.querySelector(`script[src="${src}"]`)) {
1686
+ resolve();
1687
+ return;
1688
+ }
1559
1689
 
1560
- const script = document.createElement('script');
1561
- script.src = src;
1562
- script.onload = resolve;
1563
- script.onerror = reject;
1564
- document.head.appendChild(script);
1565
- });
1566
- }
1690
+ const script = document.createElement('script');
1691
+ script.src = src;
1692
+ script.onload = resolve;
1693
+ script.onerror = reject;
1694
+ document.head.appendChild(script);
1695
+ });
1696
+ }
1567
1697
 
1568
- dispose() {
1569
- if (this.qualityMonitorInterval) {
1570
- clearInterval(this.qualityMonitorInterval);
1571
- this.qualityMonitorInterval = null;
1572
- }
1698
+ dispose() {
1699
+ if (this.qualityMonitorInterval) {
1700
+ clearInterval(this.qualityMonitorInterval);
1701
+ this.qualityMonitorInterval = null;
1702
+ }
1573
1703
 
1574
- if (this.autoHideTimer) {
1575
- clearTimeout(this.autoHideTimer);
1576
- this.autoHideTimer = null;
1577
- }
1704
+ if (this.autoHideTimer) {
1705
+ clearTimeout(this.autoHideTimer);
1706
+ this.autoHideTimer = null;
1707
+ }
1578
1708
 
1579
- this.cleanupQualityChange();
1580
- this.clearControlsTimeout();
1581
- this.clearTitleTimeout();
1709
+ this.cleanupQualityChange();
1710
+ this.clearControlsTimeout();
1711
+ this.clearTitleTimeout();
1582
1712
 
1583
- // Destroy adaptive streaming players
1584
- this.destroyAdaptivePlayer();
1713
+ // Destroy adaptive streaming players
1714
+ this.destroyAdaptivePlayer();
1585
1715
 
1586
- if (this.controls) {
1587
- this.controls.remove();
1588
- }
1589
- if (this.loadingOverlay) {
1590
- this.loadingOverlay.remove();
1591
- }
1592
- if (this.titleOverlay) {
1593
- this.titleOverlay.remove();
1594
- }
1595
- if (this.initialLoading) {
1596
- this.initialLoading.remove();
1597
- }
1716
+ if (this.controls) {
1717
+ this.controls.remove();
1718
+ }
1719
+ if (this.loadingOverlay) {
1720
+ this.loadingOverlay.remove();
1721
+ }
1722
+ if (this.titleOverlay) {
1723
+ this.titleOverlay.remove();
1724
+ }
1725
+ if (this.initialLoading) {
1726
+ this.initialLoading.remove();
1727
+ }
1598
1728
 
1599
- if (this.video) {
1600
- this.video.classList.remove('video-player');
1601
- this.video.controls = true;
1602
- this.video.style.visibility = '';
1603
- this.video.style.opacity = '';
1604
- this.video.style.pointerEvents = '';
1605
- }
1729
+ if (this.video) {
1730
+ this.video.classList.remove('video-player');
1731
+ this.video.controls = true;
1732
+ this.video.style.visibility = '';
1733
+ this.video.style.opacity = '';
1734
+ this.video.style.pointerEvents = '';
1735
+ }
1606
1736
  if (this.chapterMarkersContainer) {
1607
1737
  this.chapterMarkersContainer.remove();
1608
1738
  }
@@ -1614,37 +1744,37 @@ isPosterVisible() {
1614
1744
  }
1615
1745
  this.disposeAllPlugins();
1616
1746
 
1617
- }
1747
+ }
1618
1748
 
1619
- /**
1749
+ /**
1620
1750
 
1621
- * Apply specified resolution mode to video
1751
+ * Apply specified resolution mode to video
1622
1752
 
1623
- * @param {string} resolution - The resolution mode to apply
1753
+ * @param {string} resolution - The resolution mode to apply
1624
1754
 
1625
- */
1755
+ */
1626
1756
 
1627
- /**
1757
+ /**
1628
1758
 
1629
- * Get currently set resolution
1759
+ * Get currently set resolution
1630
1760
 
1631
- * @returns {string} Current resolution
1761
+ * @returns {string} Current resolution
1632
1762
 
1633
- */
1763
+ */
1634
1764
 
1635
- /**
1765
+ /**
1636
1766
 
1637
- * Initialize resolution from options value
1767
+ * Initialize resolution from options value
1638
1768
 
1639
- */
1769
+ */
1640
1770
 
1641
- /**
1771
+ /**
1642
1772
 
1643
- * Restore resolution after quality change - internal method
1773
+ * Restore resolution after quality change - internal method
1644
1774
 
1645
- * @private
1775
+ * @private
1646
1776
 
1647
- */
1777
+ */
1648
1778
 
1649
1779
  // Core methods for main class
1650
- // All original functionality preserved exactly
1780
+ // All original functionality preserved exactly