myetv-player 1.0.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 (50) hide show
  1. package/.github/workflows/npm-publish.yml +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +866 -0
  4. package/build.js +189 -0
  5. package/css/README.md +1 -0
  6. package/css/myetv-player.css +13702 -0
  7. package/css/myetv-player.min.css +1 -0
  8. package/dist/README.md +1 -0
  9. package/dist/myetv-player.js +6408 -0
  10. package/dist/myetv-player.min.js +6183 -0
  11. package/package.json +27 -0
  12. package/plugins/README.md +1 -0
  13. package/plugins/google-analytics/README.md +1 -0
  14. package/plugins/google-analytics/myetv-player-g-analytics-plugin.js +548 -0
  15. package/plugins/youtube/README.md +1 -0
  16. package/plugins/youtube/myetv-player-youtube-plugin.js +418 -0
  17. package/scss/README.md +1 -0
  18. package/scss/_audio-player.scss +21 -0
  19. package/scss/_base.scss +131 -0
  20. package/scss/_controls.scss +30 -0
  21. package/scss/_loading.scss +111 -0
  22. package/scss/_menus.scss +4070 -0
  23. package/scss/_mixins.scss +112 -0
  24. package/scss/_poster.scss +8 -0
  25. package/scss/_progress-bar.scss +2203 -0
  26. package/scss/_resolution.scss +68 -0
  27. package/scss/_responsive.scss +1532 -0
  28. package/scss/_themes.scss +30 -0
  29. package/scss/_title-overlay.scss +2262 -0
  30. package/scss/_tooltips.scss +7 -0
  31. package/scss/_variables.scss +49 -0
  32. package/scss/_video.scss +2401 -0
  33. package/scss/_volume.scss +1981 -0
  34. package/scss/_watermark.scss +8 -0
  35. package/scss/myetv-player.scss +51 -0
  36. package/scss/package.json +16 -0
  37. package/src/README.md +1 -0
  38. package/src/chapters.js +521 -0
  39. package/src/controls.js +1005 -0
  40. package/src/core.js +1650 -0
  41. package/src/events.js +330 -0
  42. package/src/fullscreen.js +82 -0
  43. package/src/i18n.js +348 -0
  44. package/src/playlist.js +177 -0
  45. package/src/plugins.js +384 -0
  46. package/src/quality.js +921 -0
  47. package/src/streaming.js +346 -0
  48. package/src/subtitles.js +426 -0
  49. package/src/utils.js +51 -0
  50. package/src/watermark.js +195 -0
