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/quality.js ADDED
@@ -0,0 +1,921 @@
1
+ // Quality Module for MYETV Video Player
2
+ // Conservative modularization - original code preserved exactly
3
+ // Created by https://www.myetv.tv https://oskarcosimo.com
4
+
5
+ initializeQualityMonitoring() {
6
+ this.qualityMonitorInterval = setInterval(() => {
7
+ if (!this.isChangingQuality) {
8
+ this.updateCurrentPlayingQuality();
9
+ }
10
+ }, 3000);
11
+
12
+ if (this.video) {
13
+ this.video.addEventListener('loadedmetadata', () => {
14
+ setTimeout(() => {
15
+ if (!this.isChangingQuality) {
16
+ this.updateCurrentPlayingQuality();
17
+ }
18
+ }, 100);
19
+ });
20
+
21
+ this.video.addEventListener('resize', () => {
22
+ if (!this.isChangingQuality) {
23
+ this.updateCurrentPlayingQuality();
24
+ }
25
+ });
26
+
27
+ this.video.addEventListener('loadeddata', () => {
28
+ setTimeout(() => {
29
+ if (!this.isChangingQuality) {
30
+ this.updateCurrentPlayingQuality();
31
+ }
32
+ }, 1000);
33
+ });
34
+ }
35
+ }
36
+
37
+ getCurrentPlayingQuality() {
38
+ if (!this.video) return null;
39
+
40
+ if (this.video.currentSrc && this.qualities && this.qualities.length > 0) {
41
+ const currentSource = this.qualities.find(q => {
42
+ const currentUrl = this.video.currentSrc.toLowerCase();
43
+ const qualityUrl = q.src.toLowerCase();
44
+
45
+ if (this.debugQuality) {
46
+ if (this.options.debug) console.log('Quality comparison:', {
47
+ current: currentUrl,
48
+ quality: qualityUrl,
49
+ qualityName: q.quality,
50
+ match: currentUrl === qualityUrl || currentUrl.includes(qualityUrl) || qualityUrl.includes(currentUrl)
51
+ });
52
+ }
53
+
54
+ return currentUrl === qualityUrl ||
55
+ currentUrl.includes(qualityUrl) ||
56
+ qualityUrl.includes(currentUrl);
57
+ });
58
+
59
+ if (currentSource) {
60
+ if (this.debugQuality) {
61
+ if (this.options.debug) console.log('Quality found from source:', currentSource.quality);
62
+ }
63
+ return currentSource.quality;
64
+ }
65
+ }
66
+
67
+ if (this.video.videoHeight && this.video.videoWidth) {
68
+ const height = this.video.videoHeight;
69
+ const width = this.video.videoWidth;
70
+
71
+ if (this.debugQuality) {
72
+ if (this.options.debug) console.log('Risoluzione video:', { height, width });
73
+ }
74
+
75
+ if (height >= 2160) return '4K';
76
+ if (height >= 1440) return '1440p';
77
+ if (height >= 1080) return '1080p';
78
+ if (height >= 720) return '720p';
79
+ if (height >= 480) return '480p';
80
+ if (height >= 360) return '360p';
81
+ if (height >= 240) return '240p';
82
+
83
+ return `${height}p`;
84
+ }
85
+
86
+ if (this.debugQuality) {
87
+ if (this.options.debug) console.log('No quality detected:', {
88
+ currentSrc: this.video.currentSrc,
89
+ videoHeight: this.video.videoHeight,
90
+ videoWidth: this.video.videoWidth,
91
+ qualities: this.qualities
92
+ });
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ updateCurrentPlayingQuality() {
99
+ const newPlayingQuality = this.getCurrentPlayingQuality();
100
+
101
+ if (newPlayingQuality && newPlayingQuality !== this.currentPlayingQuality) {
102
+ if (this.options.debug) console.log(`Quality changed: ${this.currentPlayingQuality} → ${newPlayingQuality}`);
103
+ this.currentPlayingQuality = newPlayingQuality;
104
+ this.updateQualityDisplay();
105
+ }
106
+ }
107
+
108
+ updateQualityDisplay() {
109
+ this.updateQualityButton();
110
+ this.updateQualityMenu();
111
+ }
112
+
113
+ updateQualityButton() {
114
+ const qualityBtn = this.controls?.querySelector('.quality-btn');
115
+ if (!qualityBtn) return;
116
+
117
+ let btnText = qualityBtn.querySelector('.quality-btn-text');
118
+ if (!btnText) {
119
+ qualityBtn.innerHTML = `
120
+ <span class="icon">⚙</span>
121
+ <div class="quality-btn-text">
122
+ <div class="selected-quality">${this.selectedQuality === 'auto' ? this.t('auto') : this.selectedQuality}</div>
123
+ <div class="current-quality">${this.currentPlayingQuality || ''}</div>
124
+ </div>
125
+ `;
126
+ } else {
127
+ const selectedEl = btnText.querySelector('.selected-quality');
128
+ const currentEl = btnText.querySelector('.current-quality');
129
+
130
+ if (selectedEl) {
131
+ selectedEl.textContent = this.selectedQuality === 'auto' ? this.t('auto') : this.selectedQuality;
132
+ }
133
+
134
+ if (currentEl) {
135
+ currentEl.textContent = this.currentPlayingQuality || '';
136
+ currentEl.style.display = this.currentPlayingQuality ? 'block' : 'none';
137
+ }
138
+ }
139
+ }
140
+
141
+ updateQualityMenu() {
142
+ const qualityMenu = this.controls?.querySelector('.quality-menu');
143
+ if (!qualityMenu) return;
144
+
145
+ let menuHTML = '';
146
+
147
+ // Check if adaptive streaming is active (HLS/DASH)
148
+ if (this.isAdaptiveStream && this.adaptiveQualities && this.adaptiveQualities.length > 0) {
149
+ // Show adaptive streaming qualities
150
+ const currentIndex = this.getCurrentAdaptiveQuality();
151
+ const autoSelected = currentIndex === -1 || currentIndex === null || this.selectedQuality === 'auto';
152
+ const autoClass = autoSelected ? 'selected' : '';
153
+
154
+ menuHTML += `<div class="quality-option ${autoClass}" data-adaptive-quality="auto">${this.t('auto')}</div>`;
155
+
156
+ this.adaptiveQualities.forEach(quality => {
157
+ const isSelected = currentIndex === quality.index && !autoSelected;
158
+ const className = isSelected ? 'selected' : '';
159
+ const label = quality.label || `${quality.height}p` || 'Unknown';
160
+ menuHTML += `<div class="quality-option ${className}" data-adaptive-quality="${quality.index}">${label}</div>`;
161
+ });
162
+ } else {
163
+ // Show standard qualities for regular videos
164
+ const autoSelected = this.selectedQuality === 'auto';
165
+ const autoPlaying = this.selectedQuality === 'auto' && this.currentPlayingQuality;
166
+ let autoClass = '';
167
+ if (autoSelected && autoPlaying) {
168
+ autoClass = 'selected playing';
169
+ } else if (autoSelected) {
170
+ autoClass = 'selected';
171
+ }
172
+
173
+ menuHTML += `<div class="quality-option ${autoClass}" data-quality="auto">${this.t('auto')}</div>`;
174
+
175
+ this.qualities.forEach(quality => {
176
+ const isSelected = this.selectedQuality === quality.quality;
177
+ const isPlaying = this.currentPlayingQuality === quality.quality;
178
+ let className = 'quality-option';
179
+ if (isSelected && isPlaying) {
180
+ className += ' selected playing';
181
+ } else if (isSelected) {
182
+ className += ' selected';
183
+ } else if (isPlaying) {
184
+ className += ' playing';
185
+ }
186
+ menuHTML += `<div class="${className}" data-quality="${quality.quality}">${quality.quality}</div>`;
187
+ });
188
+ }
189
+
190
+ qualityMenu.innerHTML = menuHTML;
191
+ }
192
+
193
+ getQualityStatus() {
194
+ return {
195
+ selected: this.selectedQuality,
196
+ playing: this.currentPlayingQuality,
197
+ isAuto: this.selectedQuality === 'auto',
198
+ isChanging: this.isChangingQuality
199
+ };
200
+ }
201
+
202
+ getSelectedQuality() {
203
+ return this.selectedQuality;
204
+ }
205
+
206
+ isAutoQualityActive() {
207
+ return this.selectedQuality === 'auto';
208
+ }
209
+
210
+ enableQualityDebug() {
211
+ this.debugQuality = true;
212
+ this.enableAutoHideDebug(); // Abilita anche debug auto-hide
213
+ if (this.options.debug) console.log('Quality AND auto-hide debug enabled');
214
+ this.updateCurrentPlayingQuality();
215
+ }
216
+
217
+ disableQualityDebug() {
218
+ this.debugQuality = false;
219
+ this.disableAutoHideDebug();
220
+ if (this.options.debug) console.log('Quality AND auto-hide debug disabled');
221
+ }
222
+
223
+ changeQuality(e) {
224
+ if (!e.target.classList.contains('quality-option')) return;
225
+ if (this.isChangingQuality) return;
226
+
227
+ // Handle adaptive streaming quality change
228
+ const adaptiveQuality = e.target.getAttribute('data-adaptive-quality');
229
+ if (adaptiveQuality !== null && this.isAdaptiveStream) {
230
+ const qualityIndex = adaptiveQuality === 'auto' ? -1 : parseInt(adaptiveQuality);
231
+ this.setAdaptiveQuality(qualityIndex);
232
+ this.updateAdaptiveQualityMenu();
233
+ return;
234
+ }
235
+
236
+ const quality = e.target.getAttribute('data-quality');
237
+ if (!quality || quality === this.selectedQuality) return;
238
+
239
+ if (this.options.debug) console.log(`Quality change requested: ${this.selectedQuality} → ${quality}`);
240
+
241
+ this.selectedQuality = quality;
242
+
243
+ if (quality === 'auto') {
244
+ this.enableAutoQuality();
245
+ } else {
246
+ this.setQuality(quality);
247
+ }
248
+
249
+ this.updateQualityDisplay();
250
+ }
251
+
252
+ setQuality(targetQuality) {
253
+ if (this.options.debug) console.log(`setQuality("${targetQuality}") called`);
254
+
255
+ if (!targetQuality) {
256
+ if (this.options.debug) console.error('targetQuality is empty!');
257
+ return;
258
+ }
259
+
260
+ if (!this.video || !this.qualities || this.qualities.length === 0) return;
261
+ if (this.isChangingQuality) return;
262
+
263
+ const newSource = this.qualities.find(q => q.quality === targetQuality);
264
+ if (!newSource || !newSource.src) {
265
+ if (this.options.debug) console.error(`Quality "${targetQuality}" not found`);
266
+ return;
267
+ }
268
+
269
+ const currentTime = this.video.currentTime || 0;
270
+ const wasPlaying = !this.video.paused;
271
+
272
+ this.isChangingQuality = true;
273
+ this.selectedQuality = targetQuality;
274
+ this.video.pause();
275
+
276
+ // Show loading state during quality change
277
+ this.showLoading();
278
+ if (this.video.classList) {
279
+ this.video.classList.add('quality-changing');
280
+ }
281
+
282
+ const onLoadedData = () => {
283
+ if (this.options.debug) console.log(`Quality ${targetQuality} applied!`);
284
+ this.video.currentTime = currentTime;
285
+
286
+ if (wasPlaying) {
287
+ this.video.play().catch(e => {
288
+ if (this.options.debug) console.log('Play error:', e);
289
+ });
290
+ }
291
+
292
+ this.currentPlayingQuality = targetQuality;
293
+ this.updateQualityDisplay();
294
+ this.isChangingQuality = false;
295
+
296
+ // Restore resolution settings after quality change
297
+ this.restoreResolutionAfterQualityChange();
298
+ cleanup();
299
+ };
300
+
301
+ const onError = (error) => {
302
+ if (this.options.debug) console.error(`Loading error ${targetQuality}:`, error);
303
+ this.isChangingQuality = false;
304
+ cleanup();
305
+ };
306
+
307
+ const cleanup = () => {
308
+ this.video.removeEventListener('loadeddata', onLoadedData);
309
+ this.video.removeEventListener('error', onError);
310
+ };
311
+
312
+ this.video.addEventListener('loadeddata', onLoadedData, { once: true });
313
+ this.video.addEventListener('error', onError, { once: true });
314
+
315
+ this.video.src = newSource.src;
316
+ this.video.load();
317
+ }
318
+
319
+ finishQualityChange(success, wasPlaying, currentTime, currentVolume, wasMuted, targetQuality) {
320
+ if (this.options.debug) console.log(`Quality change completion: success=${success}, target=${targetQuality}`);
321
+
322
+ if (this.qualityChangeTimeout) {
323
+ clearTimeout(this.qualityChangeTimeout);
324
+ this.qualityChangeTimeout = null;
325
+ }
326
+
327
+ if (this.video) {
328
+ try {
329
+ if (success && currentTime > 0 && this.video.duration) {
330
+ this.video.currentTime = Math.min(currentTime, this.video.duration);
331
+ }
332
+
333
+ this.video.volume = currentVolume;
334
+ this.video.muted = wasMuted;
335
+
336
+ if (success && wasPlaying) {
337
+ this.video.play().catch(err => {
338
+ if (this.options.debug) console.warn('Play after quality change failed:', err);
339
+ });
340
+ }
341
+ } catch (error) {
342
+ if (this.options.debug) console.error('Errore ripristino stato:', error);
343
+ }
344
+
345
+ if (this.video.classList) {
346
+ this.video.classList.remove('quality-changing');
347
+ }
348
+ }
349
+
350
+ this.hideLoading();
351
+ this.isChangingQuality = false;
352
+
353
+ if (success) {
354
+ if (this.options.debug) console.log('Quality change completed successfully');
355
+ setTimeout(() => {
356
+ this.currentPlayingQuality = targetQuality;
357
+ this.updateQualityDisplay();
358
+ if (this.options.debug) console.log(`🎯 Quality confirmed active: ${targetQuality}`);
359
+ }, 100);
360
+ } else {
361
+ if (this.options.debug) console.warn('Quality change failed or timeout');
362
+ }
363
+
364
+ setTimeout(() => {
365
+ this.updateCurrentPlayingQuality();
366
+ }, 2000);
367
+ }
368
+
369
+ cleanupQualityChange() {
370
+ if (this.qualityChangeTimeout) {
371
+ clearTimeout(this.qualityChangeTimeout);
372
+ this.qualityChangeTimeout = null;
373
+ }
374
+ }
375
+
376
+ enableAutoQuality() {
377
+ if (this.options.debug) console.log('🔄 enableAutoQuality - keeping selectedQuality as "auto"');
378
+
379
+ // IMPORTANT: Keep selectedQuality as 'auto' for proper UI display
380
+ this.selectedQuality = 'auto';
381
+
382
+ if (!this.qualities || this.qualities.length === 0) {
383
+ if (this.options.debug) console.warn('⚠️ No qualities available for auto selection');
384
+ this.updateQualityDisplay();
385
+ return;
386
+ }
387
+
388
+ // Smart connection-based quality selection
389
+ let autoSelectedQuality = this.getAutoQualityBasedOnConnection();
390
+
391
+ if (this.options.debug) {
392
+ console.log('🎯 Auto quality selected:', autoSelectedQuality);
393
+ console.log('📊 selectedQuality remains: "auto" (for UI)');
394
+ }
395
+
396
+ // Apply the auto-selected quality but keep UI showing "auto"
397
+ this.applyAutoQuality(autoSelectedQuality);
398
+ }
399
+
400
+ // ENHANCED CONNECTION DETECTION - Uses RTT + downlink heuristics
401
+ // Handles both Ethernet and real mobile 4G intelligently
402
+
403
+ getAutoQualityBasedOnConnection() {
404
+ // Get available qualities
405
+ const maxQualityIndex = this.qualities.length - 1;
406
+ const maxQuality = this.qualities[maxQualityIndex];
407
+ let selectedQuality = maxQuality.quality;
408
+
409
+ // =====================================================
410
+ // MOBILE DETECTION
411
+ // =====================================================
412
+ const isDefinitelyMobile = () => {
413
+ const ua = navigator.userAgent.toLowerCase();
414
+ const checks = [
415
+ ua.includes('android'),
416
+ ua.includes('mobile'),
417
+ ua.includes('iphone'),
418
+ ua.includes('ipad'),
419
+ window.innerWidth < 1024,
420
+ window.innerHeight < 768,
421
+ 'ontouchstart' in window,
422
+ navigator.maxTouchPoints > 0,
423
+ 'orientation' in window,
424
+ window.devicePixelRatio > 1.5
425
+ ];
426
+
427
+ // Count positive checks - mobile if 4+ indicators (more aggressive)
428
+ const mobileScore = checks.filter(Boolean).length;
429
+
430
+ if (this.options.debug) {
431
+ console.log('🔍 Mobile Detection Score:', {
432
+ score: mobileScore + '/10',
433
+ android: ua.includes('android'),
434
+ mobile: ua.includes('mobile'),
435
+ width: window.innerWidth,
436
+ touch: 'ontouchstart' in window,
437
+ maxTouch: navigator.maxTouchPoints
438
+ });
439
+ }
440
+
441
+ return mobileScore >= 4; // Threshold: 4 out of 10 checks
442
+ };
443
+
444
+ // FORCE MOBILE BEHAVIOR FIRST - Override everything else
445
+ if (isDefinitelyMobile()) {
446
+ // Helper function for mobile
447
+ const findMobileQuality = (maxHeight) => {
448
+ const mobileQualities = this.qualities
449
+ .filter(q => q.height && q.height <= maxHeight)
450
+ .sort((a, b) => b.height - a.height);
451
+ return mobileQualities[0] || maxQuality;
452
+ };
453
+
454
+ // Conservative quality for mobile devices - MAX 1080p
455
+ const mobileQuality = findMobileQuality(1080);
456
+
457
+ if (this.options.debug) console.log('🚨 MOBILE FORCE OVERRIDE: ' + mobileQuality.quality + ' (max 1080p)');
458
+ return mobileQuality.quality;
459
+ }
460
+
461
+ // =====================================================
462
+ // DESKTOP CONNECTION ANALYSIS
463
+ // =====================================================
464
+ const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
465
+
466
+ if (connection) {
467
+ const physicalType = connection.type; // Usually undefined
468
+ const downlinkSpeed = connection.downlink || 0;
469
+ const rtt = connection.rtt; // Round Trip Time in milliseconds
470
+
471
+ if (this.options.debug) {
472
+ console.log('🌐 Enhanced Connection Detection:', {
473
+ physicalType: physicalType || 'undefined',
474
+ downlink: downlinkSpeed + ' Mbps',
475
+ rtt: rtt + ' ms',
476
+ userAgent: navigator.userAgent.includes('Mobile') ? 'Mobile' : 'Desktop'
477
+ });
478
+ }
479
+
480
+ // Helper function to detect mobile device via User-Agent (backup)
481
+ const isMobileDevice = () => {
482
+ return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
483
+ };
484
+
485
+ // Helper function to find quality by minimum height
486
+ const findQualityByMinHeight = (minHeight) => {
487
+ // Sort qualities by height (descending) and find first match >= minHeight
488
+ const sortedQualities = this.qualities
489
+ .filter(q => q.height && q.height >= minHeight)
490
+ .sort((a, b) => b.height - a.height);
491
+
492
+ return sortedQualities[0] || maxQuality;
493
+ };
494
+
495
+ // Helper function to find highest available quality
496
+ const findHighestQuality = () => {
497
+ const sortedQualities = this.qualities
498
+ .filter(q => q.height)
499
+ .sort((a, b) => b.height - a.height);
500
+
501
+ return sortedQualities[0] || maxQuality;
502
+ };
503
+
504
+ // PRIORITY 1: Physical type detection (when available - rare)
505
+ if (physicalType === 'ethernet') {
506
+ const quality = findHighestQuality(); // Maximum available quality
507
+ if (this.options.debug) console.log('🔥 Ethernet Detected: ' + quality.quality);
508
+ return quality.quality;
509
+ }
510
+
511
+ if (physicalType === 'wifi') {
512
+ const quality = findQualityByMinHeight(1440) || findHighestQuality(); // 2K preferred
513
+ if (this.options.debug) console.log('📶 WiFi Detected: ' + quality.quality);
514
+ return quality.quality;
515
+ }
516
+
517
+ if (physicalType === 'cellular') {
518
+ // Conservative approach for confirmed mobile connection
519
+ if (downlinkSpeed >= 20 && rtt < 40) {
520
+ const quality = findQualityByMinHeight(1080); // Max 1080p for excellent mobile
521
+ if (this.options.debug) console.log('📱 Excellent Cellular: ' + quality.quality);
522
+ return quality.quality;
523
+ } else if (downlinkSpeed >= 10) {
524
+ const quality = findQualityByMinHeight(720); // 720p for good mobile
525
+ if (this.options.debug) console.log('📱 Good Cellular: ' + quality.quality);
526
+ return quality.quality;
527
+ } else {
528
+ const quality = findQualityByMinHeight(480); // 480p for standard mobile
529
+ if (this.options.debug) console.log('📱 Standard Cellular: ' + quality.quality);
530
+ return quality.quality;
531
+ }
532
+ }
533
+
534
+ // PRIORITY 2: RTT + Downlink + User-Agent heuristics (most common case)
535
+ if (this.options.debug) {
536
+ console.log('🌐 Physical type undefined - using enhanced RTT + UA heuristics');
537
+ }
538
+
539
+ // SPECIAL CASE: RTT = 0 (Ultra-fast connection with mobile detection)
540
+ if (rtt === 0) {
541
+ if (isMobileDevice()) {
542
+ // Mobile device with RTT=0 = excellent 4G/5G, but be conservative for data usage
543
+ const quality = findQualityByMinHeight(1080); // Max 1080p for mobile
544
+ if (this.options.debug) console.log('📱 Mobile Device (UA) with RTT=0: ' + quality.quality);
545
+ return quality.quality;
546
+ } else {
547
+ // Desktop with RTT=0 = true ultra-fast fixed connection (Ethernet/Fiber)
548
+ const quality = findHighestQuality();
549
+ if (this.options.debug) console.log('🚀 Desktop Ultra-Fast (RTT=0): ' + quality.quality);
550
+ return quality.quality;
551
+ }
552
+ }
553
+
554
+ // Very low RTT + high speed with mobile detection
555
+ if (rtt < 20 && downlinkSpeed >= 10) {
556
+ if (isMobileDevice()) {
557
+ if (rtt < 10 && downlinkSpeed >= 15) {
558
+ // Excellent 5G with very low RTT - allow higher quality but still conservative
559
+ const quality = findQualityByMinHeight(1080); // Max 1080p for excellent mobile
560
+ if (this.options.debug) console.log('📱 Mobile 5G Ultra-Fast (RTT<10): ' + quality.quality);
561
+ return quality.quality;
562
+ } else {
563
+ // Good mobile connection but conservative
564
+ const quality = findQualityByMinHeight(720); // 720p for mobile with good RTT
565
+ if (this.options.debug) console.log('📱 Mobile Good Connection (RTT<20): ' + quality.quality);
566
+ return quality.quality;
567
+ }
568
+ } else {
569
+ // Desktop with low RTT = fast fixed connection
570
+ const quality = findQualityByMinHeight(1440) || findHighestQuality(); // 2K or best available
571
+ if (this.options.debug) console.log('🔥 Desktop High-Speed Fixed (RTT<20): ' + quality.quality);
572
+ return quality.quality;
573
+ }
574
+ }
575
+
576
+ // Low-medium RTT with speed analysis
577
+ if (rtt < 40 && downlinkSpeed >= 8) {
578
+ if (isMobileDevice()) {
579
+ // Mobile with decent connection
580
+ const quality = findQualityByMinHeight(720); // 720p for mobile
581
+ if (this.options.debug) console.log('📱 Mobile Decent Connection (RTT<40): ' + quality.quality);
582
+ return quality.quality;
583
+ } else {
584
+ // Desktop with medium RTT = good fixed connection (WiFi/ADSL)
585
+ const quality = findQualityByMinHeight(1080); // 1080p for desktop
586
+ if (this.options.debug) console.log('⚡ Desktop Good Connection (RTT<40): ' + quality.quality);
587
+ return quality.quality;
588
+ }
589
+ }
590
+
591
+ // Higher RTT = likely mobile or congested connection
592
+ if (rtt >= 40) {
593
+ if (downlinkSpeed >= 15 && !isMobileDevice()) {
594
+ // High speed but high RTT on desktop = congested but fast connection
595
+ const quality = findQualityByMinHeight(1080); // 1080p
596
+ if (this.options.debug) console.log('🌐 Desktop Congested Fast Connection: ' + quality.quality);
597
+ return quality.quality;
598
+ } else if (downlinkSpeed >= 10) {
599
+ // High RTT with good speed = mobile or congested WiFi
600
+ const quality = findQualityByMinHeight(720); // 720p
601
+ if (this.options.debug) console.log('📱 Mobile/Congested Connection (RTT≥40): ' + quality.quality);
602
+ return quality.quality;
603
+ } else {
604
+ // High RTT with lower speed = definitely mobile or slow connection
605
+ const quality = findQualityByMinHeight(480); // 480p
606
+ if (this.options.debug) console.log('📱 Slow Mobile Connection: ' + quality.quality);
607
+ return quality.quality;
608
+ }
609
+ }
610
+
611
+ // Medium speed cases without clear RTT data
612
+ if (downlinkSpeed >= 8) {
613
+ if (isMobileDevice()) {
614
+ const quality = findQualityByMinHeight(720); // Conservative for mobile
615
+ if (this.options.debug) console.log('📱 Mobile Standard Speed: ' + quality.quality);
616
+ return quality.quality;
617
+ } else {
618
+ const quality = findQualityByMinHeight(1080); // Good for desktop
619
+ if (this.options.debug) console.log('🌐 Desktop Standard Speed: ' + quality.quality);
620
+ return quality.quality;
621
+ }
622
+ } else if (downlinkSpeed >= 5) {
623
+ // Lower speed - conservative approach
624
+ const quality = findQualityByMinHeight(720);
625
+ if (this.options.debug) console.log('🌐 Lower Speed Connection: ' + quality.quality);
626
+ return quality.quality;
627
+ } else {
628
+ // Very low speed
629
+ const quality = findQualityByMinHeight(480);
630
+ if (this.options.debug) console.log('🌐 Very Low Speed Connection: ' + quality.quality);
631
+ return quality.quality;
632
+ }
633
+
634
+ } else {
635
+ // No connection information available
636
+ const isMobileDevice = () => {
637
+ return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
638
+ };
639
+
640
+ // Helper function for fallback
641
+ const findQualityByMinHeight = (minHeight) => {
642
+ const sortedQualities = this.qualities
643
+ .filter(q => q.height && q.height >= minHeight)
644
+ .sort((a, b) => b.height - a.height);
645
+ return sortedQualities[0] || maxQuality;
646
+ };
647
+
648
+ if (isMobileDevice()) {
649
+ // Mobile device without connection info - be conservative
650
+ const quality = findQualityByMinHeight(720);
651
+ if (this.options.debug) console.log('📱 Mobile - No Connection Info: ' + quality.quality);
652
+ return quality.quality;
653
+ } else {
654
+ // Desktop without connection info - assume good connection
655
+ const quality = findQualityByMinHeight(1080) || maxQuality;
656
+ if (this.options.debug) console.log('🌐 Desktop - No Connection Info: ' + quality.quality);
657
+ return quality.quality;
658
+ }
659
+ }
660
+
661
+ // Final fallback (should rarely reach here)
662
+ if (this.options.debug) console.log('🌐 Fallback to max quality: ' + maxQuality.quality);
663
+ return maxQuality.quality;
664
+ }
665
+
666
+ applyAutoQuality(targetQuality) {
667
+ if (!targetQuality || !this.video || !this.qualities || this.qualities.length === 0) {
668
+ return;
669
+ }
670
+
671
+ if (this.isChangingQuality) return;
672
+
673
+ const newSource = this.qualities.find(q => q.quality === targetQuality);
674
+ if (!newSource || !newSource.src) {
675
+ if (this.options.debug) console.error('Auto quality', targetQuality, 'not found');
676
+ return;
677
+ }
678
+
679
+ // Store current resolution to restore after quality change
680
+ const currentResolution = this.getCurrentResolution();
681
+
682
+ const currentTime = this.video.currentTime || 0;
683
+ const wasPlaying = !this.video.paused;
684
+
685
+ this.isChangingQuality = true;
686
+ this.video.pause();
687
+
688
+ const onLoadedData = () => {
689
+ if (this.options.debug) console.log('Auto quality', targetQuality, 'applied');
690
+ this.video.currentTime = currentTime;
691
+ if (wasPlaying) {
692
+ this.video.play().catch(e => {
693
+ if (this.options.debug) console.log('Autoplay prevented:', e);
694
+ });
695
+ }
696
+ this.currentPlayingQuality = targetQuality;
697
+ // Keep selectedQuality as 'auto' for UI display
698
+ this.updateQualityDisplay();
699
+ this.isChangingQuality = false;
700
+ cleanup();
701
+ };
702
+
703
+ const onError = (error) => {
704
+ if (this.options.debug) console.error('Auto quality loading error:', error);
705
+ this.isChangingQuality = false;
706
+ cleanup();
707
+ };
708
+
709
+ const cleanup = () => {
710
+ this.video.removeEventListener('loadeddata', onLoadedData);
711
+ this.video.removeEventListener('error', onError);
712
+ };
713
+
714
+ this.video.addEventListener('loadeddata', onLoadedData, { once: true });
715
+ this.video.addEventListener('error', onError, { once: true });
716
+ this.video.src = newSource.src;
717
+ this.video.load();
718
+ }
719
+
720
+ setDefaultQuality(quality) {
721
+ if (this.options.debug) console.log(`🔧 Setting defaultQuality: "${quality}"`);
722
+ this.options.defaultQuality = quality;
723
+ this.selectedQuality = quality;
724
+
725
+ if (quality === 'auto') {
726
+ this.enableAutoQuality();
727
+ } else {
728
+ this.setQuality(quality);
729
+ }
730
+
731
+ return this;
732
+ }
733
+
734
+ getDefaultQuality() {
735
+ return this.options.defaultQuality;
736
+ }
737
+
738
+ getQualityLabel(height, width) {
739
+ if (height >= 2160) return '4K';
740
+ if (height >= 1440) return '1440p';
741
+ if (height >= 1080) return '1080p';
742
+ if (height >= 720) return '720p';
743
+ if (height >= 480) return '480p';
744
+ if (height >= 360) return '360p';
745
+ if (height >= 240) return '240p';
746
+ return `${height}p`;
747
+ }
748
+
749
+ updateAdaptiveQualityMenu() {
750
+ const qualityMenu = this.controls?.querySelector('.quality-menu');
751
+ if (!qualityMenu || !this.isAdaptiveStream) return;
752
+
753
+ let menuHTML = `<div class="quality-option ${this.isAutoQuality() ? 'active' : ''}" data-adaptive-quality="auto">Auto</div>`;
754
+
755
+ this.adaptiveQualities.forEach(quality => {
756
+ const isActive = this.getCurrentAdaptiveQuality() === quality.index;
757
+ menuHTML += `<div class="quality-option ${isActive ? 'active' : ''}" data-adaptive-quality="${quality.index}">${quality.label}</div>`;
758
+ });
759
+
760
+ qualityMenu.innerHTML = menuHTML;
761
+ }
762
+
763
+ updateAdaptiveQualityDisplay() {
764
+ if (!this.isAdaptiveStream) return;
765
+
766
+ const qualityBtn = this.controls?.querySelector('.quality-btn');
767
+ if (!qualityBtn) return;
768
+
769
+ // Determine if auto quality is active
770
+ const isAuto = this.selectedQuality === 'auto' || this.getCurrentAdaptiveQuality() === -1;
771
+ const currentQuality = isAuto ? this.tauto : this.getCurrentAdaptiveQualityLabel();
772
+
773
+ const btnText = qualityBtn.querySelector('.quality-btn-text');
774
+ if (btnText) {
775
+ const selectedEl = btnText.querySelector('.selected-quality');
776
+ const currentEl = btnText.querySelector('.current-quality');
777
+
778
+ if (selectedEl) {
779
+ selectedEl.textContent = isAuto ? this.tauto : currentQuality;
780
+ }
781
+ if (currentEl) {
782
+ currentEl.textContent = currentQuality;
783
+ }
784
+ }
785
+ }
786
+
787
+ setAdaptiveQuality(qualityIndex) {
788
+ if (!this.isAdaptiveStream) return;
789
+
790
+ try {
791
+ if (qualityIndex === 'auto' || qualityIndex === -1) {
792
+ // Enable auto quality
793
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
794
+ this.dashPlayer.updateSettings({
795
+ streaming: {
796
+ abr: { autoSwitchBitrate: { video: true } }
797
+ }
798
+ });
799
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
800
+ this.hlsPlayer.currentLevel = -1; // Auto level selection
801
+ }
802
+ this.selectedQuality = 'auto';
803
+ } else {
804
+ // Set specific quality
805
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
806
+ this.dashPlayer.updateSettings({
807
+ streaming: {
808
+ abr: { autoSwitchBitrate: { video: false } }
809
+ }
810
+ });
811
+ this.dashPlayer.setQualityFor('video', qualityIndex);
812
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
813
+ this.hlsPlayer.currentLevel = qualityIndex;
814
+ }
815
+ this.selectedQuality = this.adaptiveQualities[qualityIndex]?.label || 'Unknown';
816
+ }
817
+
818
+ this.updateAdaptiveQualityDisplay();
819
+ if (this.options.debug) console.log('📡 Adaptive quality set to:', qualityIndex);
820
+
821
+ } catch (error) {
822
+ if (this.options.debug) console.error('📡 Error setting adaptive quality:', error);
823
+ }
824
+ }
825
+
826
+ getCurrentAdaptiveQuality() {
827
+ if (!this.isAdaptiveStream) return null;
828
+
829
+ try {
830
+ if (this.adaptiveStreamingType === 'dash' && this.dashPlayer) {
831
+ return this.dashPlayer.getQualityFor('video');
832
+ } else if (this.adaptiveStreamingType === 'hls' && this.hlsPlayer) {
833
+ return this.hlsPlayer.currentLevel;
834
+ }
835
+ } catch (error) {
836
+ if (this.options.debug) console.error('📡 Error getting current quality:', error);
837
+ }
838
+
839
+ return null;
840
+ }
841
+
842
+ getCurrentAdaptiveQualityLabel() {
843
+ const currentIndex = this.getCurrentAdaptiveQuality();
844
+ if (currentIndex === null || currentIndex === -1) {
845
+ return this.tauto; // Return "Auto" instead of "Unknown"
846
+ }
847
+ return this.adaptiveQualities[currentIndex]?.label || this.tauto;
848
+ }
849
+
850
+ isAutoQuality() {
851
+ if (this.isAdaptiveStream) {
852
+ const currentQuality = this.getCurrentAdaptiveQuality();
853
+ return currentQuality === null || currentQuality === -1 || this.selectedQuality === 'auto';
854
+ }
855
+ return this.selectedQuality === 'auto';
856
+ }
857
+
858
+ setResolution(resolution) {
859
+ if (!this.video || !this.container) {
860
+ if (this.options.debug) console.warn("Video or container not available for setResolution");
861
+ return;
862
+ }
863
+
864
+ // Supported values including new scale-to-fit mode
865
+ const supportedResolutions = ["normal", "4:3", "16:9", "stretched", "fit-to-screen", "scale-to-fit"];
866
+
867
+ if (!supportedResolutions.includes(resolution)) {
868
+ if (this.options.debug) console.warn(`Resolution "${resolution}" not supported. Supported values: ${supportedResolutions.join(", ")}`);
869
+ return;
870
+ }
871
+
872
+ // Remove all previous resolution classes
873
+ const allResolutionClasses = [
874
+ "resolution-normal", "resolution-4-3", "resolution-16-9",
875
+ "resolution-stretched", "resolution-fit-to-screen", "resolution-scale-to-fit"
876
+ ];
877
+
878
+ this.video.classList.remove(...allResolutionClasses);
879
+ if (this.container) {
880
+ this.container.classList.remove(...allResolutionClasses);
881
+ }
882
+
883
+ // Apply new resolution class
884
+ const cssClass = `resolution-${resolution.replace(":", "-")}`;
885
+ this.video.classList.add(cssClass);
886
+ if (this.container) {
887
+ this.container.classList.add(cssClass);
888
+ }
889
+
890
+ // Update option
891
+ this.options.resolution = resolution;
892
+
893
+ if (this.options.debug) {
894
+ console.log(`Resolution applied: ${resolution} (CSS class: ${cssClass})`);
895
+ }
896
+ }
897
+
898
+ getCurrentResolution() {
899
+ return this.options.resolution || "normal";
900
+ }
901
+
902
+ initializeResolution() {
903
+ if (this.options.resolution && this.options.resolution !== "normal") {
904
+ this.setResolution(this.options.resolution);
905
+ }
906
+ }
907
+
908
+ restoreResolutionAfterQualityChange() {
909
+ if (this.options.resolution && this.options.resolution !== "normal") {
910
+ if (this.options.debug) {
911
+ console.log(`Restoring resolution "${this.options.resolution}" after quality change`);
912
+ }
913
+ // Small delay to ensure video element is ready
914
+ setTimeout(() => {
915
+ this.setResolution(this.options.resolution);
916
+ }, 150);
917
+ }
918
+ }
919
+
920
+ // Quality methods for main class
921
+ // All original functionality preserved exactly