myetv-player 1.0.0 → 1.0.8

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