package/src/core.js ADDED
@@ -0,0 +1,1650 @@
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,
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
+ };
62
+
63
+ this.isUserSeeking = false;
64
+ this.controlsTimeout = null;
65
+ this.titleTimeout = null;
66
+ this.currentQualityIndex = 0;
67
+ this.qualities = [];
68
+ this.originalSources = [];
69
+ this.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;
89
+
90
+ // Quality change management
91
+ this.qualityChangeTimeout = null;
92
+ this.isChangingQuality = false;
93
+
94
+ // Quality debug
95
+ this.debugQuality = false;
96
+
97
+ // Auto-hide system
98
+ this.autoHideTimer = null;
99
+ this.mouseOverControls = false;
100
+ this.autoHideDebug = false;
101
+ this.autoHideInitialized = false;
102
+
103
+ // Poster management
104
+ this.posterOverlay = null;
105
+
106
+ // Watermark overlay
107
+ this.watermarkElement = null;
108
+
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': []
123
+ };
124
+
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
+ };
141
+
142
+ this.lastTimeUpdate = 0; // For throttling timeupdate events
143
+
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;
150
+ }
151
+
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
+ };
225
+
226
+ draw();
227
+ };
228
+ this.adaptToAudioFile();
229
+ this.bindEvents();
230
+
231
+ if (this.options.keyboardControls) {
232
+ this.setupKeyboardControls();
233
+ }
234
+
235
+ this.updateVolumeSliderVisual();
236
+ this.initVolumeTooltip();
237
+ this.updateTooltips();
238
+ this.markPlayerReady();
239
+ this.initializePluginSystem();
240
+ this.restoreSourcesAsync();
241
+
242
+ this.initializeSubtitles();
243
+ this.initializeQualityMonitoring();
244
+
245
+ this.initializeResolution();
246
+ this.initializeChapters();
247
+ this.initializePoster();
248
+ this.initializeWatermark();
249
+
250
+ } catch (error) {
251
+ if (this.options.debug) console.error('Video player initialization error:', error);
252
+ }
253
+ }
254
+
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
+ }
279
+
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
+ }
288
+
289
+ const fallback = {
290
+ 'play_pause': 'Play/Pause (Space)',
291
+ 'mute_unmute': 'Mute/Unmute (M)',
292
+ 'volume': 'Volume',
293
+ 'playback_speed': 'Playback speed',
294
+ 'video_quality': 'Video quality',
295
+ 'picture_in_picture': 'Picture-in-Picture (P)',
296
+ 'fullscreen': 'Fullscreen (F)',
297
+ 'subtitles': 'Subtitles (S)',
298
+ 'subtitles_enable': 'Enable subtitles',
299
+ 'subtitles_disable': 'Disable subtitles',
300
+ 'subtitles_off': 'Off',
301
+ 'auto': 'Auto',
302
+ 'brand_logo': 'Brand logo',
303
+ 'next_video': 'Next video (N)',
304
+ 'prev_video': 'Previous video (P)',
305
+ 'playlist_next': 'Next',
306
+ 'playlist_prev': 'Previous'
307
+ };
308
+
309
+ return fallback[key] || key;
310
+ }
311
+
312
+ interceptAutoLoading() {
313
+ this.saveOriginalSources();
314
+ this.disableSources();
315
+
316
+ this.video.preload = 'none';
317
+ this.video.controls = false;
318
+ this.video.autoplay = false;
319
+
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 = '';
324
+ }
325
+
326
+ this.hideNativePlayer();
327
+
328
+ if (this.options.debug) console.log('📁 Sources temporarily disabled to prevent blocking');
329
+ }
330
+
331
+ saveOriginalSources() {
332
+ const sources = this.video.querySelectorAll('source');
333
+ this.originalSources = [];
334
+
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
+ });
344
+ }
345
+ });
346
+
347
+ if (this.options.debug) console.log(`📁 Saved ${this.originalSources.length} sources originali:`, this.originalSources);
348
+ }
349
+
350
+ disableSources() {
351
+ const sources = this.video.querySelectorAll('source');
352
+ sources.forEach(source => {
353
+ if (source.src) {
354
+ source.removeAttribute('src');
355
+ }
356
+ });
357
+ }
358
+
359
+ restoreSourcesAsync() {
360
+ setTimeout(() => {
361
+ this.restoreSources();
362
+ }, 200);
363
+ }
364
+
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
+ }
379
+
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
+ }
387
+ }
388
+
389
+ // Fallback to traditional sources
390
+ if (this.originalSrc) {
391
+ this.video.src = this.originalSrc;
392
+ }
393
+
394
+ this.originalSources.forEach(sourceData => {
395
+ if (sourceData.element && sourceData.src) {
396
+ sourceData.element.src = sourceData.src;
397
+ }
398
+ });
399
+
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();
416
+
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
+ }
425
+
426
+ if (this.options.debug) console.log('✅ Sources ripristinate:', this.qualities);
427
+
428
+ } catch (error) {
429
+ if (this.options.debug) console.error('❌ Errore ripristino sources:', error);
430
+ }
431
+ }
432
+
433
+ reinitializeSubtitles() {
434
+ if (this.options.debug) console.log('🔄 Re-initializing subtitles...');
435
+
436
+ // Clear existing subtitle data
437
+ this.textTracks = [];
438
+ this.currentSubtitleTrack = null;
439
+ this.subtitlesEnabled = false;
440
+
441
+ // Re-detect and initialize subtitles
442
+ this.detectTextTracks();
443
+ this.updateSubtitlesUI();
444
+ this.bindSubtitleEvents();
445
+
446
+ if (this.options.debug) console.log(`📝 Re-detected ${this.textTracks.length} subtitle tracks`);
447
+
448
+ // Auto-enable first subtitle track if available and default is set
449
+ const defaultTrack = this.getDefaultSubtitleTrack();
450
+ if (defaultTrack !== -1 && this.options.subtitlesEnabled === true) { // <-- AGGIUNTO!
451
+ setTimeout(() => {
452
+ this.enableSubtitleTrack(defaultTrack);
453
+ if (this.options.debug) console.log(`🎯 Auto-enabled default subtitle track: ${defaultTrack}`);
454
+ }, 100);
455
+ } else {
456
+ if (this.options.debug) {
457
+ console.log(`📝 Default subtitle track NOT auto-enabled:`, {
458
+ defaultTrack: defaultTrack,
459
+ subtitlesEnabled: this.options.subtitlesEnabled
460
+ });
461
+ }
462
+ }
463
+ }
464
+
465
+ getDefaultSubtitleTrack() {
466
+ if (!this.video.textTracks) return -1;
467
+
468
+ for (let i = 0; i < this.video.textTracks.length; i++) {
469
+ const track = this.video.textTracks[i];
470
+ if (track.mode === 'showing' || track.default) {
471
+ return i;
472
+ }
473
+ }
474
+ return -1;
475
+ }
476
+
477
+ markPlayerReady() {
478
+ setTimeout(() => {
479
+ this.isPlayerReady = true;
480
+ if (this.container) {
481
+ this.container.classList.add('player-initialized');
482
+ }
483
+
484
+ if (this.video) {
485
+ this.video.style.visibility = '';
486
+ this.video.style.opacity = '';
487
+ this.video.style.pointerEvents = '';
488
+ }
489
+
490
+ // INITIALIZE AUTO-HIDE AFTER EVERYTHING IS READY
491
+ setTimeout(() => {
492
+ if (this.options.autoHide && !this.autoHideInitialized) {
493
+ this.initAutoHide();
494
+ }
495
+
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}"`);
499
+
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
+ }
515
+
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);
526
+
527
+ }, 100);
528
+ }
529
+
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
+ }
538
+
539
+ this.container = wrapper;
540
+
541
+ this.createInitialLoading();
542
+ this.createLoadingOverlay();
543
+ this.collectVideoQualities();
544
+ this.createControls();
545
+ this.createBrandLogo();
546
+ this.detectPlaylist();
547
+
548
+ if (this.options.showTitleOverlay) {
549
+ this.createTitleOverlay();
550
+ }
551
+ }
552
+
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
+ }
560
+
561
+ collectVideoQualities() {
562
+ if (this.options.debug) console.log('📁 Video qualities will be loaded with restored sources');
563
+ }
564
+
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>';
570
+ this.container.appendChild(overlay);
571
+ this.loadingOverlay = overlay;
572
+ }
573
+
574
+ createTitleOverlay() {
575
+ const overlay = document.createElement('div');
576
+ overlay.className = 'title-overlay';
577
+ overlay.id = 'titleOverlay-' + this.getUniqueId();
578
+
579
+ const titleText = document.createElement('h2');
580
+ titleText.className = 'title-text';
581
+ titleText.textContent = this.options.videoTitle || '';
582
+
583
+ overlay.appendChild(titleText);
584
+
585
+ if (this.controls) {
586
+ this.container.insertBefore(overlay, this.controls);
587
+ } else {
588
+ this.container.appendChild(overlay);
589
+ }
590
+
591
+ this.titleOverlay = overlay;
592
+
593
+ if (this.options.persistentTitle && this.options.videoTitle) {
594
+ this.showTitleOverlay();
595
+ }
596
+ }
597
+
598
+ updateTooltips() {
599
+ if (!this.controls) return;
600
+
601
+ 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');
610
+ }
611
+ } catch (error) {
612
+ if (this.options.debug) console.warn('Errore aggiornamento tooltip:', error);
613
+ }
614
+ }
615
+
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);
625
+ }
626
+ }
627
+ return false;
628
+ }
629
+
630
+ setVideoTitle(title) {
631
+ this.options.videoTitle = title || '';
632
+
633
+ if (this.titleOverlay) {
634
+ const titleElement = this.titleOverlay.querySelector('.title-text');
635
+ if (titleElement) {
636
+ titleElement.textContent = this.options.videoTitle;
637
+ }
638
+
639
+ if (title) {
640
+ this.showTitleOverlay();
641
+
642
+ if (!this.options.persistentTitle) {
643
+ this.clearTitleTimeout();
644
+ this.titleTimeout = setTimeout(() => {
645
+ this.hideTitleOverlay();
646
+ }, 3000);
647
+ }
648
+ }
649
+ }
650
+
651
+ return this;
652
+ }
653
+
654
+ getVideoTitle() {
655
+ return this.options.videoTitle;
656
+ }
657
+
658
+ setPersistentTitle(persistent) {
659
+ this.options.persistentTitle = persistent;
660
+
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
+ }
675
+
676
+ return this;
677
+ }
678
+
679
+ enableTitleOverlay() {
680
+ if (!this.titleOverlay && !this.options.showTitleOverlay) {
681
+ this.options.showTitleOverlay = true;
682
+ this.createTitleOverlay();
683
+ }
684
+ return this;
685
+ }
686
+
687
+ disableTitleOverlay() {
688
+ if (this.titleOverlay) {
689
+ this.titleOverlay.remove();
690
+ this.titleOverlay = null;
691
+ }
692
+ this.options.showTitleOverlay = false;
693
+ return this;
694
+ }
695
+
696
+ getUniqueId() {
697
+ return Math.random().toString(36).substr(2, 9);
698
+ }
699
+
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');
706
+
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');
716
+
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');
725
+
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');
732
+ }
733
+
734
+ updateVolumeSliderVisual() {
735
+ if (!this.video || !this.container) return;
736
+
737
+ const volume = this.video.muted ? 0 : this.video.volume;
738
+ const percentage = Math.round(volume * 100);
739
+
740
+ this.container.style.setProperty('--player-volume-fill', percentage + '%');
741
+
742
+ if (this.volumeSlider) {
743
+ this.volumeSlider.value = percentage;
744
+ }
745
+ }
746
+
747
+ createVolumeTooltip() {
748
+ const volumeContainer = this.controls?.querySelector('.volume-container');
749
+ if (!volumeContainer || volumeContainer.querySelector('.volume-tooltip')) {
750
+ return; // Tooltip already present
751
+ }
752
+
753
+ const tooltip = document.createElement('div');
754
+ tooltip.className = 'volume-tooltip';
755
+ tooltip.textContent = '50%';
756
+ volumeContainer.appendChild(tooltip);
757
+
758
+ this.volumeTooltip = tooltip;
759
+
760
+ if (this.options.debug) {
761
+ console.log('Dynamic volume tooltip created');
762
+ }
763
+ }
764
+
765
+ updateVolumeTooltip() {
766
+ if (!this.volumeTooltip || !this.video) return;
767
+
768
+ const volume = Math.round(this.video.volume * 100);
769
+ this.volumeTooltip.textContent = volume + '%';
770
+
771
+ // Aggiorna la posizione del tooltip
772
+ this.updateVolumeTooltipPosition(this.video.volume);
773
+
774
+ if (this.options.debug) {
775
+ console.log('Volume tooltip updated:', volume + '%');
776
+ }
777
+ }
778
+
779
+ updateVolumeTooltipPosition(volumeValue = null) {
780
+ if (!this.volumeTooltip || !this.video) return;
781
+
782
+ const volumeSlider = this.controls?.querySelector('.volume-slider');
783
+ if (!volumeSlider) return;
784
+
785
+ // If no volume provided, use current volume
786
+ if (volumeValue === null) {
787
+ volumeValue = this.video.volume;
788
+ }
789
+
790
+ // Calcola la posizione esatta del thumb
791
+ const sliderRect = volumeSlider.getBoundingClientRect();
792
+ const sliderWidth = sliderRect.width;
793
+
794
+ // Thumb size is typically 14px (as defined in CSS)
795
+ const thumbSize = 14; // var(--player-volume-handle-size)
796
+
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);
801
+
802
+ // Converti in percentuale relativa al container dello slider
803
+ const percentage = (thumbCenterPosition / sliderWidth) * 100;
804
+
805
+ // Posiziona il tooltip
806
+ this.volumeTooltip.style.left = percentage + '%';
807
+
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
+ }
816
+ }
817
+
818
+ initVolumeTooltip() {
819
+ this.createVolumeTooltip();
820
+ this.setupVolumeTooltipEvents();
821
+
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
+ }
830
+
831
+ updateVolumeSliderVisualWithTooltip() {
832
+ const volumeSlider = this.controls?.querySelector('.volume-slider');
833
+ if (!volumeSlider || !this.video) return;
834
+
835
+ const volume = this.video.volume || 0;
836
+ const percentage = Math.round(volume * 100);
837
+
838
+ volumeSlider.value = volume;
839
+
840
+ // Update CSS custom property per il riempimento visuale
841
+ const volumeFillPercentage = percentage + '%';
842
+ volumeSlider.style.setProperty('--player-volume-fill', volumeFillPercentage);
843
+
844
+ // Aggiorna anche il tooltip se presente (testo e posizione)
845
+ this.updateVolumeTooltip();
846
+
847
+ if (this.options.debug) {
848
+ console.log('Volume slider aggiornato:', {
849
+ volume: volume,
850
+ percentage: percentage,
851
+ fillPercentage: volumeFillPercentage
852
+ });
853
+ }
854
+ }
855
+
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);
860
+ return this;
861
+ }
862
+
863
+ this.options.volumeSlider = orientation;
864
+ const volumeContainer = this.controls?.querySelector('.volume-container');
865
+ if (volumeContainer) {
866
+ volumeContainer.setAttribute('data-orientation', orientation);
867
+ }
868
+
869
+ if (this.options.debug) console.log('Volume slider orientation set to:', orientation);
870
+ return this;
871
+ }
872
+
873
+ getVolumeSliderOrientation() {
874
+ return this.options.volumeSlider;
875
+ }
876
+
877
+
878
+ initVolumeTooltip() {
879
+
880
+ this.createVolumeTooltip();
881
+
882
+ // Setup events
883
+ this.setupVolumeTooltipEvents();
884
+
885
+ setTimeout(() => {
886
+ this.updateVolumeTooltip();
887
+ }, 200);
888
+
889
+ if (this.options.debug) {
890
+ console.log('Dynamic volume tooltip inizializzato');
891
+ }
892
+ }
893
+
894
+ setupSeekTooltip() {
895
+ if (!this.options.showSeekTooltip || !this.progressContainer || !this.seekTooltip) return;
896
+
897
+ this.progressContainer.addEventListener('mouseenter', () => {
898
+ if (this.seekTooltip) {
899
+ this.seekTooltip.classList.add('visible');
900
+ }
901
+ });
902
+
903
+ this.progressContainer.addEventListener('mouseleave', () => {
904
+ if (this.seekTooltip) {
905
+ this.seekTooltip.classList.remove('visible');
906
+ }
907
+ });
908
+
909
+ this.progressContainer.addEventListener('mousemove', (e) => {
910
+ this.updateSeekTooltip(e);
911
+ });
912
+ }
913
+
914
+ updateSeekTooltip(e) {
915
+ if (!this.seekTooltip || !this.progressContainer || !this.video || !this.video.duration) return;
916
+
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;
921
+
922
+ this.seekTooltip.textContent = this.formatTime(targetTime);
923
+
924
+ const tooltipRect = this.seekTooltip.getBoundingClientRect();
925
+ let leftPosition = clickX;
926
+
927
+ const tooltipWidth = tooltipRect.width || 50;
928
+ const containerWidth = rect.width;
929
+
930
+ leftPosition = Math.max(tooltipWidth / 2, Math.min(containerWidth - tooltipWidth / 2, clickX));
931
+
932
+ this.seekTooltip.style.left = leftPosition + 'px';
933
+ }
934
+
935
+ play() {
936
+ if (!this.video || this.isChangingQuality) return;
937
+
938
+ this.video.play().catch(err => {
939
+ if (this.options.debug) console.log('Play failed:', err);
940
+ });
941
+
942
+ if (this.playIcon) this.playIcon.classList.add('hidden');
943
+ if (this.pauseIcon) this.pauseIcon.classList.remove('hidden');
944
+
945
+ // Trigger event played
946
+ this.triggerEvent('played', {
947
+ currentTime: this.getCurrentTime(),
948
+ duration: this.getDuration()
949
+ });
950
+ }
951
+
952
+ pause() {
953
+ if (!this.video) return;
954
+
955
+ this.video.pause();
956
+ if (this.playIcon) this.playIcon.classList.remove('hidden');
957
+ if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
958
+
959
+ // Trigger paused event
960
+ this.triggerEvent('paused', {
961
+ currentTime: this.getCurrentTime(),
962
+ duration: this.getDuration()
963
+ });
964
+ }
965
+
966
+ updateVolume(value) {
967
+ if (!this.video) return;
968
+
969
+ const previousVolume = this.video.volume;
970
+ const previousMuted = this.video.muted;
971
+
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();
977
+
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
+ }
987
+ }
988
+
989
+ changeVolume(delta) {
990
+ if (!this.video) return;
991
+
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
+ }
997
+
998
+ updateProgress() {
999
+ if (!this.video || !this.progressFilled || !this.progressHandle || this.isUserSeeking) return;
1000
+
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
+ }
1006
+
1007
+ this.updateTimeDisplay();
1008
+
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
+ }
1018
+ }
1019
+
1020
+ updateBuffer() {
1021
+ if (!this.video || !this.progressBuffer) return;
1022
+
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);
1030
+ }
1031
+ }
1032
+
1033
+ startSeeking(e) {
1034
+ if (this.isChangingQuality) return;
1035
+
1036
+ this.isUserSeeking = true;
1037
+ this.seek(e);
1038
+ e.preventDefault();
1039
+
1040
+ // Show controls during seeking
1041
+ if (this.options.autoHide && this.autoHideInitialized) {
1042
+ this.showControlsNow();
1043
+ this.resetAutoHideTimer();
1044
+ }
1045
+ }
1046
+
1047
+ continueSeeking(e) {
1048
+ if (this.isUserSeeking && !this.isChangingQuality) {
1049
+ this.seek(e);
1050
+ }
1051
+ }
1052
+
1053
+ endSeeking() {
1054
+ this.isUserSeeking = false;
1055
+ }
1056
+
1057
+ seek(e) {
1058
+ if (!this.video || !this.progressContainer || !this.progressFilled || !this.progressHandle || this.isChangingQuality) return;
1059
+
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));
1063
+
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
+ }
1070
+ }
1071
+
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
+ }
1076
+ }
1077
+
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';
1085
+
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
+ }
1092
+
1093
+ // Trigger speedchange event
1094
+ const previousSpeed = this.video.playbackRate;
1095
+ this.triggerEvent('speedchange', {
1096
+ speed: speed,
1097
+ previousSpeed: previousSpeed
1098
+ });
1099
+ }
1100
+ }
1101
+
1102
+ onVideoEnded() {
1103
+ if (this.playIcon) this.playIcon.classList.remove('hidden');
1104
+ if (this.pauseIcon) this.pauseIcon.classList.add('hidden');
1105
+
1106
+ // Handle loop option
1107
+ if (this.options.loop) {
1108
+ if (this.options.debug) console.log('🔄 Video loop enabled - restarting from beginning');
1109
+ this.video.currentTime = 0;
1110
+ this.video.play().catch(error => {
1111
+ if (this.options.debug) console.warn('Loop play failed:', error);
1112
+ });
1113
+ return; // Don't show controls or trigger ended event when looping
1114
+ }
1115
+
1116
+ this.showControlsNow();
1117
+
1118
+ // Trigger ended event
1119
+ this.triggerEvent('ended', {
1120
+ currentTime: this.getCurrentTime(),
1121
+ duration: this.getDuration(),
1122
+ playlistInfo: this.getPlaylistInfo()
1123
+ });
1124
+ }
1125
+
1126
+ getCurrentTime() { return this.video ? this.video.currentTime || 0 : 0; }
1127
+
1128
+ setCurrentTime(time) { if (this.video && typeof time === 'number' && time >= 0 && !this.isChangingQuality) { this.video.currentTime = time; } }
1129
+
1130
+ getDuration() { return this.video && this.video.duration ? this.video.duration : 0; }
1131
+
1132
+ getVolume() { return this.video ? this.video.volume || 0 : 0; }
1133
+
1134
+ setVolume(volume) {
1135
+ if (typeof volume === 'number' && volume >= 0 && volume <= 1) {
1136
+ this.updateVolume(volume * 100);
1137
+ }
1138
+ }
1139
+
1140
+ isPaused() { return this.video ? this.video.paused : true; }
1141
+
1142
+ isMuted() { return this.video ? this.video.muted : false; }
1143
+
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
+ }
1151
+ }
1152
+
1153
+ getPlaybackRate() { return this.video ? this.video.playbackRate || 1 : 1; }
1154
+
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'; } }
1156
+
1157
+ isPictureInPictureActive() { return document.pictureInPictureElement === this.video; }
1158
+
1159
+ getCurrentLanguage() {
1160
+ return this.isI18nAvailable() ?
1161
+ VideoPlayerTranslations.getCurrentLanguage() : 'en';
1162
+ }
1163
+
1164
+ getSupportedLanguages() {
1165
+ return this.isI18nAvailable() ?
1166
+ VideoPlayerTranslations.getSupportedLanguages() : ['en'];
1167
+ }
1168
+
1169
+ createBrandLogo() {
1170
+ if (!this.options.brandLogoEnabled || !this.options.brandLogoUrl) return;
1171
+
1172
+ const controlsRight = this.controls?.querySelector('.controls-right');
1173
+ if (!controlsRight) return;
1174
+
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');
1180
+
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
+ };
1186
+
1187
+ logo.onload = () => {
1188
+ if (this.options.debug) console.log('Brand logo loaded successfully');
1189
+ };
1190
+
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
+ }
1202
+
1203
+ // Position the brand logo at the right of the controlbar (at the left of the buttons)
1204
+ controlsRight.insertBefore(logo, controlsRight.firstChild);
1205
+
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
+ }
1213
+ }
1214
+
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
+ }
1223
+
1224
+ // Remove existing brand logo
1225
+ const existingLogo = this.controls?.querySelector('.brand-logo');
1226
+ if (existingLogo) {
1227
+ existingLogo.remove();
1228
+ }
1229
+
1230
+ // Recreate the logo if enabled
1231
+ if (enabled && this.options.brandLogoUrl) {
1232
+ this.createBrandLogo();
1233
+ }
1234
+
1235
+ return this;
1236
+ }
1237
+
1238
+ getBrandLogoSettings() {
1239
+ return {
1240
+ enabled: this.options.brandLogoEnabled,
1241
+ url: this.options.brandLogoUrl,
1242
+ linkUrl: this.options.brandLogoLinkUrl
1243
+ };
1244
+ }
1245
+
1246
+ switchToVideo(newVideoElement, shouldPlay = false) {
1247
+ if (!newVideoElement) {
1248
+ if (this.options.debug) console.error('🎵 New video element not found');
1249
+ return false;
1250
+ }
1251
+
1252
+ // Pause current video
1253
+ this.video.pause();
1254
+
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
+ }));
1261
+
1262
+ if (newSources.length === 0) {
1263
+ if (this.options.debug) console.error('🎵 New video has no sources');
1264
+ return false;
1265
+ }
1266
+
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
+ }
1280
+ }
1281
+ });
1282
+ return true;
1283
+ }
1284
+ }
1285
+
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
+ });
1295
+
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
+ });
1307
+
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
+ }
1315
+ }
1316
+
1317
+ // Reload video
1318
+ this.video.load();
1319
+
1320
+ // Update qualities and quality selector
1321
+ this.collectVideoQualities();
1322
+ this.updateQualityMenu();
1323
+
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
+ }
1332
+ }
1333
+
1334
+ return true;
1335
+ }
1336
+
1337
+ /**
1338
+ * POSTER IMAGE MANAGEMENT
1339
+ * Initialize and manage video poster image
1340
+ */
1341
+ initializePoster() {
1342
+ if (!this.video) {
1343
+ return;
1344
+ }
1345
+
1346
+ // Set poster from options if provided
1347
+ if (this.options.poster) {
1348
+ this.video.setAttribute('poster', this.options.poster);
1349
+ if (this.options.debug) console.log('🖼️ Poster set from options:', this.options.poster);
1350
+ }
1351
+
1352
+ // Create custom poster overlay to prevent disappearing
1353
+ this.createPosterOverlay();
1354
+
1355
+ // Bind poster events
1356
+ this.bindPosterEvents();
1357
+
1358
+ if (this.options.debug) console.log('🖼️ Poster management initialized');
1359
+ }
1360
+
1361
+ /**
1362
+ * Create custom poster overlay element
1363
+ * This prevents the poster from disappearing after video loads
1364
+ */
1365
+ createPosterOverlay() {
1366
+ if (!this.container || !this.video) {
1367
+ return;
1368
+ }
1369
+
1370
+ // Check if poster exists (either from attribute or options)
1371
+ const posterUrl = this.video.getAttribute('poster') || this.options.poster;
1372
+
1373
+ if (!posterUrl) {
1374
+ if (this.options.debug) console.log('🖼️ No poster URL found');
1375
+ return;
1376
+ }
1377
+
1378
+ // Create poster overlay element
1379
+ const posterOverlay = document.createElement('div');
1380
+ posterOverlay.className = 'video-poster-overlay';
1381
+ posterOverlay.style.backgroundImage = `url(${posterUrl})`;
1382
+
1383
+ // Insert poster overlay before controls
1384
+ if (this.controls) {
1385
+ this.container.insertBefore(posterOverlay, this.controls);
1386
+ } else {
1387
+ this.container.appendChild(posterOverlay);
1388
+ }
1389
+
1390
+ this.posterOverlay = posterOverlay;
1391
+
1392
+ if (this.options.debug) console.log('🖼️ Custom poster overlay created');
1393
+ }
1394
+
1395
+ /**
1396
+ * Bind poster-related events
1397
+ */
1398
+ bindPosterEvents() {
1399
+ if (!this.video || !this.posterOverlay) {
1400
+ return;
1401
+ }
1402
+
1403
+ // Hide poster when video starts playing
1404
+ this.video.addEventListener('play', () => {
1405
+ this.hidePoster();
1406
+ });
1407
+
1408
+ // Show poster when video ends (optional)
1409
+ this.video.addEventListener('ended', () => {
1410
+ if (this.options.showPosterOnEnd) {
1411
+ this.showPoster();
1412
+ }
1413
+ });
1414
+
1415
+ // Hide poster when video is loading/playing
1416
+ this.video.addEventListener('playing', () => {
1417
+ this.hidePoster();
1418
+ });
1419
+
1420
+ // Show poster on load if not autoplay
1421
+ if (!this.options.autoplay) {
1422
+ this.showPoster();
1423
+ }
1424
+
1425
+ // Click on poster to play video
1426
+ if (this.posterOverlay) {
1427
+ this.posterOverlay.addEventListener('click', (e) => {
1428
+ e.stopPropagation();
1429
+ if (this.video.paused) {
1430
+ this.play();
1431
+ }
1432
+ });
1433
+ }
1434
+
1435
+ if (this.options.debug) console.log('🖼️ Poster events bound');
1436
+ }
1437
+
1438
+ /**
1439
+ * Show poster overlay
1440
+ */
1441
+ showPoster() {
1442
+ if (this.posterOverlay) {
1443
+ this.posterOverlay.classList.add('visible');
1444
+ this.posterOverlay.classList.remove('hidden');
1445
+ if (this.options.debug) console.log('🖼️ Poster shown');
1446
+ }
1447
+ }
1448
+
1449
+ /**
1450
+ * Hide poster overlay
1451
+ */
1452
+ hidePoster() {
1453
+ if (this.posterOverlay) {
1454
+ this.posterOverlay.classList.remove('visible');
1455
+ this.posterOverlay.classList.add('hidden');
1456
+ if (this.options.debug) console.log('🖼️ Poster hidden');
1457
+ }
1458
+ }
1459
+
1460
+ /**
1461
+ * Set poster image dynamically
1462
+ * @param {String} posterUrl - URL of the poster image
1463
+ */
1464
+ setPoster(posterUrl) {
1465
+ if (!posterUrl) {
1466
+ if (this.options.debug) console.warn('🖼️ Invalid poster URL');
1467
+ return this;
1468
+ }
1469
+
1470
+ this.options.poster = posterUrl;
1471
+
1472
+ // Update video poster attribute
1473
+ if (this.video) {
1474
+ this.video.setAttribute('poster', posterUrl);
1475
+ }
1476
+
1477
+ // Update or create poster overlay
1478
+ if (this.posterOverlay) {
1479
+ this.posterOverlay.style.backgroundImage = `url(${posterUrl})`;
1480
+ } else {
1481
+ this.createPosterOverlay();
1482
+ this.bindPosterEvents();
1483
+ }
1484
+
1485
+ if (this.options.debug) console.log('🖼️ Poster updated:', posterUrl);
1486
+
1487
+ return this;
1488
+ }
1489
+
1490
+ /**
1491
+ * Get current poster URL
1492
+ * @returns {String|null} Poster URL or null
1493
+ */
1494
+ getPoster() {
1495
+ return this.options.poster || this.video?.getAttribute('poster') || null;
1496
+ }
1497
+
1498
+ /**
1499
+ * Remove poster
1500
+ */
1501
+ removePoster() {
1502
+ if (this.posterOverlay) {
1503
+ this.posterOverlay.remove();
1504
+ this.posterOverlay = null;
1505
+ }
1506
+
1507
+ if (this.video) {
1508
+ this.video.removeAttribute('poster');
1509
+ }
1510
+
1511
+ this.options.poster = null;
1512
+
1513
+ if (this.options.debug) console.log('🖼️ Poster removed');
1514
+
1515
+ return this;
1516
+ }
1517
+
1518
+ /**
1519
+ * Toggle poster visibility
1520
+ * @param {Boolean|null} show - True to show, false to hide, null to toggle
1521
+ * @returns {Object} this
1522
+ */
1523
+ togglePoster(show = null) {
1524
+ if (!this.posterOverlay) {
1525
+ return this;
1526
+ }
1527
+
1528
+ if (show === null) {
1529
+ // Toggle
1530
+ if (this.posterOverlay.classList.contains('visible')) {
1531
+ this.hidePoster();
1532
+ } else {
1533
+ this.showPoster();
1534
+ }
1535
+ } else if (show) {
1536
+ this.showPoster();
1537
+ } else {
1538
+ this.hidePoster();
1539
+ }
1540
+
1541
+ return this;
1542
+ }
1543
+
1544
+ /**
1545
+ * Check if poster is visible
1546
+ * @returns {Boolean} True if poster is visible
1547
+ */
1548
+ isPosterVisible() {
1549
+ return this.posterOverlay ? this.posterOverlay.classList.contains('visible') : false;
1550
+ }
1551
+
1552
+
1553
+ loadScript(src) {
1554
+ return new Promise((resolve, reject) => {
1555
+ if (document.querySelector(`script[src="${src}"]`)) {
1556
+ resolve();
1557
+ return;
1558
+ }
1559
+
1560
+ const script = document.createElement('script');
1561
+ script.src = src;
1562
+ script.onload = resolve;
1563
+ script.onerror = reject;
1564
+ document.head.appendChild(script);
1565
+ });
1566
+ }
1567
+
1568
+ dispose() {
1569
+ if (this.qualityMonitorInterval) {
1570
+ clearInterval(this.qualityMonitorInterval);
1571
+ this.qualityMonitorInterval = null;
1572
+ }
1573
+
1574
+ if (this.autoHideTimer) {
1575
+ clearTimeout(this.autoHideTimer);
1576
+ this.autoHideTimer = null;
1577
+ }
1578
+
1579
+ this.cleanupQualityChange();
1580
+ this.clearControlsTimeout();
1581
+ this.clearTitleTimeout();
1582
+
1583
+ // Destroy adaptive streaming players
1584
+ this.destroyAdaptivePlayer();
1585
+
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
+ }
1598
+
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
+ }
1606
+ if (this.chapterMarkersContainer) {
1607
+ this.chapterMarkersContainer.remove();
1608
+ }
1609
+ if (this.chapterTooltip) {
1610
+ this.chapterTooltip.remove();
1611
+ }
1612
+ if (this.posterOverlay) {
1613
+ this.posterOverlay.remove();
1614
+ }
1615
+ this.disposeAllPlugins();
1616
+
1617
+ }
1618
+
1619
+ /**
1620
+
1621
+ * Apply specified resolution mode to video
1622
+
1623
+ * @param {string} resolution - The resolution mode to apply
1624
+
1625
+ */
1626
+
1627
+ /**
1628
+
1629
+ * Get currently set resolution
1630
+
1631
+ * @returns {string} Current resolution
1632
+
1633
+ */
1634
+
1635
+ /**
1636
+
1637
+ * Initialize resolution from options value
1638
+
1639
+ */
1640
+
1641
+ /**
1642
+
1643
+ * Restore resolution after quality change - internal method
1644
+
1645
+ * @private
1646
+
1647
+ */
1648
+
1649
+ // Core methods for main class
1650
+ // All original functionality preserved exactly