myetv-player 1.3.0 → 1.5.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(),
@@ -1108,6 +1199,11 @@ markPlayerReady() {
1108
1199
  this.video.style.pointerEvents = '';
1109
1200
  }
1110
1201
 
1202
+ // UPDATE SETTINGS MENU VISIBILITY IF APPLICABLE
1203
+ if (typeof this.updateSettingsMenuVisibility === 'function') {
1204
+ this.updateSettingsMenuVisibility();
1205
+ }
1206
+
1111
1207
  // INITIALIZE AUTO-HIDE AFTER EVERYTHING IS READY
1112
1208
  setTimeout(() => {
1113
1209
  if (this.options.autoHide && !this.autoHideInitialized) {
@@ -1159,6 +1255,7 @@ createPlayerStructure() {
1159
1255
 
1160
1256
  this.container = wrapper;
1161
1257
 
1258
+ this.optimizeVideoForFireTV();
1162
1259
  this.createInitialLoading();
1163
1260
  this.createLoadingOverlay();
1164
1261
  this.collectVideoQualities();
@@ -1199,14 +1296,14 @@ createTitleOverlay() {
1199
1296
 
1200
1297
  const titleText = document.createElement('h2');
1201
1298
  titleText.className = 'title-text';
1202
- titleText.textContent = this.options.videoTitle || '';
1299
+ titleText.textContent = this.decodeHTMLEntities(this.options.videoTitle) || '';
1203
1300
  overlay.appendChild(titleText);
1204
1301
 
1205
1302
  // add subtitles
1206
1303
  if (this.options.videoSubtitle) {
1207
1304
  const subtitleText = document.createElement('p');
1208
1305
  subtitleText.className = 'subtitle-text';
1209
- subtitleText.textContent = this.options.videoSubtitle;
1306
+ subtitleText.textContent = this.decodeHTMLEntities(this.options.videoSubtitle);
1210
1307
  overlay.appendChild(subtitleText);
1211
1308
  }
1212
1309
 
@@ -1261,7 +1358,7 @@ setVideoTitle(title) {
1261
1358
  if (this.titleOverlay) {
1262
1359
  const titleElement = this.titleOverlay.querySelector('.title-text');
1263
1360
  if (titleElement) {
1264
- titleElement.textContent = this.options.videoTitle;
1361
+ titleElement.textContent = this.decodeHTMLEntities(this.options.videoTitle);
1265
1362
  }
1266
1363
 
1267
1364
  if (title) {
@@ -1743,9 +1840,10 @@ updateProgress() {
1743
1840
  this.progressHandle.style.left = progress + '%';
1744
1841
  }
1745
1842
 
1843
+ // Always call updateTimeDisplay, regardless of duration validity
1746
1844
  this.updateTimeDisplay();
1747
1845
 
1748
- // Trigger timeupdate event (with throttling to avoid too many events)
1846
+ // Trigger timeupdate event with throttling
1749
1847
  if (!this.lastTimeUpdate || Date.now() - this.lastTimeUpdate > 250) {
1750
1848
  this.triggerEvent('timeupdate', {
1751
1849
  currentTime: this.getCurrentTime(),
@@ -1824,6 +1922,8 @@ updateDuration() {
1824
1922
  if (this.durationEl && this.video && this.video.duration && !isNaN(this.video.duration)) {
1825
1923
  this.durationEl.textContent = this.formatTime(this.video.duration);
1826
1924
  }
1925
+ // Call updateTimeDisplay to handle all states (loading, encoding, normal)
1926
+ this.updateTimeDisplay();
1827
1927
  }
1828
1928
 
1829
1929
  changeSpeed(e) {
@@ -2116,7 +2216,7 @@ switchToVideo(newVideoElement, shouldPlay = false) {
2116
2216
  if (newTitle && this.options.showTitleOverlay) {
2117
2217
  this.options.videoTitle = newTitle;
2118
2218
  if (this.titleText) {
2119
- this.titleText.textContent = newTitle;
2219
+ this.titleText.textContent = this.decodeHTMLEntities(newTitle);
2120
2220
  }
2121
2221
  }
2122
2222
 
@@ -2710,6 +2810,10 @@ addEventListener(eventType, callback) {
2710
2810
  if (!this.isChangingQuality) {
2711
2811
  this.showLoading();
2712
2812
  }
2813
+
2814
+ // Update time display to show "Loading..." during initial buffering
2815
+ this.updateTimeDisplay();
2816
+
2713
2817
  // Trigger loadstart event - browser started loading media
2714
2818
  this.triggerEvent('loadstart');
2715
2819
  });
@@ -2717,6 +2821,9 @@ addEventListener(eventType, callback) {
2717
2821
  this.video.addEventListener('loadedmetadata', () => {
2718
2822
  this.updateDuration();
2719
2823
 
2824
+ // Update time display when metadata is loaded
2825
+ this.updateTimeDisplay();
2826
+
2720
2827
  // Trigger loadedmetadata event - video metadata loaded
2721
2828
  this.triggerEvent('loadedmetadata', {
2722
2829
  duration: this.getDuration(),
@@ -2734,6 +2841,10 @@ addEventListener(eventType, callback) {
2734
2841
  if (!this.isChangingQuality) {
2735
2842
  this.hideLoading();
2736
2843
  }
2844
+
2845
+ // Update time display when data is loaded
2846
+ this.updateTimeDisplay();
2847
+
2737
2848
  // Trigger loadeddata event - current frame data loaded
2738
2849
  this.triggerEvent('loadeddata', {
2739
2850
  currentTime: this.getCurrentTime()
@@ -2744,6 +2855,10 @@ addEventListener(eventType, callback) {
2744
2855
  if (!this.isChangingQuality) {
2745
2856
  this.hideLoading();
2746
2857
  }
2858
+
2859
+ // Update time display when video can play
2860
+ this.updateTimeDisplay();
2861
+
2747
2862
  // Trigger canplay event - video can start playing
2748
2863
  this.triggerEvent('canplay', {
2749
2864
  currentTime: this.getCurrentTime(),
@@ -2751,6 +2866,21 @@ addEventListener(eventType, callback) {
2751
2866
  });
2752
2867
  });
2753
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
+
2754
2884
  this.video.addEventListener('progress', () => {
2755
2885
  this.updateBuffer();
2756
2886
  // Trigger progress event - browser is downloading media
@@ -3486,30 +3616,12 @@ createControls() {
3486
3616
  <span class="icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M12.5 4v8l-7-4zm-8 0v8l7-4z"/></svg></span>
3487
3617
  </button>
3488
3618
 
3489
- ${this.options.showSubtitles ? `
3490
- <div class="subtitles-control" style="display: none;">
3491
- <button class="control-btn subtitles-btn" data-tooltip="subtitles">
3492
- <span class="icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1z"/><path d="M6.096 4.972c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152m4.915 0c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152M6.096 9.972c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152m4.915 0c.234 0 .44.05.617.152.177.1.312.235.405.403.093.169.14.36.14.577 0 .216-.047.406-.14.572a1.03 1.03 0 0 1-.405.403 1.2 1.2 0 0 1-.617.152 1.2 1.2 0 0 1-.615-.152 1.03 1.03 0 0 1-.406-.403 1.28 1.28 0 0 1-.14-.572c0-.216.046-.408.14-.577.093-.168.228-.303.406-.403.177-.101.383-.152.615-.152"/></svg></span>
3619
+ <div class="settings-control">
3620
+ <button class="control-btn settings-btn" data-tooltip="settings_menu">
3621
+ <span class="icon"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></svg></span>
3493
3622
  </button>
3494
- <div class="subtitles-menu">
3495
- <div class="subtitles-option active" data-track="off">Off</div>
3496
- </div>
3497
- </div>
3498
- ` : ''}
3499
-
3500
- ${this.options.showSpeedControl ? `
3501
- <div class="speed-control">
3502
- <button class="control-btn speed-btn" data-tooltip="playback_speed">1x</button>
3503
- <div class="speed-menu">
3504
- <div class="speed-option" data-speed="0.5">0.5x</div>
3505
- <div class="speed-option" data-speed="0.75">0.75x</div>
3506
- <div class="speed-option active" data-speed="1">1x</div>
3507
- <div class="speed-option" data-speed="1.25">1.25x</div>
3508
- <div class="speed-option" data-speed="1.5">1.5x</div>
3509
- <div class="speed-option" data-speed="2">2x</div>
3510
- </div>
3623
+ <div class="settings-menu"></div>
3511
3624
  </div>
3512
- ` : ''}
3513
3625
 
3514
3626
  ${this.options.showQualitySelector && this.originalSources && this.originalSources.length > 1 ? `
3515
3627
  <div class="quality-control">
@@ -3522,8 +3634,8 @@ createControls() {
3522
3634
  <div class="quality-menu">
3523
3635
  <div class="quality-option selected" data-quality="auto">${this.t('auto')}</div>
3524
3636
  ${this.originalSources.map(s =>
3525
- `<div class="quality-option" data-quality="${s.quality}">${s.quality}</div>`
3526
- ).join('')}
3637
+ `<div class="quality-option" data-quality="${s.quality}">${s.quality}</div>`
3638
+ ).join('')}
3527
3639
  </div>
3528
3640
  </div>
3529
3641
  ` : ''}
@@ -3535,13 +3647,6 @@ createControls() {
3535
3647
  </button>
3536
3648
  ` : ''}
3537
3649
 
3538
- <div class="settings-control">
3539
- <button class="control-btn settings-btn" data-tooltip="settings_menu">
3540
- <span class="icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52z"/></svg></span>
3541
- </button>
3542
- <div class="settings-menu"></div>
3543
- </div>
3544
-
3545
3650
  ${this.options.showFullscreen ? `
3546
3651
  <button class="control-btn fullscreen-btn" data-tooltip="fullscreen">
3547
3652
  <span class="icon fullscreen-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5M.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5m15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5"/></svg></span>
@@ -3638,44 +3743,22 @@ checkScreenSize() {
3638
3743
  }
3639
3744
  }
3640
3745
 
3641
- /* Update settings menu visibility based on screen size */
3746
+ /* Update settings menu visibility */
3642
3747
  updateSettingsMenuVisibility() {
3643
3748
  const settingsControl = this.controls?.querySelector('.settings-control');
3644
3749
  if (!settingsControl) return;
3645
3750
 
3646
- if (this.isSmallScreen) {
3647
- // Show settings menu and hide individual controls
3648
- settingsControl.style.display = 'block';
3649
-
3650
- // Hide controls that will be moved to settings menu
3651
- const pipBtn = this.controls.querySelector('.pip-btn');
3652
- const speedControl = this.controls.querySelector('.speed-control');
3653
- const subtitlesControl = this.controls.querySelector('.subtitles-control');
3654
-
3655
- if (pipBtn) pipBtn.style.display = 'none';
3656
- if (speedControl) speedControl.style.display = 'none';
3657
- if (subtitlesControl) subtitlesControl.style.display = 'none';
3658
-
3659
- this.populateSettingsMenu();
3660
- } else {
3661
- // Hide settings menu and show individual controls
3662
- settingsControl.style.display = 'none';
3751
+ // always show settings
3752
+ settingsControl.style.display = 'block';
3663
3753
 
3664
- // Show original controls
3665
- const pipBtn = this.controls.querySelector('.pip-btn');
3666
- const speedControl = this.controls.querySelector('.speed-control');
3667
- const subtitlesControl = this.controls.querySelector('.subtitles-control');
3754
+ // Populate settings menu
3755
+ this.populateSettingsMenu();
3668
3756
 
3669
- if (pipBtn && this.options.showPictureInPicture && this.isPiPSupported) {
3670
- pipBtn.style.display = 'flex';
3671
- }
3672
- if (speedControl && this.options.showSpeedControl) {
3673
- speedControl.style.display = 'block';
3674
- }
3675
- if (subtitlesControl && this.options.showSubtitles && this.textTracks.length > 0) {
3676
- subtitlesControl.style.display = 'block';
3677
- }
3678
- }
3757
+ // hide speed and subtitles controls
3758
+ const speedControl = this.controls.querySelector('.speed-control');
3759
+ const subtitlesControl = this.controls.querySelector('.subtitles-control');
3760
+ if (speedControl) speedControl.style.display = 'none';
3761
+ if (subtitlesControl) subtitlesControl.style.display = 'none';
3679
3762
  }
3680
3763
 
3681
3764
  /**
@@ -3687,54 +3770,40 @@ populateSettingsMenu() {
3687
3770
 
3688
3771
  let menuHTML = '';
3689
3772
 
3690
- // Picture-in-Picture option
3691
- if (this.options.showPictureInPicture && this.isPiPSupported) {
3692
- const pipLabel = this.t('picture_in_picture') || 'Picture-in-Picture';
3693
- menuHTML += `<div class="settings-option" data-action="pip">
3694
- <span class="settings-option-label">${pipLabel}</span>
3695
- </div>`;
3696
- }
3697
-
3698
- // Speed Control - expandable
3773
+ // SPEED - always included
3699
3774
  if (this.options.showSpeedControl) {
3700
3775
  const speedLabel = this.t('playback_speed') || 'Playback Speed';
3701
3776
  const currentSpeed = this.video ? this.video.playbackRate : 1;
3702
-
3703
- menuHTML += `
3704
- <div class="settings-expandable-wrapper">
3705
- <div class="settings-option expandable-trigger" data-action="speed-expand">
3706
- <span class="settings-option-label">${speedLabel}: ${currentSpeed}x</span>
3707
- <span class="expand-arrow">▼</span>
3708
- </div>
3709
- <div class="settings-expandable-content" style="display: none;">`;
3777
+ menuHTML += `<div class="settings-expandable-wrapper">
3778
+ <div class="settings-option expandable-trigger" data-action="speed-expand">
3779
+ <span class="settings-option-label">${speedLabel} <strong>${currentSpeed}x</strong></span>
3780
+ <span class="expand-arrow">▶</span>
3781
+ </div>
3782
+ <div class="settings-expandable-content" style="display: none;">`;
3710
3783
 
3711
3784
  const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
3712
3785
  speeds.forEach(speed => {
3713
3786
  const isActive = Math.abs(speed - currentSpeed) < 0.01;
3714
3787
  menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-speed="${speed}">${speed}x</div>`;
3715
3788
  });
3716
-
3717
3789
  menuHTML += `</div></div>`;
3718
3790
  }
3719
3791
 
3720
- // Subtitles - expandable
3792
+ // SUBTITLES - always included
3721
3793
  if (this.options.showSubtitles && this.textTracks && this.textTracks.length > 0) {
3722
3794
  const subtitlesLabel = this.t('subtitles') || 'Subtitles';
3723
3795
  const currentTrack = this.currentSubtitleTrack;
3724
- const currentLabel = this.subtitlesEnabled && currentTrack ? currentTrack.label : (this.t('subtitlesoff') || 'Off');
3796
+ const currentLabel = this.subtitlesEnabled ? (currentTrack ? currentTrack.label : 'Unknown') : (this.t('subtitlesoff') || 'Off');
3725
3797
 
3726
- menuHTML += `
3727
- <div class="settings-expandable-wrapper">
3728
- <div class="settings-option expandable-trigger" data-action="subtitles-expand">
3729
- <span class="settings-option-label">${subtitlesLabel}: ${currentLabel}</span>
3730
- <span class="expand-arrow">▼</span>
3731
- </div>
3732
- <div class="settings-expandable-content" style="display: none;">`;
3798
+ menuHTML += `<div class="settings-expandable-wrapper">
3799
+ <div class="settings-option expandable-trigger" data-action="subtitles-expand">
3800
+ <span class="settings-option-label">${subtitlesLabel} <strong>${currentLabel}</strong></span>
3801
+ <span class="expand-arrow">▶</span>
3802
+ </div>
3803
+ <div class="settings-expandable-content" style="display: none;">`;
3733
3804
 
3734
- // Off option
3735
3805
  menuHTML += `<div class="settings-suboption ${!this.subtitlesEnabled ? 'active' : ''}" data-track="off">${this.t('subtitlesoff') || 'Off'}</div>`;
3736
3806
 
3737
- // Subtitle tracks
3738
3807
  this.textTracks.forEach((trackData, index) => {
3739
3808
  const isActive = this.currentSubtitleTrack === trackData.track;
3740
3809
  menuHTML += `<div class="settings-suboption ${isActive ? 'active' : ''}" data-track="${index}">${trackData.label}</div>`;
@@ -3744,9 +3813,6 @@ populateSettingsMenu() {
3744
3813
  }
3745
3814
 
3746
3815
  settingsMenu.innerHTML = menuHTML;
3747
-
3748
- // Add scrollbar if needed
3749
- this.addSettingsMenuScrollbar();
3750
3816
  }
3751
3817
 
3752
3818
  /**
@@ -3756,12 +3822,27 @@ addSettingsMenuScrollbar() {
3756
3822
  const settingsMenu = this.controls?.querySelector('.settings-menu');
3757
3823
  if (!settingsMenu) return;
3758
3824
 
3759
- const playerHeight = this.container.offsetHeight;
3760
- const maxMenuHeight = playerHeight - 100;
3825
+ const settingsBtn = document.querySelector('.settings-btn');
3826
+ if (!settingsBtn) return;
3761
3827
 
3762
- settingsMenu.style.maxHeight = `${maxMenuHeight}px`;
3763
- settingsMenu.style.overflowY = 'auto';
3764
- settingsMenu.style.overflowX = 'hidden';
3828
+ // helper to update menu height
3829
+ const updateMenuHeight = () => {
3830
+ if (settingsMenu.classList.contains('active')) {
3831
+ const containerRect = settingsMenu.parentElement.parentElement.getBoundingClientRect();
3832
+ const btnRect = settingsBtn.getBoundingClientRect();
3833
+ const spaceBelow = containerRect.bottom - btnRect.bottom;
3834
+ const maxMenuHeight = Math.max(100, Math.min(250, spaceBelow - 20));
3835
+ settingsMenu.style.maxHeight = `${maxMenuHeight}px`;
3836
+ settingsMenu.style.overflowY = 'auto';
3837
+ settingsMenu.style.overflowX = 'hidden';
3838
+ }
3839
+ };
3840
+
3841
+ // run initially
3842
+ updateMenuHeight();
3843
+
3844
+ // recalculate on window resize
3845
+ window.addEventListener('resize', updateMenuHeight);
3765
3846
 
3766
3847
  // Add scrollbar styling
3767
3848
  if (!document.getElementById('player-settings-scrollbar-style')) {
@@ -3794,9 +3875,43 @@ addSettingsMenuScrollbar() {
3794
3875
  * Bind settings menu events
3795
3876
  */
3796
3877
  bindSettingsMenuEvents() {
3878
+ const settingsBtn = this.controls?.querySelector('.settings-btn');
3797
3879
  const settingsMenu = this.controls?.querySelector('.settings-menu');
3798
- if (!settingsMenu) return;
3880
+ if (!settingsMenu || !settingsBtn) return;
3799
3881
 
3882
+ // toggle menu on button click
3883
+ settingsBtn.addEventListener('click', (e) => {
3884
+ e.stopPropagation();
3885
+ settingsMenu.classList.toggle('active');
3886
+
3887
+ // when menu is opened, set max height and overflow
3888
+ if (settingsMenu.classList.contains('active')) {
3889
+ const settingsBtn = document.querySelector('.settings-btn');
3890
+ const containerRect = settingsMenu.parentElement.parentElement.getBoundingClientRect();
3891
+ const btnRect = settingsBtn.getBoundingClientRect();
3892
+ const spaceBelow = containerRect.bottom - btnRect.bottom;
3893
+ const maxMenuHeight = Math.max(100, Math.min(250, spaceBelow - 20));
3894
+
3895
+ settingsMenu.style.maxHeight = `${maxMenuHeight}px`;
3896
+ settingsMenu.style.overflowY = 'auto';
3897
+ settingsMenu.style.overflowX = 'hidden';
3898
+ } else {
3899
+ settingsMenu.style.maxHeight = 'none';
3900
+ settingsMenu.style.overflowY = 'visible';
3901
+ }
3902
+
3903
+ });
3904
+
3905
+ // close menu when clicking outside
3906
+ document.addEventListener('click', (e) => {
3907
+ if (!settingsBtn?.contains(e.target) && !settingsMenu?.contains(e.target)) {
3908
+ settingsMenu?.classList.remove('active');
3909
+ settingsMenu.style.maxHeight = 'none';
3910
+ settingsMenu.style.overflowY = 'visible';
3911
+ }
3912
+ });
3913
+
3914
+ // manage clicks inside the menu
3800
3915
  settingsMenu.addEventListener('click', (e) => {
3801
3916
  e.stopPropagation();
3802
3917
 
@@ -7731,63 +7846,79 @@ removePluginControlButton(buttonId) {
7731
7846
  }
7732
7847
 
7733
7848
  getBufferedTime() {
7734
- if (!this.video || !this.video.buffered || this.video.buffered.length === 0) return 0;
7735
- try {
7736
- return this.video.buffered.end(this.video.buffered.length - 1);
7737
- } catch (error) {
7738
- return 0;
7739
- }
7849
+ if (!this.video || !this.video.buffered || this.video.buffered.length === 0) return 0;
7850
+ try {
7851
+ return this.video.buffered.end(this.video.buffered.length - 1);
7852
+ } catch (error) {
7853
+ return 0;
7740
7854
  }
7855
+ }
7741
7856
 
7742
- clearTitleTimeout() {
7743
- if (this.titleTimeout) {
7744
- clearTimeout(this.titleTimeout);
7745
- this.titleTimeout = null;
7746
- }
7857
+ clearTitleTimeout() {
7858
+ if (this.titleTimeout) {
7859
+ clearTimeout(this.titleTimeout);
7860
+ this.titleTimeout = null;
7747
7861
  }
7862
+ }
7748
7863
 
7749
- skipTime(seconds) {
7750
- if (!this.video || !this.video.duration || this.isChangingQuality) return;
7864
+ skipTime(seconds) {
7865
+ if (!this.video || !this.video.duration || this.isChangingQuality) return;
7751
7866
 
7752
- this.video.currentTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
7753
- }
7867
+ this.video.currentTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
7868
+ }
7754
7869
 
7755
7870
  updateTimeDisplay() {
7756
- // update current time
7871
+ // Update current time
7757
7872
  if (this.currentTimeEl && this.video) {
7758
7873
  this.currentTimeEl.textContent = this.formatTime(this.video.currentTime || 0);
7759
7874
  }
7760
7875
 
7761
- // update duration or show badge if encoding
7876
+ // Update duration or show appropriate message
7762
7877
  if (this.durationEl && this.video) {
7763
7878
  const duration = this.video.duration;
7764
-
7765
- // check if duration is valid
7766
- if (!duration || isNaN(duration) || !isFinite(duration)) {
7767
- // Video in encoding - show badge instead of duration
7768
- this.durationEl.innerHTML = '<span class="encoding-badge">Encoding in progress</span>';
7879
+ const readyState = this.video.readyState;
7880
+ const currentTime = this.video.currentTime;
7881
+ const networkState = this.video.networkState;
7882
+
7883
+ // Check for initial buffering state
7884
+ // readyState < 2 means not enough data to play (HAVE_NOTHING or HAVE_METADATA)
7885
+ // currentTime === 0 and duration === 0 indicates initial loading
7886
+ const isInitialBuffering = (readyState < 2 && currentTime === 0) ||
7887
+ (currentTime === 0 && (!duration || duration === 0) && networkState === 2);
7888
+
7889
+ // Check if duration is invalid (NaN or Infinity)
7890
+ const isDurationInvalid = !duration || isNaN(duration) || !isFinite(duration);
7891
+
7892
+ if (isInitialBuffering) {
7893
+ // Initial buffering - show loading message
7894
+ this.durationEl.textContent = t('loading');
7895
+ this.durationEl.classList.remove('encoding-state');
7896
+ this.durationEl.classList.add('loading-state');
7897
+ } else if (isDurationInvalid) {
7898
+ // Video is encoding (FFmpeg still processing) - show encoding badge
7899
+ this.durationEl.textContent = t('encoding_in_progress');
7900
+ this.durationEl.classList.remove('loading-state');
7769
7901
  this.durationEl.classList.add('encoding-state');
7770
7902
  } else {
7771
- // valid duration - show normal
7903
+ // Valid duration - show normal time
7772
7904
  this.durationEl.textContent = this.formatTime(duration);
7773
- this.durationEl.classList.remove('encoding-state');
7905
+ this.durationEl.classList.remove('encoding-state', 'loading-state');
7774
7906
  }
7775
7907
  }
7776
7908
  }
7777
7909
 
7910
+ formatTime(seconds) {
7911
+ if (isNaN(seconds) || seconds < 0) return '0:00';
7778
7912
 
7779
- formatTime(seconds) {
7780
- if (isNaN(seconds) || seconds < 0) return '0:00';
7913
+ const hours = Math.floor(seconds / 3600);
7914
+ const minutes = Math.floor((seconds % 3600) / 60);
7915
+ const secs = Math.floor(seconds % 60);
7781
7916
 
7782
- const hours = Math.floor(seconds / 3600);
7783
- const minutes = Math.floor((seconds % 3600) / 60);
7784
- const secs = Math.floor(seconds % 60);
7785
-
7786
- if (hours > 0) {
7787
- return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
7788
- }
7789
- return `${minutes}:${secs.toString().padStart(2, '0')}`;
7917
+ if (hours > 0) {
7918
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
7790
7919
  }
7920
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
7921
+ }
7791
7922
 
7792
7923
  }
7793
7924