myetv-player 1.2.0 → 1.3.0

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