unified-video-framework 1.4.256 → 1.4.258
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 +7 -7
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +181 -277
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +259 -388
|
@@ -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 === true ? 1 : 0, // Native controls disabled by default for custom controls
|
|
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,280 @@ 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.
|
|
1397
|
+
private detectYouTubeLiveStatus(): void {
|
|
1398
|
+
if (!this.youtubePlayer || !this.youtubePlayerReady) return;
|
|
1410
1399
|
|
|
1411
1400
|
try {
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1401
|
+
const videoData = this.youtubePlayer.getVideoData();
|
|
1402
|
+
this.isYouTubeLive = videoData?.isLive || false;
|
|
1403
|
+
|
|
1404
|
+
// Use custom controls by default, unless youtubeNativeControls is explicitly set to true
|
|
1405
|
+
this.useYouTubeNativeControls = this.config.youtubeNativeControls === true;
|
|
1406
|
+
|
|
1407
|
+
this.debugLog('YouTube Live status:', {
|
|
1408
|
+
isLive: this.isYouTubeLive,
|
|
1409
|
+
useNativeControls: this.useYouTubeNativeControls,
|
|
1410
|
+
videoId: videoData?.video_id
|
|
1411
|
+
});
|
|
1414
1412
|
|
|
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
1413
|
} catch (error) {
|
|
1431
|
-
this.debugWarn('Could not
|
|
1414
|
+
this.debugWarn('Could not detect YouTube Live status:', error);
|
|
1415
|
+
// Fallback: check duration - Live videos typically have duration = 0
|
|
1416
|
+
try {
|
|
1417
|
+
const duration = this.youtubePlayer.getDuration();
|
|
1418
|
+
this.isYouTubeLive = !duration || duration === 0;
|
|
1419
|
+
this.useYouTubeNativeControls = this.config.youtubeNativeControls === true;
|
|
1420
|
+
|
|
1421
|
+
this.debugLog('YouTube Live detected via duration check:', {
|
|
1422
|
+
duration,
|
|
1423
|
+
isLive: this.isYouTubeLive,
|
|
1424
|
+
useNativeControls: this.useYouTubeNativeControls
|
|
1425
|
+
});
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
this.debugWarn('Could not check YouTube duration for Live detection:', e);
|
|
1428
|
+
}
|
|
1432
1429
|
}
|
|
1433
1430
|
}
|
|
1434
|
-
|
|
1435
|
-
private youtubeTimeTrackingInterval: NodeJS.Timeout | null = null;
|
|
1436
|
-
private youtubeAvailableQualities: any[] = [];
|
|
1437
|
-
private youtubeCurrentQuality: any = null;
|
|
1438
|
-
|
|
1431
|
+
|
|
1439
1432
|
/**
|
|
1440
|
-
*
|
|
1433
|
+
* Update controls visibility based on YouTube Live status and config
|
|
1441
1434
|
*/
|
|
1442
|
-
private
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1435
|
+
private updateControlsVisibility(): void {
|
|
1436
|
+
const controlsContainer = document.getElementById('uvf-controls-container');
|
|
1437
|
+
if (!controlsContainer) return;
|
|
1447
1438
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
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
|
-
};
|
|
1439
|
+
if (this.youtubePlayer && this.useYouTubeNativeControls) {
|
|
1440
|
+
// Hide custom controls and show YouTube native controls
|
|
1441
|
+
controlsContainer.style.display = 'none';
|
|
1475
1442
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
// Add detected qualities, filtering out 'auto' since we already added it
|
|
1482
|
-
if (availableQualityLevels && availableQualityLevels.length > 0) {
|
|
1483
|
-
availableQualityLevels.forEach((qualityLevel: string) => {
|
|
1484
|
-
if (qualityLevel !== 'auto' && qualityMap[qualityLevel]) {
|
|
1485
|
-
this.youtubeAvailableQualities.push(qualityMap[qualityLevel]);
|
|
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
|
|
1443
|
+
// Re-create YouTube player with native controls if the current player doesn't have them
|
|
1444
|
+
// This happens when the player was initially created with controls disabled
|
|
1445
|
+
if (this.config.youtubeNativeControls === false) {
|
|
1446
|
+
this.recreateYouTubePlayerWithNativeControls();
|
|
1493
1447
|
}
|
|
1494
1448
|
|
|
1495
|
-
this.debugLog('✅
|
|
1449
|
+
this.debugLog('✅ YouTube native controls enabled', {
|
|
1450
|
+
isLive: this.isYouTubeLive,
|
|
1451
|
+
reason: this.config.youtubeNativeControls === true ? 'Explicitly enabled in config' : 'Live stream detected'
|
|
1452
|
+
});
|
|
1453
|
+
} else {
|
|
1454
|
+
// Show custom controls and ensure YouTube native controls are disabled
|
|
1455
|
+
controlsContainer.style.display = 'flex';
|
|
1496
1456
|
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
this.youtubeCurrentQuality = qualityMap[currentQuality] || qualityMap['auto'];
|
|
1501
|
-
this.debugLog('Current YouTube quality:', this.youtubeCurrentQuality);
|
|
1502
|
-
} catch (e) {
|
|
1503
|
-
this.debugWarn('Could not get current YouTube quality:', e);
|
|
1504
|
-
this.youtubeCurrentQuality = qualityMap['auto'];
|
|
1457
|
+
// Re-create YouTube player without native controls if needed
|
|
1458
|
+
if (this.config.youtubeNativeControls === true) {
|
|
1459
|
+
this.recreateYouTubePlayerWithoutNativeControls();
|
|
1505
1460
|
}
|
|
1506
1461
|
|
|
1507
|
-
|
|
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);
|
|
1528
|
-
}
|
|
1462
|
+
this.debugLog('✅ Custom controls enabled for YouTube video');
|
|
1529
1463
|
}
|
|
1530
1464
|
}
|
|
1531
1465
|
|
|
1532
1466
|
/**
|
|
1533
|
-
*
|
|
1467
|
+
* Recreate YouTube player with native controls enabled
|
|
1534
1468
|
*/
|
|
1535
|
-
private
|
|
1536
|
-
if (!this.
|
|
1469
|
+
private recreateYouTubePlayerWithNativeControls(): void {
|
|
1470
|
+
if (!this.source?.metadata?.videoId) return;
|
|
1537
1471
|
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// Verify quality was changed after multiple delays with retry logic
|
|
1605
|
-
this.verifyYouTubeQualityChange(qualityLevel, qualityMap, 0);
|
|
1606
|
-
} catch (error) {
|
|
1607
|
-
this.debugWarn('❌ Could not set YouTube quality:', error);
|
|
1608
|
-
this.showNotification('Quality control limited on YouTube');
|
|
1472
|
+
const videoId = this.source.metadata.videoId;
|
|
1473
|
+
const currentTime = this.youtubePlayer?.getCurrentTime() || 0;
|
|
1474
|
+
|
|
1475
|
+
// Find the container
|
|
1476
|
+
const container = this.playerWrapper || this.video?.parentElement;
|
|
1477
|
+
if (!container) return;
|
|
1478
|
+
|
|
1479
|
+
// Destroy current player
|
|
1480
|
+
if (this.youtubePlayer) {
|
|
1481
|
+
this.youtubePlayer.destroy();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Remove existing iframe container
|
|
1485
|
+
const existingContainer = container.querySelector(`#youtube-player-${videoId}`);
|
|
1486
|
+
if (existingContainer) {
|
|
1487
|
+
existingContainer.remove();
|
|
1609
1488
|
}
|
|
1489
|
+
|
|
1490
|
+
// Create new iframe container
|
|
1491
|
+
const iframeContainer = document.createElement('div');
|
|
1492
|
+
iframeContainer.id = `youtube-player-${videoId}`;
|
|
1493
|
+
iframeContainer.style.cssText = `
|
|
1494
|
+
position: absolute;
|
|
1495
|
+
top: 0;
|
|
1496
|
+
left: 0;
|
|
1497
|
+
width: 100%;
|
|
1498
|
+
height: 100%;
|
|
1499
|
+
z-index: 1;
|
|
1500
|
+
`;
|
|
1501
|
+
container.appendChild(iframeContainer);
|
|
1502
|
+
|
|
1503
|
+
// Create new player with native controls
|
|
1504
|
+
this.youtubePlayer = new window.YT.Player(iframeContainer.id, {
|
|
1505
|
+
videoId: videoId,
|
|
1506
|
+
width: '100%',
|
|
1507
|
+
height: '100%',
|
|
1508
|
+
playerVars: {
|
|
1509
|
+
autoplay: this.config.autoPlay ? 1 : 0,
|
|
1510
|
+
controls: 1, // Enable native controls
|
|
1511
|
+
modestbranding: 1,
|
|
1512
|
+
rel: 0,
|
|
1513
|
+
showinfo: 0,
|
|
1514
|
+
iv_load_policy: 3,
|
|
1515
|
+
playsinline: 1,
|
|
1516
|
+
start: Math.floor(currentTime)
|
|
1517
|
+
},
|
|
1518
|
+
events: {
|
|
1519
|
+
onReady: () => {
|
|
1520
|
+
this.youtubePlayerReady = true;
|
|
1521
|
+
this.debugLog('YouTube player with native controls ready');
|
|
1522
|
+
this.emit('onReady');
|
|
1523
|
+
},
|
|
1524
|
+
onStateChange: (event: any) => this.onYouTubePlayerStateChange(event),
|
|
1525
|
+
onError: (event: any) => this.onYouTubePlayerError(event)
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1610
1528
|
}
|
|
1611
1529
|
|
|
1612
1530
|
/**
|
|
1613
|
-
*
|
|
1531
|
+
* Recreate YouTube player without native controls enabled
|
|
1614
1532
|
*/
|
|
1615
|
-
private
|
|
1616
|
-
|
|
1617
|
-
const delays = [500, 1500, 3000]; // Progressive delays
|
|
1533
|
+
private recreateYouTubePlayerWithoutNativeControls(): void {
|
|
1534
|
+
if (!this.source?.metadata?.videoId) return;
|
|
1618
1535
|
|
|
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
|
-
this.
|
|
1671
|
-
this.
|
|
1672
|
-
|
|
1673
|
-
|
|
1536
|
+
const videoId = this.source.metadata.videoId;
|
|
1537
|
+
const currentTime = this.youtubePlayer?.getCurrentTime() || 0;
|
|
1538
|
+
|
|
1539
|
+
// Find the container
|
|
1540
|
+
const container = this.playerWrapper || this.video?.parentElement;
|
|
1541
|
+
if (!container) return;
|
|
1542
|
+
|
|
1543
|
+
// Destroy current player
|
|
1544
|
+
if (this.youtubePlayer) {
|
|
1545
|
+
this.youtubePlayer.destroy();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// Remove existing iframe container
|
|
1549
|
+
const existingContainer = container.querySelector(`#youtube-player-${videoId}`);
|
|
1550
|
+
if (existingContainer) {
|
|
1551
|
+
existingContainer.remove();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// Create new iframe container
|
|
1555
|
+
const iframeContainer = document.createElement('div');
|
|
1556
|
+
iframeContainer.id = `youtube-player-${videoId}`;
|
|
1557
|
+
iframeContainer.style.cssText = `
|
|
1558
|
+
position: absolute;
|
|
1559
|
+
top: 0;
|
|
1560
|
+
left: 0;
|
|
1561
|
+
width: 100%;
|
|
1562
|
+
height: 100%;
|
|
1563
|
+
z-index: 1;
|
|
1564
|
+
`;
|
|
1565
|
+
container.appendChild(iframeContainer);
|
|
1566
|
+
|
|
1567
|
+
// Create new player without native controls
|
|
1568
|
+
this.youtubePlayer = new window.YT.Player(iframeContainer.id, {
|
|
1569
|
+
videoId: videoId,
|
|
1570
|
+
width: '100%',
|
|
1571
|
+
height: '100%',
|
|
1572
|
+
playerVars: {
|
|
1573
|
+
autoplay: this.config.autoPlay ? 1 : 0,
|
|
1574
|
+
controls: 0, // Disable native controls
|
|
1575
|
+
disablekb: 0,
|
|
1576
|
+
fs: 0,
|
|
1577
|
+
iv_load_policy: 3,
|
|
1578
|
+
modestbranding: 1,
|
|
1579
|
+
rel: 0,
|
|
1580
|
+
showinfo: 0,
|
|
1581
|
+
playsinline: 1,
|
|
1582
|
+
start: Math.floor(currentTime)
|
|
1583
|
+
},
|
|
1584
|
+
events: {
|
|
1585
|
+
onReady: () => {
|
|
1586
|
+
this.youtubePlayerReady = true;
|
|
1587
|
+
this.debugLog('YouTube player without native controls ready');
|
|
1588
|
+
this.emit('onReady');
|
|
1589
|
+
},
|
|
1590
|
+
onStateChange: (event: any) => this.onYouTubePlayerStateChange(event),
|
|
1591
|
+
onError: (event: any) => this.onYouTubePlayerError(event)
|
|
1674
1592
|
}
|
|
1675
|
-
}
|
|
1593
|
+
});
|
|
1676
1594
|
}
|
|
1677
1595
|
|
|
1678
1596
|
/**
|
|
1679
|
-
*
|
|
1597
|
+
* Toggle between native and custom YouTube controls
|
|
1680
1598
|
*/
|
|
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);
|
|
1599
|
+
public toggleYouTubeControls(useNative: boolean = !this.useYouTubeNativeControls): void {
|
|
1600
|
+
if (!this.youtubePlayer) {
|
|
1601
|
+
this.debugWarn('Cannot toggle YouTube controls - no YouTube player active');
|
|
1602
|
+
return;
|
|
1709
1603
|
}
|
|
1604
|
+
|
|
1605
|
+
this.useYouTubeNativeControls = useNative;
|
|
1606
|
+
this.config.youtubeNativeControls = useNative; // Update config
|
|
1607
|
+
this.updateControlsVisibility();
|
|
1608
|
+
|
|
1609
|
+
this.debugLog('YouTube controls toggled:', {
|
|
1610
|
+
useNative: this.useYouTubeNativeControls,
|
|
1611
|
+
isLive: this.isYouTubeLive
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
// Show notification
|
|
1615
|
+
this.showNotification(
|
|
1616
|
+
`YouTube Controls: ${this.useYouTubeNativeControls ? 'Native' : 'Custom'}`
|
|
1617
|
+
);
|
|
1710
1618
|
}
|
|
1711
1619
|
|
|
1712
1620
|
/**
|
|
1713
|
-
*
|
|
1621
|
+
* Fallback method to get title from YouTube oembed API
|
|
1714
1622
|
*/
|
|
1715
|
-
private
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1623
|
+
private async getYouTubeVideoTitleFromOEmbed(): Promise<void> {
|
|
1624
|
+
if (!this.source?.metadata?.videoId) return;
|
|
1625
|
+
|
|
1626
|
+
try {
|
|
1627
|
+
const videoId = this.source.metadata.videoId;
|
|
1628
|
+
const oembedUrl = `https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`;
|
|
1629
|
+
|
|
1630
|
+
const response = await fetch(oembedUrl);
|
|
1631
|
+
if (response.ok) {
|
|
1632
|
+
const data = await response.json();
|
|
1633
|
+
if (data.title) {
|
|
1634
|
+
this.debugLog('Got YouTube title from oembed API:', data.title);
|
|
1635
|
+
|
|
1636
|
+
// Update source metadata
|
|
1637
|
+
if (this.source && this.source.metadata) {
|
|
1638
|
+
this.source.metadata.title = data.title;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Update UI
|
|
1642
|
+
this.updateMetadataUI();
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
this.debugWarn('Could not get YouTube title from oembed API:', error);
|
|
1731
1647
|
}
|
|
1732
1648
|
}
|
|
1733
1649
|
|
|
1650
|
+
private youtubeTimeTrackingInterval: NodeJS.Timeout | null = null;
|
|
1651
|
+
private isYouTubeLive: boolean = false;
|
|
1652
|
+
private useYouTubeNativeControls: boolean = false;
|
|
1653
|
+
|
|
1734
1654
|
private startYouTubeTimeTracking(): void {
|
|
1735
1655
|
if (this.youtubeTimeTrackingInterval) {
|
|
1736
1656
|
clearInterval(this.youtubeTimeTrackingInterval);
|
|
1737
1657
|
}
|
|
1738
1658
|
|
|
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
|
-
}
|
|
1659
|
+
this.youtubeTimeTrackingInterval = setInterval(() => {
|
|
1660
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1661
|
+
try {
|
|
1662
|
+
const currentTime = this.youtubePlayer.getCurrentTime();
|
|
1663
|
+
const duration = this.youtubePlayer.getDuration();
|
|
1664
|
+
const buffered = this.youtubePlayer.getVideoLoadedFraction() * 100;
|
|
1665
|
+
|
|
1666
|
+
this.state.currentTime = currentTime || 0;
|
|
1667
|
+
this.state.duration = duration || 0;
|
|
1668
|
+
this.state.bufferedPercentage = buffered || 0;
|
|
1759
1669
|
|
|
1760
1670
|
// Update UI progress bar
|
|
1761
1671
|
this.updateYouTubeProgressBar(currentTime, duration, buffered);
|
|
@@ -9536,23 +9446,15 @@ export class WebPlayer extends BasePlayer {
|
|
|
9536
9446
|
private setQualityByLabel(quality: string): void {
|
|
9537
9447
|
// Handle YouTube player
|
|
9538
9448
|
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
9539
|
-
//
|
|
9540
|
-
this.
|
|
9541
|
-
|
|
9542
|
-
// Update UI to reflect selection
|
|
9449
|
+
// YouTube quality is limited - show notification
|
|
9450
|
+
this.showShortcutIndicator('Quality control not available for YouTube');
|
|
9451
|
+
// Update UI to reflect current quality
|
|
9543
9452
|
document.querySelectorAll('.quality-option').forEach(option => {
|
|
9544
9453
|
option.classList.remove('active');
|
|
9545
|
-
if ((option as HTMLElement).dataset.quality ===
|
|
9454
|
+
if ((option as HTMLElement).dataset.quality === 'auto') {
|
|
9546
9455
|
option.classList.add('active');
|
|
9547
9456
|
}
|
|
9548
9457
|
});
|
|
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
9458
|
return;
|
|
9557
9459
|
}
|
|
9558
9460
|
|
|
@@ -9853,17 +9755,9 @@ export class WebPlayer extends BasePlayer {
|
|
|
9853
9755
|
|
|
9854
9756
|
// Quality Accordion Section (only if enabled in config and qualities detected)
|
|
9855
9757
|
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);
|
|
9758
|
+
const currentQuality = this.availableQualities.find(q => q.value === this.currentQuality);
|
|
9863
9759
|
const currentQualityLabel = currentQuality ? currentQuality.label : 'Auto';
|
|
9864
9760
|
|
|
9865
|
-
this.debugLog('🎛️ Current quality for display:', qualityForDisplay, '(', currentQualityLabel, ')');
|
|
9866
|
-
|
|
9867
9761
|
menuHTML += `
|
|
9868
9762
|
<div class="uvf-accordion-item">
|
|
9869
9763
|
<div class="uvf-accordion-header" data-section="quality">
|
|
@@ -9881,11 +9775,7 @@ export class WebPlayer extends BasePlayer {
|
|
|
9881
9775
|
<div class="uvf-accordion-content" data-section="quality">`;
|
|
9882
9776
|
|
|
9883
9777
|
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' : '';
|
|
9778
|
+
const isActive = quality.value === this.currentQuality ? 'active' : '';
|
|
9889
9779
|
const isPremium = this.isQualityPremium(quality);
|
|
9890
9780
|
const isLocked = isPremium && !this.isPremiumUser();
|
|
9891
9781
|
const qualityHeight = (quality as any).height || 0;
|
|
@@ -9953,22 +9843,10 @@ export class WebPlayer extends BasePlayer {
|
|
|
9953
9843
|
* Detect available video qualities from different sources
|
|
9954
9844
|
*/
|
|
9955
9845
|
private detectAvailableQualities(): void {
|
|
9956
|
-
|
|
9957
|
-
const isYouTube = this.youtubePlayer && this.youtubePlayerReady && this.youtubeAvailableQualities.length > 0;
|
|
9958
|
-
this.availableQualities = [];
|
|
9846
|
+
this.availableQualities = [{ value: 'auto', label: 'Auto' }];
|
|
9959
9847
|
let detectedQualities: Array<{ value: string; label: string; height?: number }> = [];
|
|
9960
9848
|
|
|
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) {
|
|
9849
|
+
if (this.hls && this.hls.levels) {
|
|
9972
9850
|
// HLS qualities
|
|
9973
9851
|
this.hls.levels.forEach((level: any, index: number) => {
|
|
9974
9852
|
if (level.height) {
|
|
@@ -10340,13 +10218,6 @@ export class WebPlayer extends BasePlayer {
|
|
|
10340
10218
|
private setQualityFromSettings(quality: string): void {
|
|
10341
10219
|
this.currentQuality = quality;
|
|
10342
10220
|
|
|
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
10221
|
if (quality === 'auto') {
|
|
10351
10222
|
// Enable auto quality with filter consideration
|
|
10352
10223
|
if (this.hls) {
|