myetv-player 1.4.0 → 1.6.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.
@@ -25,7 +25,9 @@ class VideoPlayerI18n {
25
25
  'prev_video': 'Video precedente (P)',
26
26
  'playlist_next': 'Avanti',
27
27
  'playlist_prev': 'Indietro',
28
- 'settings_menu': 'Impostazioni'
28
+ 'settings_menu': 'Impostazioni',
29
+ 'loading': 'Caricamento...',
30
+ 'encoding_in_progress': 'Encoding in corso'
29
31
  },
30
32
 
31
33
  // English
@@ -47,7 +49,9 @@ class VideoPlayerI18n {
47
49
  'prev_video': 'Previous video (P)',
48
50
  'playlist_next': 'Next',
49
51
  'playlist_prev': 'Previous',
50
- 'settings_menu': 'Settings'
52
+ 'settings_menu': 'Settings',
53
+ 'loading': 'Loading...',
54
+ 'encoding_in_progress': 'Encoding in progress'
51
55
  },
52
56
 
53
57
  // Español
@@ -69,7 +73,9 @@ class VideoPlayerI18n {
69
73
  'prev_video': 'Vídeo anterior (P)',
70
74
  'playlist_next': 'Siguiente',
71
75
  'playlist_prev': 'Anterior',
72
- 'settings_menu': 'Configuración'
76
+ 'settings_menu': 'Configuración',
77
+ 'loading': 'Cargando...',
78
+ 'encoding_in_progress': 'Codificación en curso'
73
79
  },
74
80
 
75
81
  // Français
@@ -83,7 +89,7 @@ class VideoPlayerI18n {
83
89
  'volume': 'Volume',
84
90
  'playback_speed': 'Vitesse de lecture',
85
91
  'video_quality': 'Qualité vidéo',
86
- 'picture_in_picture': 'Image dans limage (P)',
92
+ 'picture_in_picture': 'Image dans l\'image(P)',
87
93
  'fullscreen': 'Plein écran (F)',
88
94
  'auto': 'Auto',
89
95
  'brand_logo': 'Logo de marque',
@@ -91,7 +97,9 @@ class VideoPlayerI18n {
91
97
  'prev_video': 'Vidéo précédente (P)',
92
98
  'playlist_next': 'Suivant',
93
99
  'playlist_prev': 'Précédent',
94
- 'settings_menu': 'Paramètres'
100
+ 'settings_menu': 'Paramètres',
101
+ 'loading': 'Chargement...',
102
+ 'encoding_in_progress': 'Encodage en cours'
95
103
  },
96
104
 
97
105
  // Deutsch
@@ -113,7 +121,9 @@ class VideoPlayerI18n {
113
121
  'prev_video': 'Vorheriges Video (P)',
114
122
  'playlist_next': 'Weiter',
115
123
  'playlist_prev': 'Zurück',
116
- 'settings_menu': 'Einstellungen'
124
+ 'settings_menu': 'Einstellungen',
125
+ 'loading': 'Laden...',
126
+ 'encoding_in_progress': 'Kodierung läuft'
117
127
  },
118
128
 
119
129
  // Português
@@ -135,7 +145,9 @@ class VideoPlayerI18n {
135
145
  'prev_video': 'Vídeo anterior (P)',
136
146
  'playlist_next': 'Próximo',
137
147
  'playlist_prev': 'Anterior',
138
- 'settings_menu': 'Configurações'
148
+ 'settings_menu': 'Configurações',
149
+ 'loading': 'Carregando...',
150
+ 'encoding_in_progress': 'Codificação em andamento'
139
151
  },
140
152
 
141
153
  // 中文
@@ -157,7 +169,9 @@ class VideoPlayerI18n {
157
169
  'prev_video': '上一个视频 (P)',
158
170
  'playlist_next': '下一个',
159
171
  'playlist_prev': '上一个',
160
- 'settings_menu': '设置'
172
+ 'settings_menu': '设置',
173
+ 'loading': '加载中...',
174
+ 'encoding_in_progress': '编码中'
161
175
  },
162
176
 
163
177
  // 日本語
@@ -179,7 +193,9 @@ class VideoPlayerI18n {
179
193
  'prev_video': '前の動画 (P)',
180
194
  'playlist_next': '次へ',
181
195
  'playlist_prev': '前へ',
182
- 'settings_menu': '設定'
196
+ 'settings_menu': '設定',
197
+ 'loading': '読み込み中...',
198
+ 'encoding_in_progress': 'エンコード中'
183
199
  },
184
200
 
185
201
  // Русский
@@ -201,7 +217,9 @@ class VideoPlayerI18n {
201
217
  'prev_video': 'Предыдущее видео (P)',
202
218
  'playlist_next': 'Далее',
203
219
  'playlist_prev': 'Назад',
204
- 'settings_menu': 'Настройки'
220
+ 'settings_menu': 'Настройки',
221
+ 'loading': 'Загрузка...',
222
+ 'encoding_in_progress': 'Кодирование'
205
223
  },
206
224
 
207
225
  // العربية
@@ -223,7 +241,9 @@ class VideoPlayerI18n {
223
241
  'prev_video': 'الفيديو السابق (P)',
224
242
  'playlist_next': 'التالي',
225
243
  'playlist_prev': 'السابق',
226
- 'settings_menu': 'الإعدادات'
244
+ 'settings_menu': 'الإعدادات',
245
+ 'loading': 'جاري التحميل...',
246
+ 'encoding_in_progress': 'الترميز جارٍ'
227
247
  },
228
248
 
229
249
  // 한국어 (Korean)
@@ -245,7 +265,9 @@ class VideoPlayerI18n {
245
265
  'prev_video': '이전 비디오 (P)',
246
266
  'playlist_next': '다음',
247
267
  'playlist_prev': '이전',
248
- 'settings_menu': '설정'
268
+ 'settings_menu': '설정',
269
+ 'loading': '로딩 중...',
270
+ 'encoding_in_progress': '인코딩 진행 중'
249
271
  },
250
272
 
251
273
  // Polski
@@ -267,7 +289,9 @@ class VideoPlayerI18n {
267
289
  'prev_video': 'Poprzednie wideo (P)',
268
290
  'playlist_next': 'Dalej',
269
291
  'playlist_prev': 'Wstecz',
270
- 'settings_menu': 'Ustawienia'
292
+ 'settings_menu': 'Ustawienia',
293
+ 'loading': 'Ładowanie...',
294
+ 'encoding_in_progress': 'Kodowanie w toku'
271
295
  },
272
296
 
273
297
  // Magyar
@@ -289,7 +313,9 @@ class VideoPlayerI18n {
289
313
  'prev_video': 'Előző videó (P)',
290
314
  'playlist_next': 'Következő',
291
315
  'playlist_prev': 'Előző',
292
- 'settings_menu': 'Beállítások'
316
+ 'settings_menu': 'Beállítások',
317
+ 'loading': 'Betöltés...',
318
+ 'encoding_in_progress': 'Kódolás folyamatban'
293
319
  },
294
320
 
295
321
  // Türkçe
@@ -311,7 +337,9 @@ class VideoPlayerI18n {
311
337
  'prev_video': 'Önceki video (P)',
312
338
  'playlist_next': 'Sonraki',
313
339
  'playlist_prev': 'Önceki',
314
- 'settings_menu': 'Ayarlar'
340
+ 'settings_menu': 'Ayarlar',
341
+ 'loading': 'Yükleniyor...',
342
+ 'encoding_in_progress': 'Kodlama devam ediyor'
315
343
  },
316
344
 
317
345
  // Nederlands
@@ -333,7 +361,9 @@ class VideoPlayerI18n {
333
361
  'prev_video': 'Vorige video (P)',
334
362
  'playlist_next': 'Volgende',
335
363
  'playlist_prev': 'Vorige',
336
- 'settings_menu': 'Instellingen'
364
+ 'settings_menu': 'Instellingen',
365
+ 'loading': 'Laden...',
366
+ 'encoding_in_progress': 'Codering bezig'
337
367
  },
338
368
 
339
369
  // हिन्दी (Hindi)
@@ -355,7 +385,9 @@ class VideoPlayerI18n {
355
385
  'prev_video': 'पिछला वीडियो (P)',
356
386
  'playlist_next': 'अगला',
357
387
  'playlist_prev': 'पिछला',
358
- 'settings_menu': 'सेटिंग्स'
388
+ 'settings_menu': 'सेटिंग्स',
389
+ 'loading': 'लोड हो रहा है...',
390
+ 'encoding_in_progress': 'एन्कोडिंग प्रगति में'
359
391
  },
360
392
 
361
393
  // Svenska
@@ -377,7 +409,9 @@ class VideoPlayerI18n {
377
409
  'prev_video': 'Föregående video (P)',
378
410
  'playlist_next': 'Nästa',
379
411
  'playlist_prev': 'Föregående',
380
- 'settings_menu': 'Inställningar'
412
+ 'settings_menu': 'Inställningar',
413
+ 'loading': 'Laddar...',
414
+ 'encoding_in_progress': 'Kodning pågår'
381
415
  },
382
416
 
383
417
  // Bahasa Indonesia
@@ -399,7 +433,9 @@ class VideoPlayerI18n {
399
433
  'prev_video': 'Video sebelumnya (P)',
400
434
  'playlist_next': 'Berikutnya',
401
435
  'playlist_prev': 'Sebelumnya',
402
- 'settings_menu': 'Pengaturan'
436
+ 'settings_menu': 'Pengaturan',
437
+ 'loading': 'Memuat...',
438
+ 'encoding_in_progress': 'Encoding sedang berlangsung'
403
439
  }
404
440
  };
