unified-video-framework 1.4.256 → 1.4.257
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.
- package/package.json +1 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts +1 -0
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
- package/packages/core/dist/interfaces.d.ts +1 -0
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces/IVideoPlayer.ts +3 -0
- package/packages/core/src/interfaces.ts +1 -0
- package/packages/web/dist/WebPlayer.d.ts +6 -7
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +128 -280
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +190 -392
|
@@ -1184,7 +1184,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
1184
1184
|
width: '100%',
|
|
1185
1185
|
height: '100%',
|
|
1186
1186
|
playerVars: {
|
|
1187
|
-
controls: 0,
|
|
1187
|
+
controls: this.config.youtubeNativeControls !== false ? 1 : 0, // Native controls enabled by default
|
|
1188
1188
|
disablekb: 0, // Allow keyboard controls
|
|
1189
1189
|
fs: 0, // Hide fullscreen button
|
|
1190
1190
|
iv_load_policy: 3, // Hide annotations
|
|
@@ -1252,16 +1252,16 @@ export class WebPlayer extends BasePlayer {
|
|
|
1252
1252
|
this.youtubePlayer.mute();
|
|
1253
1253
|
}
|
|
1254
1254
|
|
|
1255
|
-
//
|
|
1256
|
-
this.
|
|
1255
|
+
// Detect if YouTube video is Live and handle controls
|
|
1256
|
+
this.detectYouTubeLiveStatus();
|
|
1257
1257
|
|
|
1258
1258
|
// Try to get video title from YouTube API
|
|
1259
1259
|
this.getYouTubeVideoTitle();
|
|
1260
1260
|
|
|
1261
|
-
// Update metadata UI and
|
|
1261
|
+
// Update metadata UI and controls visibility after player is ready
|
|
1262
1262
|
setTimeout(() => {
|
|
1263
1263
|
this.updateMetadataUI();
|
|
1264
|
-
this.
|
|
1264
|
+
this.updateControlsVisibility();
|
|
1265
1265
|
}, 500);
|
|
1266
1266
|
}
|
|
1267
1267
|
|
|
@@ -1277,16 +1277,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
1277
1277
|
switch (state) {
|
|
1278
1278
|
case window.YT.PlayerState.PLAYING:
|
|
1279
1279
|
this.state.isPlaying = true;
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
// Extract qualities when video starts playing
|
|
1285
|
-
setTimeout(() => {
|
|
1286
|
-
this.extractYouTubeAvailableQualities();
|
|
1287
|
-
}, 1000);
|
|
1288
|
-
|
|
1289
|
-
this.emit('onPlay');
|
|
1280
|
+
this.state.isPaused = false;
|
|
1281
|
+
this.state.isBuffering = false;
|
|
1282
|
+
this.updateYouTubeUI('playing');
|
|
1283
|
+
this.emit('onPlay');
|
|
1290
1284
|
break;
|
|
1291
1285
|
|
|
1292
1286
|
case window.YT.PlayerState.PAUSED:
|
|
@@ -1297,16 +1291,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
1297
1291
|
this.emit('onPause');
|
|
1298
1292
|
break;
|
|
1299
1293
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
// Try to extract qualities during buffering as well
|
|
1305
|
-
setTimeout(() => {
|
|
1306
|
-
this.extractYouTubeAvailableQualities();
|
|
1307
|
-
}, 500);
|
|
1308
|
-
|
|
1309
|
-
this.emit('onBuffering', true);
|
|
1294
|
+
case window.YT.PlayerState.BUFFERING:
|
|
1295
|
+
this.state.isBuffering = true;
|
|
1296
|
+
this.updateYouTubeUI('buffering');
|
|
1297
|
+
this.emit('onBuffering', true);
|
|
1310
1298
|
break;
|
|
1311
1299
|
|
|
1312
1300
|
case window.YT.PlayerState.ENDED:
|
|
@@ -1317,15 +1305,16 @@ export class WebPlayer extends BasePlayer {
|
|
|
1317
1305
|
this.emit('onEnded');
|
|
1318
1306
|
break;
|
|
1319
1307
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1308
|
+
case window.YT.PlayerState.CUED:
|
|
1309
|
+
this.state.duration = this.youtubePlayer.getDuration();
|
|
1310
|
+
this.updateYouTubeUI('cued');
|
|
1311
|
+
|
|
1312
|
+
// Re-check Live status when video is cued
|
|
1313
|
+
setTimeout(() => {
|
|
1314
|
+
this.detectYouTubeLiveStatus();
|
|
1315
|
+
this.updateControlsVisibility();
|
|
1316
|
+
}, 500);
|
|
1317
|
+
break;
|
|
1329
1318
|
}
|
|
1330
1319
|
}
|
|
1331
1320
|
|
|
@@ -1403,359 +1392,207 @@ export class WebPlayer extends BasePlayer {
|
|
|
1403
1392
|
}
|
|
1404
1393
|
|
|
1405
1394
|
/**
|
|
1406
|
-
*
|
|
1395
|
+
* Detect if YouTube video is Live
|
|
1407
1396
|
*/
|
|
1408
|
-
private
|
|
1409
|
-
if (!this.
|
|
1410
|
-
|
|
1411
|
-
try {
|
|
1412
|
-
const videoId = this.source.metadata.videoId;
|
|
1413
|
-
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
|
|
1414
|
-
|
|
1415
|
-
const response = await fetch(oembedUrl);
|
|
1416
|
-
if (response.ok) {
|
|
1417
|
-
const data = await response.json();
|
|
1418
|
-
if (data.title) {
|
|
1419
|
-
this.debugLog('Got YouTube title from oembed API:', data.title);
|
|
1420
|
-
|
|
1421
|
-
// Update source metadata
|
|
1422
|
-
if (this.source && this.source.metadata) {
|
|
1423
|
-
this.source.metadata.title = data.title;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Update UI
|
|
1427
|
-
this.updateMetadataUI();
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
} catch (error) {
|
|
1431
|
-
this.debugWarn('Could not get YouTube title from oembed API:', error);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
private youtubeTimeTrackingInterval: NodeJS.Timeout | null = null;
|
|
1436
|
-
private youtubeAvailableQualities: any[] = [];
|
|
1437
|
-
private youtubeCurrentQuality: any = null;
|
|
1438
|
-
|
|
1439
|
-
/**
|
|
1440
|
-
* Extract available YouTube video qualities with retry logic
|
|
1441
|
-
*/
|
|
1442
|
-
private extractYouTubeAvailableQualities(retryCount: number = 0): void {
|
|
1443
|
-
if (!this.youtubePlayer || !this.youtubePlayerReady) {
|
|
1444
|
-
this.debugLog('YouTube player not ready for quality extraction');
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1397
|
+
private detectYouTubeLiveStatus(): void {
|
|
1398
|
+
if (!this.youtubePlayer || !this.youtubePlayerReady) return;
|
|
1447
1399
|
|
|
1448
1400
|
try {
|
|
1449
|
-
const
|
|
1450
|
-
this.
|
|
1451
|
-
this.debugLog('🔍 YouTube player state:', this.youtubePlayer.getPlayerState());
|
|
1452
|
-
this.debugLog('🔍 Current YouTube quality:', this.youtubePlayer.getPlaybackQuality());
|
|
1453
|
-
|
|
1454
|
-
// If no qualities detected and we haven't retried too much, retry after a delay
|
|
1455
|
-
if ((!availableQualityLevels || availableQualityLevels.length === 0 || (availableQualityLevels.length === 1 && availableQualityLevels[0] === 'auto')) && retryCount < 5) {
|
|
1456
|
-
this.debugLog('No meaningful qualities detected, retrying in', (retryCount + 1) * 1000, 'ms...');
|
|
1457
|
-
setTimeout(() => {
|
|
1458
|
-
this.extractYouTubeAvailableQualities(retryCount + 1);
|
|
1459
|
-
}, (retryCount + 1) * 1000);
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// Map YouTube quality levels to standard labels
|
|
1464
|
-
const qualityMap: Record<string, any> = {
|
|
1465
|
-
'hd2160': { label: '4K', value: 'hd2160', height: 2160 },
|
|
1466
|
-
'hd1440': { label: '1440p', value: 'hd1440', height: 1440 },
|
|
1467
|
-
'hd1080': { label: '1080p', value: 'hd1080', height: 1080 },
|
|
1468
|
-
'hd720': { label: '720p', value: 'hd720', height: 720 },
|
|
1469
|
-
'large': { label: '480p', value: 'large', height: 480 },
|
|
1470
|
-
'medium': { label: '360p', value: 'medium', height: 360 },
|
|
1471
|
-
'small': { label: '240p', value: 'small', height: 240 },
|
|
1472
|
-
'tiny': { label: '144p', value: 'tiny', height: 144 },
|
|
1473
|
-
'auto': { label: 'Auto', value: 'auto', height: 0 }
|
|
1474
|
-
};
|
|
1475
|
-
|
|
1476
|
-
this.youtubeAvailableQualities = [];
|
|
1401
|
+
const videoData = this.youtubePlayer.getVideoData();
|
|
1402
|
+
this.isYouTubeLive = videoData?.isLive || false;
|
|
1477
1403
|
|
|
1478
|
-
//
|
|
1479
|
-
this.
|
|
1404
|
+
// Auto-enable native controls for all YouTube content by default, unless explicitly disabled
|
|
1405
|
+
this.useYouTubeNativeControls = this.config.youtubeNativeControls !== false;
|
|
1480
1406
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
}
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
// Sort qualities by height (descending, excluding auto)
|
|
1490
|
-
const autoQuality = this.youtubeAvailableQualities.shift(); // Remove auto temporarily
|
|
1491
|
-
this.youtubeAvailableQualities.sort((a, b) => (b.height || 0) - (a.height || 0));
|
|
1492
|
-
if (autoQuality) this.youtubeAvailableQualities.unshift(autoQuality); // Add auto back at beginning
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
this.debugLog('✅ Successfully mapped YouTube qualities:', this.youtubeAvailableQualities);
|
|
1407
|
+
this.debugLog('YouTube Live status:', {
|
|
1408
|
+
isLive: this.isYouTubeLive,
|
|
1409
|
+
useNativeControls: this.useYouTubeNativeControls,
|
|
1410
|
+
videoId: videoData?.video_id
|
|
1411
|
+
});
|
|
1496
1412
|
|
|
1497
|
-
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
this.debugWarn('Could not detect YouTube Live status:', error);
|
|
1415
|
+
// Fallback: check duration - Live videos typically have duration = 0
|
|
1498
1416
|
try {
|
|
1499
|
-
const
|
|
1500
|
-
this.
|
|
1501
|
-
this.
|
|
1417
|
+
const duration = this.youtubePlayer.getDuration();
|
|
1418
|
+
this.isYouTubeLive = !duration || duration === 0;
|
|
1419
|
+
this.useYouTubeNativeControls = this.config.youtubeNativeControls !== false;
|
|
1420
|
+
|
|
1421
|
+
this.debugLog('YouTube Live detected via duration check:', {
|
|
1422
|
+
duration,
|
|
1423
|
+
isLive: this.isYouTubeLive,
|
|
1424
|
+
useNativeControls: this.useYouTubeNativeControls
|
|
1425
|
+
});
|
|
1502
1426
|
} catch (e) {
|
|
1503
|
-
this.debugWarn('Could not
|
|
1504
|
-
this.youtubeCurrentQuality = qualityMap['auto'];
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// Update detected qualities to include YouTube qualities
|
|
1508
|
-
this.detectAvailableQualities();
|
|
1509
|
-
|
|
1510
|
-
this.debugLog('🚀 YouTube qualities successfully detected! Available qualities now:', this.availableQualities.length);
|
|
1511
|
-
|
|
1512
|
-
// Update quality badge with current YouTube quality
|
|
1513
|
-
this.updateQualityBadge();
|
|
1514
|
-
|
|
1515
|
-
// Force settings menu refresh with new qualities
|
|
1516
|
-
setTimeout(() => {
|
|
1517
|
-
this.debugLog('🔄 Force refreshing settings menu with YouTube qualities');
|
|
1518
|
-
this.updateSettingsMenu();
|
|
1519
|
-
}, 100);
|
|
1520
|
-
|
|
1521
|
-
} catch (error) {
|
|
1522
|
-
this.debugWarn('Could not extract YouTube qualities:', error);
|
|
1523
|
-
// Retry if we haven't tried too many times
|
|
1524
|
-
if (retryCount < 3) {
|
|
1525
|
-
setTimeout(() => {
|
|
1526
|
-
this.extractYouTubeAvailableQualities(retryCount + 1);
|
|
1527
|
-
}, 2000);
|
|
1427
|
+
this.debugWarn('Could not check YouTube duration for Live detection:', e);
|
|
1528
1428
|
}
|
|
1529
1429
|
}
|
|
1530
1430
|
}
|
|
1531
1431
|
|
|
1532
1432
|
/**
|
|
1533
|
-
*
|
|
1433
|
+
* Update controls visibility based on YouTube Live status and config
|
|
1534
1434
|
*/
|
|
1535
|
-
private
|
|
1536
|
-
|
|
1435
|
+
private updateControlsVisibility(): void {
|
|
1436
|
+
const controlsContainer = document.getElementById('uvf-controls-container');
|
|
1437
|
+
if (!controlsContainer) return;
|
|
1537
1438
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
'hd1440': { label: '1440p', value: 'hd1440', height: 1440 },
|
|
1542
|
-
'hd1080': { label: '1080p', value: 'hd1080', height: 1080 },
|
|
1543
|
-
'hd720': { label: '720p', value: 'hd720', height: 720 },
|
|
1544
|
-
'large': { label: '480p', value: 'large', height: 480 },
|
|
1545
|
-
'medium': { label: '360p', value: 'medium', height: 360 },
|
|
1546
|
-
'small': { label: '240p', value: 'small', height: 240 },
|
|
1547
|
-
'tiny': { label: '144p', value: 'tiny', height: 144 },
|
|
1548
|
-
'auto': { label: 'Auto', value: 'auto', height: 0 }
|
|
1549
|
-
};
|
|
1439
|
+
if (this.youtubePlayer && this.useYouTubeNativeControls) {
|
|
1440
|
+
// Hide custom controls and show YouTube native controls
|
|
1441
|
+
controlsContainer.style.display = 'none';
|
|
1550
1442
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
if (qualityLevel === 'auto') {
|
|
1555
|
-
// For auto, just use setPlaybackQuality
|
|
1556
|
-
this.youtubePlayer.setPlaybackQuality(qualityLevel);
|
|
1557
|
-
} else {
|
|
1558
|
-
// For specific qualities, try multiple methods
|
|
1559
|
-
try {
|
|
1560
|
-
// Method 1: Set quality range (most effective for forcing specific quality)
|
|
1561
|
-
if (typeof this.youtubePlayer.setPlaybackQualityRange === 'function') {
|
|
1562
|
-
this.youtubePlayer.setPlaybackQualityRange(qualityLevel, qualityLevel);
|
|
1563
|
-
this.debugLog('✅ setPlaybackQualityRange called for:', qualityLevel);
|
|
1564
|
-
}
|
|
1565
|
-
} catch (e) {
|
|
1566
|
-
this.debugWarn('setPlaybackQualityRange failed:', e);
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
try {
|
|
1570
|
-
// Method 2: Standard quality setting
|
|
1571
|
-
this.youtubePlayer.setPlaybackQuality(qualityLevel);
|
|
1572
|
-
this.debugLog('✅ setPlaybackQuality called for:', qualityLevel);
|
|
1573
|
-
} catch (e) {
|
|
1574
|
-
this.debugWarn('setPlaybackQuality failed:', e);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// Method 3: Force quality with seekTo trick (most aggressive)
|
|
1578
|
-
try {
|
|
1579
|
-
const currentTime = this.youtubePlayer.getCurrentTime();
|
|
1580
|
-
const wasPlaying = this.youtubePlayer.getPlayerState() === window.YT.PlayerState.PLAYING;
|
|
1581
|
-
|
|
1582
|
-
// Seek to current time + 0.1 seconds to force reload with new quality
|
|
1583
|
-
this.youtubePlayer.seekTo(currentTime + 0.1, true);
|
|
1584
|
-
|
|
1585
|
-
// Set quality immediately after seek
|
|
1586
|
-
setTimeout(() => {
|
|
1587
|
-
this.youtubePlayer.setPlaybackQuality(qualityLevel);
|
|
1588
|
-
|
|
1589
|
-
// Seek back to original time
|
|
1590
|
-
setTimeout(() => {
|
|
1591
|
-
this.youtubePlayer.seekTo(currentTime, true);
|
|
1592
|
-
if (!wasPlaying) {
|
|
1593
|
-
this.youtubePlayer.pauseVideo();
|
|
1594
|
-
}
|
|
1595
|
-
}, 100);
|
|
1596
|
-
}, 50);
|
|
1597
|
-
|
|
1598
|
-
this.debugLog('🔄 Applied seekTo trick for quality change');
|
|
1599
|
-
} catch (e) {
|
|
1600
|
-
this.debugWarn('Force quality change with seekTo trick failed:', e);
|
|
1601
|
-
}
|
|
1443
|
+
// Re-create YouTube player with native controls if needed
|
|
1444
|
+
if (!this.config.youtubeNativeControls) {
|
|
1445
|
+
this.recreateYouTubePlayerWithNativeControls();
|
|
1602
1446
|
}
|
|
1603
1447
|
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1448
|
+
this.debugLog('✅ YouTube native controls enabled', {
|
|
1449
|
+
isLive: this.isYouTubeLive,
|
|
1450
|
+
reason: this.config.youtubeNativeControls !== false ? 'Default behavior (all YouTube content)' : 'Live stream detected'
|
|
1451
|
+
});
|
|
1452
|
+
} else {
|
|
1453
|
+
// Show custom controls
|
|
1454
|
+
controlsContainer.style.display = 'flex';
|
|
1455
|
+
this.debugLog('✅ Custom controls enabled for YouTube video');
|
|
1609
1456
|
}
|
|
1610
1457
|
}
|
|
1611
1458
|
|
|
1612
1459
|
/**
|
|
1613
|
-
*
|
|
1460
|
+
* Recreate YouTube player with native controls enabled
|
|
1614
1461
|
*/
|
|
1615
|
-
private
|
|
1616
|
-
|
|
1617
|
-
const delays = [500, 1500, 3000]; // Progressive delays
|
|
1462
|
+
private recreateYouTubePlayerWithNativeControls(): void {
|
|
1463
|
+
if (!this.source?.metadata?.videoId) return;
|
|
1618
1464
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}
|
|
1465
|
+
const videoId = this.source.metadata.videoId;
|
|
1466
|
+
const currentTime = this.youtubePlayer?.getCurrentTime() || 0;
|
|
1467
|
+
|
|
1468
|
+
// Find the container
|
|
1469
|
+
const container = this.playerWrapper || this.video?.parentElement;
|
|
1470
|
+
if (!container) return;
|
|
1471
|
+
|
|
1472
|
+
// Destroy current player
|
|
1473
|
+
if (this.youtubePlayer) {
|
|
1474
|
+
this.youtubePlayer.destroy();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// Remove existing iframe container
|
|
1478
|
+
const existingContainer = container.querySelector(`#youtube-player-${videoId}`);
|
|
1479
|
+
if (existingContainer) {
|
|
1480
|
+
existingContainer.remove();
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Create new iframe container
|
|
1484
|
+
const iframeContainer = document.createElement('div');
|
|
1485
|
+
iframeContainer.id = `youtube-player-${videoId}`;
|
|
1486
|
+
iframeContainer.style.cssText = `
|
|
1487
|
+
position: absolute;
|
|
1488
|
+
top: 0;
|
|
1489
|
+
left: 0;
|
|
1490
|
+
width: 100%;
|
|
1491
|
+
height: 100%;
|
|
1492
|
+
z-index: 1;
|
|
1493
|
+
`;
|
|
1494
|
+
container.appendChild(iframeContainer);
|
|
1495
|
+
|
|
1496
|
+
// Create new player with native controls
|
|
1497
|
+
this.youtubePlayer = new window.YT.Player(iframeContainer.id, {
|
|
1498
|
+
videoId: videoId,
|
|
1499
|
+
width: '100%',
|
|
1500
|
+
height: '100%',
|
|
1501
|
+
playerVars: {
|
|
1502
|
+
autoplay: this.config.autoPlay ? 1 : 0,
|
|
1503
|
+
controls: 1, // Enable native controls
|
|
1504
|
+
modestbranding: 1,
|
|
1505
|
+
rel: 0,
|
|
1506
|
+
showinfo: 0,
|
|
1507
|
+
iv_load_policy: 3,
|
|
1508
|
+
playsinline: 1,
|
|
1509
|
+
start: Math.floor(currentTime)
|
|
1510
|
+
},
|
|
1511
|
+
events: {
|
|
1512
|
+
onReady: () => {
|
|
1513
|
+
this.youtubePlayerReady = true;
|
|
1514
|
+
this.debugLog('YouTube player with native controls ready');
|
|
1515
|
+
this.emit('onReady');
|
|
1516
|
+
},
|
|
1517
|
+
onStateChange: (event: any) => this.onYouTubePlayerStateChange(event),
|
|
1518
|
+
onError: (event: any) => this.onYouTubePlayerError(event)
|
|
1674
1519
|
}
|
|
1675
|
-
}
|
|
1520
|
+
});
|
|
1676
1521
|
}
|
|
1677
1522
|
|
|
1678
1523
|
/**
|
|
1679
|
-
*
|
|
1524
|
+
* Toggle between native and custom YouTube controls
|
|
1680
1525
|
*/
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
this.debugLog('🔄 Retry attempt 1: Using cueVideoById');
|
|
1686
|
-
const videoData = this.youtubePlayer.getVideoData();
|
|
1687
|
-
const currentTime = this.youtubePlayer.getCurrentTime();
|
|
1688
|
-
|
|
1689
|
-
this.youtubePlayer.cueVideoById({
|
|
1690
|
-
videoId: videoData.video_id,
|
|
1691
|
-
startSeconds: currentTime,
|
|
1692
|
-
suggestedQuality: qualityLevel
|
|
1693
|
-
});
|
|
1694
|
-
|
|
1695
|
-
setTimeout(() => this.youtubePlayer.playVideo(), 100);
|
|
1696
|
-
|
|
1697
|
-
} else if (attempt === 2) {
|
|
1698
|
-
// Third attempt: Multiple sequential calls
|
|
1699
|
-
this.debugLog('🔄 Retry attempt 2: Multiple sequential calls');
|
|
1700
|
-
|
|
1701
|
-
for (let i = 0; i < 3; i++) {
|
|
1702
|
-
setTimeout(() => {
|
|
1703
|
-
this.youtubePlayer.setPlaybackQuality(qualityLevel);
|
|
1704
|
-
}, i * 100);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
} catch (e) {
|
|
1708
|
-
this.debugWarn('Retry attempt', attempt, 'failed:', e);
|
|
1526
|
+
public toggleYouTubeControls(useNative: boolean = !this.useYouTubeNativeControls): void {
|
|
1527
|
+
if (!this.youtubePlayer) {
|
|
1528
|
+
this.debugWarn('Cannot toggle YouTube controls - no YouTube player active');
|
|
1529
|
+
return;
|
|
1709
1530
|
}
|
|
1531
|
+
|
|
1532
|
+
this.useYouTubeNativeControls = useNative;
|
|
1533
|
+
this.config.youtubeNativeControls = useNative; // Update config
|
|
1534
|
+
this.updateControlsVisibility();
|
|
1535
|
+
|
|
1536
|
+
this.debugLog('YouTube controls toggled:', {
|
|
1537
|
+
useNative: this.useYouTubeNativeControls,
|
|
1538
|
+
isLive: this.isYouTubeLive
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
// Show notification
|
|
1542
|
+
this.showNotification(
|
|
1543
|
+
`YouTube Controls: ${this.useYouTubeNativeControls ? 'Native' : 'Custom'}`
|
|
1544
|
+
);
|
|
1710
1545
|
}
|
|
1711
1546
|
|
|
1712
1547
|
/**
|
|
1713
|
-
*
|
|
1548
|
+
* Fallback method to get title from YouTube oembed API
|
|
1714
1549
|
*/
|
|
1715
|
-
private
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1550
|
+
private async getYouTubeVideoTitleFromOEmbed(): Promise<void> {
|
|
1551
|
+
if (!this.source?.metadata?.videoId) return;
|
|
1552
|
+
|
|
1553
|
+
try {
|
|
1554
|
+
const videoId = this.source.metadata.videoId;
|
|
1555
|
+
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
|
|
1556
|
+
|
|
1557
|
+
const response = await fetch(oembedUrl);
|
|
1558
|
+
if (response.ok) {
|
|
1559
|
+
const data = await response.json();
|
|
1560
|
+
if (data.title) {
|
|
1561
|
+
this.debugLog('Got YouTube title from oembed API:', data.title);
|
|
1562
|
+
|
|
1563
|
+
// Update source metadata
|
|
1564
|
+
if (this.source && this.source.metadata) {
|
|
1565
|
+
this.source.metadata.title = data.title;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Update UI
|
|
1569
|
+
this.updateMetadataUI();
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
this.debugWarn('Could not get YouTube title from oembed API:', error);
|
|
1731
1574
|
}
|
|
1732
1575
|
}
|
|
1733
1576
|
|
|
1577
|
+
private youtubeTimeTrackingInterval: NodeJS.Timeout | null = null;
|
|
1578
|
+
private isYouTubeLive: boolean = false;
|
|
1579
|
+
private useYouTubeNativeControls: boolean = false;
|
|
1580
|
+
|
|
1734
1581
|
private startYouTubeTimeTracking(): void {
|
|
1735
1582
|
if (this.youtubeTimeTrackingInterval) {
|
|
1736
1583
|
clearInterval(this.youtubeTimeTrackingInterval);
|
|
1737
1584
|
}
|
|
1738
1585
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
this.state.duration = duration || 0;
|
|
1750
|
-
this.state.bufferedPercentage = buffered || 0;
|
|
1751
|
-
|
|
1752
|
-
// Check for qualities periodically (every 10 seconds) if we don't have them yet
|
|
1753
|
-
qualityCheckCounter++;
|
|
1754
|
-
if (qualityCheckCounter >= 40 && this.youtubeAvailableQualities.length <= 1) { // 40 * 250ms = 10s
|
|
1755
|
-
this.debugLog('Periodic YouTube quality check triggered');
|
|
1756
|
-
this.extractYouTubeAvailableQualities();
|
|
1757
|
-
qualityCheckCounter = 0; // Reset counter
|
|
1758
|
-
}
|
|
1586
|
+
this.youtubeTimeTrackingInterval = setInterval(() => {
|
|
1587
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1588
|
+
try {
|
|
1589
|
+
const currentTime = this.youtubePlayer.getCurrentTime();
|
|
1590
|
+
const duration = this.youtubePlayer.getDuration();
|
|
1591
|
+
const buffered = this.youtubePlayer.getVideoLoadedFraction() * 100;
|
|
1592
|
+
|
|
1593
|
+
this.state.currentTime = currentTime || 0;
|
|
1594
|
+
this.state.duration = duration || 0;
|
|
1595
|
+
this.state.bufferedPercentage = buffered || 0;
|
|
1759
1596
|
|
|
1760
1597
|
// Update UI progress bar
|
|
1761
1598
|
this.updateYouTubeProgressBar(currentTime, duration, buffered);
|
|
@@ -9536,23 +9373,15 @@ export class WebPlayer extends BasePlayer {
|
|
|
9536
9373
|
private setQualityByLabel(quality: string): void {
|
|
9537
9374
|
// Handle YouTube player
|
|
9538
9375
|
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
9539
|
-
//
|
|
9540
|
-
this.
|
|
9541
|
-
|
|
9542
|
-
// Update UI to reflect selection
|
|
9376
|
+
// YouTube quality is limited - show notification
|
|
9377
|
+
this.showShortcutIndicator('Quality control not available for YouTube');
|
|
9378
|
+
// Update UI to reflect current quality
|
|
9543
9379
|
document.querySelectorAll('.quality-option').forEach(option => {
|
|
9544
9380
|
option.classList.remove('active');
|
|
9545
|
-
if ((option as HTMLElement).dataset.quality ===
|
|
9381
|
+
if ((option as HTMLElement).dataset.quality === 'auto') {
|
|
9546
9382
|
option.classList.add('active');
|
|
9547
9383
|
}
|
|
9548
9384
|
});
|
|
9549
|
-
|
|
9550
|
-
// Update quality badge
|
|
9551
|
-
const qualityBadge = document.getElementById('uvf-quality-badge');
|
|
9552
|
-
if (qualityBadge) {
|
|
9553
|
-
const qualityOption = this.youtubeAvailableQualities.find(q => q.value === quality);
|
|
9554
|
-
qualityBadge.textContent = qualityOption ? qualityOption.label : 'AUTO';
|
|
9555
|
-
}
|
|
9556
9385
|
return;
|
|
9557
9386
|
}
|
|
9558
9387
|
|
|
@@ -9853,17 +9682,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
9853
9682
|
|
|
9854
9683
|
// Quality Accordion Section (only if enabled in config and qualities detected)
|
|
9855
9684
|
if (this.settingsConfig.quality && this.availableQualities.length > 0) {
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
// For YouTube, use youtubeCurrentQuality; otherwise use currentQuality
|
|
9859
|
-
const qualityForDisplay = this.youtubePlayer && this.youtubePlayerReady && this.youtubeCurrentQuality
|
|
9860
|
-
? this.youtubeCurrentQuality.value
|
|
9861
|
-
: this.currentQuality;
|
|
9862
|
-
const currentQuality = this.availableQualities.find(q => q.value === qualityForDisplay);
|
|
9685
|
+
const currentQuality = this.availableQualities.find(q => q.value === this.currentQuality);
|
|
9863
9686
|
const currentQualityLabel = currentQuality ? currentQuality.label : 'Auto';
|
|
9864
9687
|
|
|
9865
|
-
this.debugLog('🎛️ Current quality for display:', qualityForDisplay, '(', currentQualityLabel, ')');
|
|
9866
|
-
|
|
9867
9688
|
menuHTML += `
|
|
9868
9689
|
<div class="uvf-accordion-item">
|
|
9869
9690
|
<div class="uvf-accordion-header" data-section="quality">
|
|
@@ -9881,11 +9702,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9881
9702
|
<div class="uvf-accordion-content" data-section="quality">`;
|
|
9882
9703
|
|
|
9883
9704
|
this.availableQualities.forEach(quality => {
|
|
9884
|
-
|
|
9885
|
-
const qualityValue = this.youtubePlayer && this.youtubePlayerReady && this.youtubeCurrentQuality
|
|
9886
|
-
? this.youtubeCurrentQuality.value
|
|
9887
|
-
: this.currentQuality;
|
|
9888
|
-
const isActive = quality.value === qualityValue ? 'active' : '';
|
|
9705
|
+
const isActive = quality.value === this.currentQuality ? 'active' : '';
|
|
9889
9706
|
const isPremium = this.isQualityPremium(quality);
|
|
9890
9707
|
const isLocked = isPremium && !this.isPremiumUser();
|
|
9891
9708
|
const qualityHeight = (quality as any).height || 0;
|
|
@@ -9953,22 +9770,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
9953
9770
|
* Detect available video qualities from different sources
|
|
9954
9771
|
*/
|
|
9955
9772
|
private detectAvailableQualities(): void {
|
|
9956
|
-
|
|
9957
|
-
const isYouTube = this.youtubePlayer && this.youtubePlayerReady && this.youtubeAvailableQualities.length > 0;
|
|
9958
|
-
this.availableQualities = [];
|
|
9773
|
+
this.availableQualities = [{ value: 'auto', label: 'Auto' }];
|
|
9959
9774
|
let detectedQualities: Array<{ value: string; label: string; height?: number }> = [];
|
|
9960
9775
|
|
|
9961
|
-
|
|
9962
|
-
if (isYouTube) {
|
|
9963
|
-
detectedQualities = this.youtubeAvailableQualities.map(q => ({
|
|
9964
|
-
value: q.value,
|
|
9965
|
-
label: q.label,
|
|
9966
|
-
height: q.height
|
|
9967
|
-
}));
|
|
9968
|
-
this.debugLog('Using YouTube qualities:', detectedQualities);
|
|
9969
|
-
this.availableQualities = detectedQualities;
|
|
9970
|
-
return; // Early return for YouTube
|
|
9971
|
-
} else if (this.hls && this.hls.levels) {
|
|
9776
|
+
if (this.hls && this.hls.levels) {
|
|
9972
9777
|
// HLS qualities
|
|
9973
9778
|
this.hls.levels.forEach((level: any, index: number) => {
|
|
9974
9779
|
if (level.height) {
|
|
@@ -10340,13 +10145,6 @@ export class WebPlayer extends BasePlayer {
|
|
|
10340
10145
|
private setQualityFromSettings(quality: string): void {
|
|
10341
10146
|
this.currentQuality = quality;
|
|
10342
10147
|
|
|
10343
|
-
// Handle YouTube quality
|
|
10344
|
-
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
10345
|
-
this.setYouTubeQuality(quality);
|
|
10346
|
-
this.debugLog(`YouTube quality set to ${quality}`);
|
|
10347
|
-
return;
|
|
10348
|
-
}
|
|
10349
|
-
|
|
10350
10148
|
if (quality === 'auto') {
|
|
10351
10149
|
// Enable auto quality with filter consideration
|
|
10352
10150
|
if (this.hls) {
|