405
441
 
@@ -527,7 +563,9 @@ try {
527
563
  'fullscreen': 'Fullscreen (F)',
528
564
  'auto': 'Auto',
529
565
  'brand_logo': 'Brand logo',
530
- 'settings_menu': 'Settings'
566
+ 'settings_menu': 'Settings',
567
+ 'loading': 'Loading...',
568
+ 'encoding_in_progress': 'Encoding in progress'
531
569
  };
532
570
  return fallback[key] || key;
533
571
  },
@@ -865,6 +903,59 @@ constructor(videoElement, options = {}) {
865
903
  }
866
904
  }
867
905
 
906
+ /**
907
+ * Decode HTML entities to normal characters
908
+ * @param {string} text - Text with HTML entities
909
+ * @returns {string} Decoded text
910
+ */
911
+ decodeHTMLEntities(text) {
912
+ if (!text) return '';
913
+ const textarea = document.createElement('textarea');
914
+ textarea.innerHTML = text;
915
+ return textarea.value;
916
+ }
917
+
918
+ // check if the device is Fire TV
919
+ isFireTV() {
920
+ const ua = navigator.userAgent.toLowerCase();
921
+ return ua.includes('aftm') ||
922
+ ua.includes('aftb') ||
923
+ ua.includes('afts') ||
924
+ ua.includes('aftmm') ||
925
+ ua.includes('aftt');
926
+ }
927
+
928
+ // apply Fire TV specific optimizations
929
+ optimizeVideoForFireTV() {
930
+ if (!this.isFireTV() || !this.video) return;
931
+
932
+ if (this.options.debug) {
933
+ console.log('Fire TV detected - applying optimizations');
934
+ }
935
+
936
+ // set playsinline attributes
937
+ this.video.setAttribute('playsinline', '');
938
+ this.video.setAttribute('webkit-playsinline', '');
939
+
940
+ // CSS optimizations
941
+ this.video.style.transform = 'translateZ(0)';
942
+ this.video.style.webkitTransform = 'translateZ(0)';
943
+ this.video.style.backfaceVisibility = 'hidden';
944
+ this.video.style.webkitBackfaceVisibility = 'hidden';
945
+ this.video.style.willChange = 'transform';
946
+
947
+ // force repaint on loadeddata
948
+ this.video.addEventListener('loadeddata', () => {
949
+ if (this.options.debug) {
950
+ console.log('Fire TV: Video loaded, forcing repaint');
951
+ }
952
+ this.video.style.display = 'none';
953
+ setTimeout(() => {
954
+ this.video.style.display = 'block';
955
+ }, 10);
956
+ }, { once: true });
957
+ }
958
+
868
959
  getPlayerState() {
869
960
  return {
870
961
  isPlaying: !this.isPaused(),
@@ -1164,6 +1255,7 @@ createPlayerStructure() {
1164
1255
 
1165
1256
  this.container = wrapper;
1166
1257
 
1258
+ this.optimizeVideoForFireTV();
1167
1259
  this.createInitialLoading();
1168
1260
  this.createLoadingOverlay();
1169
1261
  this.collectVideoQualities();
@@ -1204,14 +1296,14 @@ createTitleOverlay() {
1204
1296
 
1205
1297
  const titleText = document.createElement('h2');
1206
1298
  titleText.className = 'title-text';
1207
- titleText.textContent = this.options.videoTitle || '';
1299
+ titleText.textContent = this.decodeHTMLEntities(this.options.videoTitle) || '';
1208
1300
  overlay.appendChild(titleText);
1209
1301
 
1210
1302
  // add subtitles
1211
1303
  if (this.options.videoSubtitle) {
1212
1304
  const subtitleText = document.createElement('p');
1213
1305
  subtitleText.className = 'subtitle-text';
1214
- subtitleText.textContent = this.options.videoSubtitle;
1306
+ subtitleText.textContent = this.decodeHTMLEntities(this.options.videoSubtitle);
1215
1307
  overlay.appendChild(subtitleText);
1216
1308
  }
1217
1309
 
@@ -1266,7 +1358,7 @@ setVideoTitle(title) {
1266
1358
  if (this.titleOverlay) {
1267
1359
  const titleElement = this.titleOverlay.querySelector('.title-text');
1268
1360
  if (titleElement) {
1269
- titleElement.textContent = this.options.videoTitle;
1361
+ titleElement.textContent = this.decodeHTMLEntities(this.options.videoTitle);
1270
1362
  }
1271
1363
 
1272
1364
  if (title) {
@@ -1748,9 +1840,10 @@ updateProgress() {
1748
1840
  this.progressHandle.style.left = progress + '%';
1749
1841
  }
1750
1842
 
1843
+ // Always call updateTimeDisplay, regardless of duration validity
1751
1844
  this.updateTimeDisplay();
1752
1845
 
1753
- // Trigger timeupdate event (with throttling to avoid too many events)
1846
+ // Trigger timeupdate event with throttling
1754
1847
  if (!this.lastTimeUpdate || Date.now() - this.lastTimeUpdate > 250) {
1755
1848
  this.triggerEvent('timeupdate', {
1756
1849
  currentTime: this.getCurrentTime(),
@@ -1829,6 +1922,8 @@ updateDuration() {
1829
1922
  if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
1830
1923
  this.durationEl.textContent = this.formatTime(this.video.duration);
1831
1924
  }
1925
+ // Call updateTimeDisplay to handle all states (loading, encoding, normal)
1926
+ this.updateTimeDisplay();
1832
1927
  }
1833
1928
 
1834
1929
  changeSpeed(e) {
@@ -2121,7 +2216,7 @@ switchToVideo(newVideoElement, shouldPlay = false) {
2121
2216
  if (newTitle && this.options.showTitleOverlay) {
2122
2217
  this.options.videoTitle = newTitle;
2123
2218
  if (this.titleText) {
2124
- this.titleText.textContent = newTitle;
2219
+ this.titleText.textContent = this.decodeHTMLEntities(newTitle);
2125
2220
  }
2126
2221
  }
2127
2222
 
@@ -2715,6 +2810,10 @@ addEventListener(eventType, callback) {
2715
2810
  if (!this.isChangingQuality) {
2716
2811
  this.showLoading();
2717
2812
  }
2813
+
2814
+ // Update time display to show "Loading..." during initial buffering
2815
+ this.updateTimeDisplay();
2816
+
2718
2817
  // Trigger loadstart event - browser started loading media
2719
2818
  this.triggerEvent('loadstart');
2720
2819
  });
@@ -2722,6 +2821,9 @@ addEventListener(eventType, callback) {
2722
2821
  this.video.addEventListener('loadedmetadata', () => {
2723
2822
  this.updateDuration();
2724
2823
 
2824
+ // Update time display when metadata is loaded
2825
+ this.updateTimeDisplay();
2826
+
2725
2827
  // Trigger loadedmetadata event - video metadata loaded
2726
2828
  this.triggerEvent('loadedmetadata', {
2727
2829
  duration: this.getDuration(),
@@ -2739,6 +2841,10 @@ addEventListener(eventType, callback) {
2739
2841
  if (!this.isChangingQuality) {
2740
2842
  this.hideLoading();
2741
2843
  }
2844
+
2845
+ // Update time display when data is loaded
2846
+ this.updateTimeDisplay();
2847
+
2742
2848
  // Trigger loadeddata event - current frame data loaded
2743
2849
  this.triggerEvent('loadeddata', {
2744
2850
  currentTime: this.getCurrentTime()
@@ -2749,6 +2855,10 @@ addEventListener(eventType, callback) {
2749
2855
  if (!this.isChangingQuality) {
2750
2856
  this.hideLoading();
2751
2857
  }
2858
+
2859
+ // Update time display when video can play
2860
+ this.updateTimeDisplay();
2861
+
2752
2862
  // Trigger canplay event - video can start playing
2753
2863
  this.triggerEvent('canplay', {
2754
2864
  currentTime: this.getCurrentTime(),
@@ -2756,6 +2866,21 @@ addEventListener(eventType, callback) {
2756
2866
  });
2757
2867
  });
2758
2868
 
2869
+ // Also add to waiting event
2870
+ this.video.addEventListener('waiting', () => {
2871
+ if (!this.isChangingQuality) {
2872
+ this.showLoading();
2873
+
2874
+ // Update time display during buffering
2875
+ this.updateTimeDisplay();
2876
+
2877
+ // Trigger waiting event - video is buffering
2878
+ this.triggerEvent('waiting', {
2879
+ currentTime: this.getCurrentTime()
2880
+ });
2881
+ }
2882
+ });
2883
+
2759
2884
  this.video.addEventListener('progress', () => {
2760
2885
  this.updateBuffer();
2761
2886
  // Trigger progress event - browser is downloading media
@@ -3498,7 +3623,7 @@ createControls() {
3498
3623
  <div class="settings-menu"></div>
3499
3624
  </div>
3500
3625
 
3501
- ${this.options.showQualitySelector && this.originalSources && this.originalSources.length > 1 ? `
3626
+ ${(this.options.showQualitySelector && this.originalSources && this.originalSources.length > 1) || this.options.adaptiveQualityControl ? `
3502
3627
  <div class="quality-control">
3503
3628
  <button class="control-btn quality-btn" data-tooltip="video_quality">
3504
3629
  <div class="quality-btn-text">
@@ -4369,6 +4494,11 @@ updateQualityDisplay() {
4369
4494
  }
4370
4495
 
4371
4496
  updateQualityButton() {
4497
+ if (this.isAdaptiveStream) {
4498
+ if (this.options.debug) console.log('🔒 Adaptive streaming active - quality button managed by streaming.js');
4499
+ return;
4500
+ }
4501
+
4372
4502
  const qualityBtn = this.controls?.querySelector('.quality-btn');
4373
4503
  if (!qualityBtn) return;
4374
4504
 
@@ -4420,6 +4550,11 @@ updateQualityMenu() {
4420
4550
  const qualityMenu = this.controls?.querySelector('.quality-menu');
4421
4551
  if (!qualityMenu) return;
4422
4552
 
4553
+ if (this.isAdaptiveStream) {
4554
+ if (this.options.debug) console.log('🔒 Adaptive streaming active - quality menu managed by streaming.js');
4555
+ return;
4556
+ }
4557
+
4423
4558
  let menuHTML = '';
4424
4559
 
4425
4560
  // Check if adaptive streaming is active (HLS/DASH)
@@ -7095,6 +7230,16 @@ async loadAdaptiveLibraries() {
7095
7230
  }
7096
7231
 
7097
7232
  try {
7233
+ // Initialize quality selection to Auto BEFORE creating player
7234
+
7235
+ // FORCE Auto mode - always reset at initialization
7236
+ this.selectedQuality = 'auto';
7237
+ this.qualityEventsInitialized = false;
7238
+
7239
+ if (this.options.debug) {
7240
+ console.log('🔍 initializeDash - FORCED selectedQuality to:', this.selectedQuality);
7241
+ }
7242
+
7098
7243
  // Destroy existing DASH player
7099
7244
  if (this.dashPlayer) {
7100
7245
  this.dashPlayer.destroy();
@@ -7259,35 +7404,464 @@ disableDashTextTracks() {
7259
7404
  }
7260
7405
  }
7261
7406
 
7262
- updateAdaptiveQualities() {
7263
- this.adaptiveQualities = [];
7407
+ updateAdaptiveQualities() {
7408
+ this.adaptiveQualities = [];
7264
7409
 
7410
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
7411
+ try {
7412
+ // dash.js 5.x - Get ALL video tracks
7413
+ const videoTracks = this.dashPlayer.getTracksFor('video');
7414
+
7415
+ if (this.options.debug) {
7416
+ console.log('✅ DASH getTracksFor result:', videoTracks);
7417
+ }
7418
+
7419
+ if (videoTracks && videoTracks.length > 0) {
7420
+ // Collect qualities from ALL video tracks
7421
+ const allQualities = [];
7422
+
7423
+ videoTracks.forEach((track, trackIndex) => {
7424
+ const bitrateList = track.bitrateList || [];
7425
+
7426
+ if (this.options.debug) {
7427
+ console.log(`✅ Track ${trackIndex} (${track.codec}):`, bitrateList);
7428
+ }
7429
+
7430
+ bitrateList.forEach((bitrate, index) => {
7431
+ allQualities.push({
7432
+ trackIndex: trackIndex,
7433
+ bitrateIndex: index,
7434
+ label: `${bitrate.height}p`,
7435
+ height: bitrate.height,
7436
+ width: bitrate.width,
7437
+ bandwidth: bitrate.bandwidth,
7438
+ codec: track.codec
7439
+ });
7440
+ });
7441
+ });
7442
+
7443
+ // Sort by height (descending) and remove duplicates
7444
+ const uniqueHeights = [...new Set(allQualities.map(q => q.height))];
7445
+ uniqueHeights.sort((a, b) => b - a);
7446
+
7447
+ this.adaptiveQualities = uniqueHeights.map((height, index) => {
7448
+ const quality = allQualities.find(q => q.height === height);
7449
+ return {
7450
+ index: index,
7451
+ label: `${height}p`,
7452
+ height: height,
7453
+ trackIndex: quality.trackIndex,
7454
+ bitrateIndex: quality.bitrateIndex,
7455
+ bandwidth: quality.bandwidth,
7456
+ codec: quality.codec
7457
+ };
7458
+ });
7459
+
7460
+ if (this.options.debug) {
7461
+ console.log('✅ All DASH qualities merged:', this.adaptiveQualities);
7462
+ }
7463
+ }
7464
+ } catch (error) {
7465
+ if (this.options.debug) {
7466
+ console.error('❌ Error getting DASH qualities:', error);
7467
+ }
7468
+ }
7469
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
7470
+ const levels = this.hlsPlayer.levels;
7471
+ this.adaptiveQualities = levels.map((level, index) => ({
7472
+ index: index,
7473
+ label: this.getQualityLabel(level.height, level.width),
7474
+ height: level.height,
7475
+ bandwidth: level.bitrate
7476
+ }));
7477
+ }
7478
+
7479
+ if (this.options.adaptiveQualityControl) {
7480
+ this.updateAdaptiveQualityMenu();
7481
+ }
7482
+
7483
+ if (this.options.debug) {
7484
+ console.log('📡 Adaptive qualities available:', this.adaptiveQualities);
7485
+ console.log('📡 Selected quality mode:', this.selectedQuality);
7486
+ }
7487
+ }
7488
+
7489
+ updateAdaptiveQualityMenu() {
7490
+ const qualityMenu = this.controls?.querySelector('.quality-menu');
7491
+ if (!qualityMenu) {
7492
+ if (this.options.debug) console.log('❌ Quality menu not found in DOM');
7493
+ return;
7494
+ }
7495
+
7496
+ if (this.adaptiveQualities.length === 0) {
7497
+ if (this.options.debug) console.log('❌ No adaptive qualities to display');
7498
+ return;
7499
+ }
7500
+
7501
+ // Generate menu HTML with "Auto" option
7502
+ const isAutoActive = this.selectedQuality === 'auto';
7503
+ let menuHTML = `<div class="quality-option ${isAutoActive ? 'active' : ''}" data-quality="auto">Auto</div>`;
7504
+
7505
+ // Add all quality options
7506
+ this.adaptiveQualities.forEach((quality) => {
7507
+ const isActive = this.selectedQuality === quality.height;
7508
+
7509
+ if (this.options.debug) {
7510
+ console.log('🔍 Quality item:', quality.label, 'height:', quality.height, 'active:', isActive);
7511
+ }
7512
+
7513
+ menuHTML += `<div class="quality-option ${isActive ? 'active' : ''}" data-quality="${quality.height}">
7514
+ ${quality.label}
7515
+ <span class="quality-playing" style="display: none; color: #4CAF50; margin-left: 8px; font-size: 0.85em;">● Playing</span>
7516
+ </div>`;
7517
+ });
7518
+
7519
+ qualityMenu.innerHTML = menuHTML;
7520
+
7521
+ if (this.options.debug) {
7522
+ console.log('✅ Quality menu populated with', this.adaptiveQualities.length, 'options');
7523
+ }
7524
+
7525
+ // Bind events ONCE
7526
+ if (!this.qualityEventsInitialized) {
7527
+ this.bindAdaptiveQualityEvents();
7528
+ this.qualityEventsInitialized = true;
7529
+ }
7530
+
7531
+ // Update display
7532
+ this.updateAdaptiveQualityDisplay();
7533
+ }
7534
+
7535
+ updateAdaptiveQualityDisplay() {
7536
+ if (!this.dashPlayer && !this.hlsPlayer) return;
7537
+
7538
+ let currentHeight = null;
7539
+
7540
+ try {
7265
7541
  if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
7266
- const bitrates = this.dashPlayer.getBitrateInfoListFor('video');
7267
- this.adaptiveQualities = bitrates.map((bitrate, index) => ({
7268
- index: index,
7269
- label: this.getQualityLabel(bitrate.height, bitrate.width),
7270
- height: bitrate.height,
7271
- bandwidth: bitrate.bandwidth
7272
- }));
7542
+ // Get video element to check actual resolution
7543
+ if (this.video && this.video.videoHeight) {
7544
+ currentHeight = this.video.videoHeight;
7545
+ }
7546
+
7547
+ if (this.options.debug) {
7548
+ console.log('📊 Current video height:', currentHeight, 'Selected mode:', this.selectedQuality);
7549
+ }
7273
7550
  } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
7274
- const levels = this.hlsPlayer.levels;
7275
- this.adaptiveQualities = levels.map((level, index) => ({
7276
- index: index,
7277
- label: this.getQualityLabel(level.height, level.width),
7278
- height: level.height,
7279
- bandwidth: level.bitrate
7280
- }));
7551
+ const currentLevel = this.hlsPlayer.currentLevel;
7552
+ if (currentLevel >= 0 && this.hlsPlayer.levels[currentLevel]) {
7553
+ currentHeight = this.hlsPlayer.levels[currentLevel].height;
7554
+ }
7555
+ }
7556
+
7557
+ // Update button text (top text)
7558
+ const qualityBtnText = this.controls?.querySelector('.quality-btn .selected-quality');
7559
+ if (qualityBtnText) {
7560
+ if (this.selectedQuality === 'auto') {
7561
+ qualityBtnText.textContent = 'Auto';
7562
+ } else {
7563
+ qualityBtnText.textContent = `${this.selectedQuality}p`;
7564
+ }
7565
+ }
7566
+
7567
+ // Update current quality display (bottom text) - ONLY in Auto mode
7568
+ const currentQualityText = this.controls?.querySelector('.quality-btn .current-quality');
7569
+ if (currentQualityText) {
7570
+ if (this.selectedQuality === 'auto' && currentHeight) {
7571
+ currentQualityText.textContent = `${currentHeight}p`;
7572
+ currentQualityText.style.display = 'block';
7573
+ } else {
7574
+ currentQualityText.textContent = '';
7575
+ currentQualityText.style.display = 'none';
7576
+ }
7577
+ }
7578
+
7579
+ // Update menu active states
7580
+ const qualityMenu = this.controls?.querySelector('.quality-menu');
7581
+ if (qualityMenu) {
7582
+ // Remove all active states
7583
+ qualityMenu.querySelectorAll('.quality-option').forEach(opt => {
7584
+ opt.classList.remove('active');
7585
+ });
7586
+
7587
+ // Set active based on selection
7588
+ if (this.selectedQuality === 'auto') {
7589
+ const autoOption = qualityMenu.querySelector('[data-quality="auto"]');
7590
+ if (autoOption) autoOption.classList.add('active');
7591
+ } else {
7592
+ const selectedOption = qualityMenu.querySelector(`[data-quality="${this.selectedQuality}"]`);
7593
+ if (selectedOption) selectedOption.classList.add('active');
7594
+ }
7595
+
7596
+ // Hide all playing indicators
7597
+ qualityMenu.querySelectorAll('.quality-playing').forEach(el => {
7598
+ el.style.display = 'none';
7599
+ });
7600
+
7601
+ // Show playing indicator only in Auto mode
7602
+ if (this.selectedQuality === 'auto' && currentHeight) {
7603
+ const playingOption = qualityMenu.querySelector(`[data-quality="${currentHeight}"] .quality-playing`);
7604
+ if (playingOption) {
7605
+ playingOption.style.display = 'inline';
7606
+ }
7607
+ }
7608
+ }
7609
+
7610
+ } catch (error) {
7611
+ if (this.options.debug) console.error('❌ Error updating quality display:', error);
7612
+ }
7613
+ }
7614
+
7615
+ updateQualityButtonText() {
7616
+ const qualityBtn = this.controls?.querySelector('.quality-btn .selected-quality');
7617
+ if (!qualityBtn) return;
7618
+
7619
+ if (this.selectedQuality === 'auto' || !this.selectedQuality) {
7620
+ qualityBtn.textContent = this.t('auto');
7621
+ } else {
7622
+ const quality = this.adaptiveQualities.find(q => q.index === parseInt(this.selectedQuality));
7623
+ qualityBtn.textContent = quality ? quality.label : 'Auto';
7624
+ }
7625
+ }
7626
+
7627
+ bindAdaptiveQualityEvents() {
7628
+ const qualityMenu = this.controls?.querySelector('.quality-menu');
7629
+ const qualityBtn = this.controls?.querySelector('.quality-btn');
7630
+
7631
+ if (!qualityMenu || !qualityBtn) return;
7632
+
7633
+ // Toggle menu
7634
+ qualityBtn.addEventListener('click', (e) => {
7635
+ e.stopPropagation();
7636
+ qualityMenu.classList.toggle('active');
7637
+
7638
+ // Update display when opening
7639
+ if (qualityMenu.classList.contains('active')) {
7640
+ this.updateAdaptiveQualityDisplay();
7281
7641
  }
7642
+ });
7282
7643
 
7283
- if (this.options.adaptiveQualityControl) {
7284
- this.updateAdaptiveQualityMenu();
7644
+ // Close menu on outside click
7645
+ const closeMenuHandler = (e) => {
7646
+ if (!qualityBtn.contains(e.target) && !qualityMenu.contains(e.target)) {
7647
+ qualityMenu.classList.remove('active');
7285
7648
  }
7649
+ };
7650
+ document.addEventListener('click', closeMenuHandler);
7651
+
7652
+ // Handle quality selection
7653
+ qualityMenu.addEventListener('click', (e) => {
7654
+ const option = e.target.closest('.quality-option');
7655
+ if (!option) return;
7656
+
7657
+ e.stopPropagation();
7658
+
7659
+ const qualityData = option.getAttribute('data-quality');
7286
7660
 
7287
7661
  if (this.options.debug) {
7288
- console.log('📡 Adaptive qualities available:', this.adaptiveQualities);
7662
+ console.log('🎬 Quality clicked - raw data:', qualityData, 'type:', typeof qualityData);
7663
+ }
7664
+
7665
+ if (qualityData === 'auto') {
7666
+ // Enable auto mode
7667
+ this.selectedQuality = 'auto';
7668
+
7669
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
7670
+ this.dashPlayer.updateSettings({
7671
+ streaming: {
7672
+ abr: {
7673
+ autoSwitchBitrate: { video: true }
7674
+ }
7675
+ }
7676
+ });
7677
+ if (this.options.debug) console.log('✅ Auto quality enabled');
7678
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
7679
+ this.hlsPlayer.currentLevel = -1;
7680
+ }
7681
+
7682
+ } else {
7683
+ // Manual quality selection
7684
+ const selectedHeight = parseInt(qualityData, 10);
7685
+
7686
+ if (isNaN(selectedHeight)) {
7687
+ if (this.options.debug) console.error('❌ Invalid quality data:', qualityData);
7688
+ return;
7689
+ }
7690
+
7691
+ if (this.options.debug) {
7692
+ console.log('🎬 Setting manual quality to height:', selectedHeight);
7693
+ }
7694
+
7695
+ this.selectedQuality = selectedHeight;
7696
+
7697
+ if (this.adaptiveStreamingType === 'dash') {
7698
+ this.setDashQualityByHeight(selectedHeight);
7699
+ } else if (this.adaptiveStreamingType === 'hls') {
7700
+ const levelIndex = this.hlsPlayer.levels.findIndex(l => l.height === selectedHeight);
7701
+ if (levelIndex >= 0) {
7702
+ this.hlsPlayer.currentLevel = levelIndex;
7703
+ }
7704
+ }
7289
7705
  }
7706
+
7707
+ // Update display immediately
7708
+ this.updateAdaptiveQualityDisplay();
7709
+
7710
+ // Close menu
7711
+ qualityMenu.classList.remove('active');
7712
+ });
7713
+
7714
+ if (this.options.debug) {
7715
+ console.log('✅ Quality events bound');
7290
7716
  }
7717
+ }
7718
+
7719
+ setDashQualityByHeight(targetHeight) {
7720
+ if (!this.dashPlayer) return;
7721
+
7722
+ try {
7723
+ const targetQuality = this.adaptiveQualities.find(q => q.height === targetHeight);
7724
+ if (!targetQuality) {
7725
+ if (this.options.debug) console.error('❌ Quality not found for height:', targetHeight);
7726
+ return;
7727
+ }
7728
+
7729
+ if (this.options.debug) {
7730
+ console.log('🎬 Setting quality:', targetQuality);
7731
+ }
7732
+
7733
+ // Disable auto quality
7734
+ this.dashPlayer.updateSettings({
7735
+ streaming: {
7736
+ abr: {
7737
+ autoSwitchBitrate: { video: false }
7738
+ }
7739
+ }
7740
+ });
7741
+
7742
+ // Get current video track
7743
+ const currentTrack = this.dashPlayer.getCurrentTrackFor('video');
7744
+
7745
+ if (!currentTrack) {
7746
+ if (this.options.debug) console.error('❌ No current video track');
7747
+ return;
7748
+ }
7749
+
7750
+ // Find the correct track for this quality
7751
+ const allTracks = this.dashPlayer.getTracksFor('video');
7752
+ let targetTrack = null;
7753
+
7754
+ for (const track of allTracks) {
7755
+ if (track.bitrateList && track.bitrateList[targetQuality.bitrateIndex]) {
7756
+ const bitrate = track.bitrateList[targetQuality.bitrateIndex];
7757
+ if (bitrate.height === targetHeight) {
7758
+ targetTrack = track;
7759
+ break;
7760
+ }
7761
+ }
7762
+ }
7763
+
7764
+ if (!targetTrack) {
7765
+ if (this.options.debug) console.error('❌ Target track not found');
7766
+ return;
7767
+ }
7768
+
7769
+ // Switch track if different
7770
+ if (currentTrack.index !== targetTrack.index) {
7771
+ this.dashPlayer.setCurrentTrack(targetTrack);
7772
+ if (this.options.debug) {
7773
+ console.log('✅ Switched to track:', targetTrack.index);
7774
+ }
7775
+ }
7776
+
7777
+ // Force quality on current track
7778
+ setTimeout(() => {
7779
+ try {
7780
+ // Use the MediaPlayer API to set quality
7781
+ this.dashPlayer.updateSettings({
7782
+ streaming: {
7783
+ abr: {
7784
+ initialBitrate: { video: targetQuality.bandwidth / 1000 },
7785
+ maxBitrate: { video: targetQuality.bandwidth / 1000 },
7786
+ minBitrate: { video: targetQuality.bandwidth / 1000 }
7787
+ }
7788
+ }
7789
+ });
7790
+
7791
+ if (this.options.debug) {
7792
+ console.log('✅ Quality locked to:', targetHeight + 'p', 'bandwidth:', targetQuality.bandwidth);
7793
+ }
7794
+
7795
+ // Update button text immediately
7796
+ const qualityBtnText = this.controls?.querySelector('.quality-btn .selected-quality');
7797
+ if (qualityBtnText) {
7798
+ qualityBtnText.textContent = `${targetHeight}p`;
7799
+ }
7800
+
7801
+ // Force reload of segments at new quality
7802
+ const currentTime = this.video.currentTime;
7803
+ this.dashPlayer.seek(currentTime + 0.1);
7804
+ setTimeout(() => {
7805
+ this.dashPlayer.seek(currentTime);
7806
+ }, 100);
7807
+
7808
+ } catch (innerError) {
7809
+ if (this.options.debug) console.error('❌ Error setting quality:', innerError);
7810
+ }
7811
+ }, 100);
7812
+
7813
+ } catch (error) {
7814
+ if (this.options.debug) console.error('❌ Error in setDashQualityByHeight:', error);
7815
+ }
7816
+ }
7817
+
7818
+ setDashQuality(qualityIndex) {
7819
+ if (!this.dashPlayer) return;
7820
+
7821
+ try {
7822
+ const selectedQuality = this.adaptiveQualities[qualityIndex];
7823
+ if (!selectedQuality) {
7824
+ if (this.options.debug) console.error('❌ Quality not found at index:', qualityIndex);
7825
+ return;
7826
+ }
7827
+
7828
+ if (this.options.debug) {
7829
+ console.log('🎬 Setting DASH quality:', selectedQuality);
7830
+ }
7831
+
7832
+ // Disable auto quality
7833
+ this.dashPlayer.updateSettings({
7834
+ streaming: {
7835
+ abr: {
7836
+ autoSwitchBitrate: { video: false }
7837
+ }
7838
+ }
7839
+ });
7840
+
7841
+ // Set the specific quality using bitrateIndex
7842
+ setTimeout(() => {
7843
+ try {
7844
+ this.dashPlayer.setQualityFor('video', selectedQuality.bitrateIndex);
7845
+
7846
+ if (this.options.debug) {
7847
+ console.log('✅ DASH quality set to bitrateIndex:', selectedQuality.bitrateIndex, 'height:', selectedQuality.height);
7848
+ }
7849
+
7850
+ // Update button text immediately
7851
+ const qualityBtnText = this.controls?.querySelector('.quality-btn .selected-quality');
7852
+ if (qualityBtnText) {
7853
+ qualityBtnText.textContent = selectedQuality.label;
7854
+ }
7855
+
7856
+ } catch (innerError) {
7857
+ if (this.options.debug) console.error('❌ Error setting quality:', innerError);
7858
+ }
7859
+ }, 100);
7860
+
7861
+ } catch (error) {
7862
+ if (this.options.debug) console.error('❌ Error in setDashQuality:', error);
7863
+ }
7864
+ }
7291
7865
 
7292
7866
  handleAdaptiveError(data) {
7293
7867
  if (this.options.debug) console.error('📡 Fatal adaptive streaming error:', data);
@@ -7721,63 +8295,79 @@ removePluginControlButton(buttonId) {
7721
8295
  }
7722
8296
 
7723
8297
  getBufferedTime() {
7724
- if (!this.video || !this.video.buffered || this.video.buffered.length === 0) return 0;
7725
- try {
7726
- return this.video.buffered.end(this.video.buffered.length - 1);
7727
- } catch (error) {
7728
- return 0;
7729
- }
8298
+ if (!this.video || !this.video.buffered || this.video.buffered.length === 0) return 0;
8299
+ try {
8300
+ return this.video.buffered.end(this.video.buffered.length - 1);
8301
+ } catch (error) {
8302
+ return 0;
7730
8303
  }
8304
+ }
7731
8305
 
7732
- clearTitleTimeout() {
7733
- if (this.titleTimeout) {
7734
- clearTimeout(this.titleTimeout);
7735
- this.titleTimeout = null;
7736
- }
8306
+ clearTitleTimeout() {
8307
+ if (this.titleTimeout) {
8308
+ clearTimeout(this.titleTimeout);
8309
+ this.titleTimeout = null;
7737
8310
  }
8311
+ }
7738
8312
 
7739
- skipTime(seconds) {
7740
- if (!this.video || !this.video.duration || this.isChangingQuality) return;
8313
+ skipTime(seconds) {
8314
+ if (!this.video || !this.video.duration || this.isChangingQuality) return;
7741
8315
 
7742
- this.video.currentTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
7743
- }
8316
+ this.video.currentTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
8317
+ }
7744
8318
 
7745
8319
  updateTimeDisplay() {
7746
- // update current time
8320
+ // Update current time
7747
8321
  if (this.currentTimeEl && this.video) {
7748
8322
  this.currentTimeEl.textContent = this.formatTime(this.video.currentTime || 0);
7749
8323
  }
7750
8324
 
7751
- // update duration or show badge if encoding
8325
+ // Update duration or show appropriate message
7752
8326
  if (this.durationEl && this.video) {
7753
8327
  const duration = this.video.duration;
7754
-
7755
- // check if duration is valid
7756
- if (!duration || isNaN(duration) || !isFinite(duration)) {
7757
- // Video in encoding - show badge instead of duration
7758
- this.durationEl.innerHTML = '<span class="encoding-badge">Encoding in progress</span>';
8328
+ const readyState = this.video.readyState;
8329
+ const currentTime = this.video.currentTime;
8330
+ const networkState = this.video.networkState;
8331
+
8332
+ // Check for initial buffering state
8333
+ // readyState < 2 means not enough data to play (HAVE_NOTHING or HAVE_METADATA)
8334
+ // currentTime === 0 and duration === 0 indicates initial loading
8335
+ const isInitialBuffering = (readyState < 2 && currentTime === 0) ||
8336
+ (currentTime === 0 && (!duration || duration === 0) && networkState === 2);
8337
+
8338
+ // Check if duration is invalid (NaN or Infinity)
8339
+ const isDurationInvalid = !duration || isNaN(duration) || !isFinite(duration);
8340
+
8341
+ if (isInitialBuffering) {
8342
+ // Initial buffering - show loading message
8343
+ this.durationEl.textContent = t('loading');
8344
+ this.durationEl.classList.remove('encoding-state');
8345
+ this.durationEl.classList.add('loading-state');
8346
+ } else if (isDurationInvalid) {
8347
+ // Video is encoding (FFmpeg still processing) - show encoding badge
8348
+ this.durationEl.textContent = t('encoding_in_progress');
8349
+ this.durationEl.classList.remove('loading-state');
7759
8350
  this.durationEl.classList.add('encoding-state');
7760
8351
  } else {
7761
- // valid duration - show normal
8352
+ // Valid duration - show normal time
7762
8353
  this.durationEl.textContent = this.formatTime(duration);
7763
- this.durationEl.classList.remove('encoding-state');
8354
+ this.durationEl.classList.remove('encoding-state', 'loading-state');
7764
8355
  }
7765
8356
  }
7766
8357
  }
7767
8358
 
8359
+ formatTime(seconds) {
8360
+ if (isNaN(seconds) || seconds < 0) return '0:00';
7768
8361
 
7769
- formatTime(seconds) {
7770
- if (isNaN(seconds) || seconds < 0) return '0:00';
7771
-
7772
- const hours = Math.floor(seconds / 3600);
7773
- const minutes = Math.floor((seconds % 3600) / 60);
7774
- const secs = Math.floor(seconds % 60);
8362
+ const hours = Math.floor(seconds / 3600);
8363
+ const minutes = Math.floor((seconds % 3600) / 60);
8364
+ const secs = Math.floor(seconds % 60);
7775
8365
 
7776
- if (hours > 0) {
7777
- return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
7778
- }
7779
- return `${minutes}:${secs.toString().padStart(2, '0')}`;
8366
+ if (hours > 0) {
8367
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
7780
8368
  }
8369
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
8370
+ }
7781
8371
 
7782
8372
  }
7783
8373