unified-video-framework 1.4.242 → 1.4.243
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.
|
@@ -1116,13 +1116,8 @@ export class WebPlayer extends BasePlayer {
|
|
|
1116
1116
|
this.video.poster = metadata.thumbnail;
|
|
1117
1117
|
}
|
|
1118
1118
|
|
|
1119
|
-
//
|
|
1120
|
-
|
|
1121
|
-
const embedUrl = YouTubeExtractor.getEmbedUrl(videoId);
|
|
1122
|
-
|
|
1123
|
-
// Use iframe approach - this is the most reliable method
|
|
1124
|
-
// We'll load the video through a special method that extracts the stream
|
|
1125
|
-
await this.loadYouTubeIframe(videoId, embedUrl);
|
|
1119
|
+
// Create YouTube iframe player with custom controls integration
|
|
1120
|
+
await this.createYouTubePlayer(videoId);
|
|
1126
1121
|
|
|
1127
1122
|
this.debugLog('✅ YouTube video loaded successfully');
|
|
1128
1123
|
} catch (error) {
|
|
@@ -1131,99 +1126,222 @@ export class WebPlayer extends BasePlayer {
|
|
|
1131
1126
|
}
|
|
1132
1127
|
}
|
|
1133
1128
|
|
|
1134
|
-
private
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
try {
|
|
1138
|
-
// First, try to load using YouTube's nocookie embed with special handling
|
|
1139
|
-
// This approach uses a proxy service to get direct MP4 URLs
|
|
1140
|
-
const proxyUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
1141
|
-
|
|
1142
|
-
// Attempt 1: Try using a CORS proxy to fetch the page and extract HLS URL
|
|
1143
|
-
try {
|
|
1144
|
-
const response = await fetch(`https://cors-anywhere.herokuapp.com/${proxyUrl}`, {
|
|
1145
|
-
method: 'GET',
|
|
1146
|
-
headers: {
|
|
1147
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
|
1148
|
-
}
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
if (response.ok) {
|
|
1152
|
-
const html = await response.text();
|
|
1153
|
-
// Try to extract streamingData from the HTML
|
|
1154
|
-
const match = html.match(/"streamingData"\s*:\s*\{([^}]*"url"[^}]*)/);
|
|
1155
|
-
if (match) {
|
|
1156
|
-
this.debugLog('✅ Found HLS stream in YouTube page');
|
|
1157
|
-
// Continue with fallback
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
} catch (proxyError) {
|
|
1161
|
-
this.debugWarn('CORS proxy attempt failed, trying fallback:', proxyError);
|
|
1162
|
-
}
|
|
1129
|
+
private youtubePlayer: any = null;
|
|
1130
|
+
private youtubePlayerReady: boolean = false;
|
|
1131
|
+
private youtubeIframe: HTMLIFrameElement | null = null;
|
|
1163
1132
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
<source
|
|
1169
|
-
src="https://www.youtube.com/embed/${videoId}?fs=1"
|
|
1170
|
-
type="text/html"
|
|
1171
|
-
/>
|
|
1172
|
-
`;
|
|
1173
|
-
|
|
1174
|
-
// Alternative: Create iframe overlay and sync with player
|
|
1175
|
-
this.createYouTubeIframeProxy(videoId);
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
this.debugWarn('YouTube iframe loading fallback:', error);
|
|
1178
|
-
// Final fallback: Use the standard YouTube URL (won't work with custom controls)
|
|
1179
|
-
// but at least something will play
|
|
1180
|
-
if (this.video) {
|
|
1181
|
-
// For the custom player UI to work, we need actual video stream
|
|
1182
|
-
// If neither method works, inform user
|
|
1183
|
-
throw new Error(
|
|
1184
|
-
'YouTube embedding requires either: 1) Backend service to extract streams, or 2) YouTube API key configuration. ' +
|
|
1185
|
-
'See documentation for setup instructions.'
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1133
|
+
private async createYouTubePlayer(videoId: string): Promise<void> {
|
|
1134
|
+
const container = this.playerWrapper || this.video?.parentElement;
|
|
1135
|
+
if (!container) {
|
|
1136
|
+
throw new Error('No container found for YouTube player');
|
|
1188
1137
|
}
|
|
1189
|
-
}
|
|
1190
1138
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
if (!container) return;
|
|
1139
|
+
// Hide the regular video element
|
|
1140
|
+
if (this.video) {
|
|
1141
|
+
this.video.style.display = 'none';
|
|
1142
|
+
}
|
|
1196
1143
|
|
|
1197
|
-
// Create iframe
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1144
|
+
// Create iframe container
|
|
1145
|
+
const iframeContainer = document.createElement('div');
|
|
1146
|
+
iframeContainer.id = `youtube-player-${videoId}`;
|
|
1147
|
+
iframeContainer.style.cssText = `
|
|
1201
1148
|
position: absolute;
|
|
1202
1149
|
top: 0;
|
|
1203
1150
|
left: 0;
|
|
1204
1151
|
width: 100%;
|
|
1205
1152
|
height: 100%;
|
|
1206
|
-
border: none;
|
|
1207
1153
|
z-index: 1;
|
|
1208
|
-
opacity: 0;
|
|
1209
|
-
pointer-events: none;
|
|
1210
1154
|
`;
|
|
1211
|
-
|
|
1212
|
-
iframe.src = `https://www.youtube.com/embed/${videoId}?enablejsapi=1&modestbranding=1&rel=0&controls=0`;
|
|
1213
|
-
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
|
|
1214
1155
|
|
|
1215
|
-
|
|
1156
|
+
// Remove existing YouTube player if any
|
|
1157
|
+
const existingPlayer = container.querySelector(`#youtube-player-${videoId}`);
|
|
1158
|
+
if (existingPlayer) {
|
|
1159
|
+
existingPlayer.remove();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
container.appendChild(iframeContainer);
|
|
1216
1163
|
|
|
1217
|
-
// Load YouTube
|
|
1164
|
+
// Load YouTube IFrame API if not loaded
|
|
1218
1165
|
if (!window.YT) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1166
|
+
await this.loadYouTubeAPI();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Wait for API to be ready
|
|
1170
|
+
await this.waitForYouTubeAPI();
|
|
1171
|
+
|
|
1172
|
+
// Create YouTube player
|
|
1173
|
+
this.youtubePlayer = new window.YT.Player(iframeContainer.id, {
|
|
1174
|
+
videoId: videoId,
|
|
1175
|
+
width: '100%',
|
|
1176
|
+
height: '100%',
|
|
1177
|
+
playerVars: {
|
|
1178
|
+
controls: 0, // Hide YouTube controls
|
|
1179
|
+
disablekb: 1, // Disable keyboard controls
|
|
1180
|
+
fs: 0, // Hide fullscreen button
|
|
1181
|
+
iv_load_policy: 3, // Hide annotations
|
|
1182
|
+
modestbranding: 1, // Minimal YouTube branding
|
|
1183
|
+
rel: 0, // Don't show related videos
|
|
1184
|
+
showinfo: 0, // Hide video info
|
|
1185
|
+
autoplay: this.config.autoPlay ? 1 : 0,
|
|
1186
|
+
mute: this.config.muted ? 1 : 0
|
|
1187
|
+
},
|
|
1188
|
+
events: {
|
|
1189
|
+
onReady: () => this.onYouTubePlayerReady(),
|
|
1190
|
+
onStateChange: (event: any) => this.onYouTubePlayerStateChange(event),
|
|
1191
|
+
onError: (event: any) => this.onYouTubePlayerError(event)
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
this.debugLog('YouTube player created');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private async loadYouTubeAPI(): Promise<void> {
|
|
1199
|
+
return new Promise((resolve) => {
|
|
1200
|
+
if (window.YT) {
|
|
1201
|
+
resolve();
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Set up the callback for when API loads
|
|
1206
|
+
(window as any).onYouTubeIframeAPIReady = () => {
|
|
1207
|
+
this.debugLog('YouTube IFrame API loaded');
|
|
1208
|
+
resolve();
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
// Load the API script
|
|
1212
|
+
const script = document.createElement('script');
|
|
1213
|
+
script.src = 'https://www.youtube.com/iframe_api';
|
|
1214
|
+
script.async = true;
|
|
1215
|
+
document.body.appendChild(script);
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private async waitForYouTubeAPI(): Promise<void> {
|
|
1220
|
+
return new Promise((resolve) => {
|
|
1221
|
+
const checkAPI = () => {
|
|
1222
|
+
if (window.YT && window.YT.Player) {
|
|
1223
|
+
resolve();
|
|
1224
|
+
} else {
|
|
1225
|
+
setTimeout(checkAPI, 100);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
checkAPI();
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private onYouTubePlayerReady(): void {
|
|
1233
|
+
this.youtubePlayerReady = true;
|
|
1234
|
+
this.debugLog('YouTube player ready');
|
|
1235
|
+
|
|
1236
|
+
// Set initial volume
|
|
1237
|
+
if (this.youtubePlayer) {
|
|
1238
|
+
const volume = this.config.volume ? this.config.volume * 100 : 100;
|
|
1239
|
+
this.youtubePlayer.setVolume(volume);
|
|
1240
|
+
|
|
1241
|
+
if (this.config.muted) {
|
|
1242
|
+
this.youtubePlayer.mute();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Start time tracking
|
|
1247
|
+
this.startYouTubeTimeTracking();
|
|
1248
|
+
|
|
1249
|
+
this.emit('onReady');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private onYouTubePlayerStateChange(event: any): void {
|
|
1253
|
+
const state = event.data;
|
|
1254
|
+
|
|
1255
|
+
switch (state) {
|
|
1256
|
+
case window.YT.PlayerState.PLAYING:
|
|
1257
|
+
this.state.isPlaying = true;
|
|
1258
|
+
this.state.isPaused = false;
|
|
1259
|
+
this.state.isBuffering = false;
|
|
1260
|
+
this.emit('onPlay');
|
|
1261
|
+
break;
|
|
1262
|
+
|
|
1263
|
+
case window.YT.PlayerState.PAUSED:
|
|
1264
|
+
this.state.isPlaying = false;
|
|
1265
|
+
this.state.isPaused = true;
|
|
1266
|
+
this.state.isBuffering = false;
|
|
1267
|
+
this.emit('onPause');
|
|
1268
|
+
break;
|
|
1269
|
+
|
|
1270
|
+
case window.YT.PlayerState.BUFFERING:
|
|
1271
|
+
this.state.isBuffering = true;
|
|
1272
|
+
this.emit('onBuffering', true);
|
|
1273
|
+
break;
|
|
1274
|
+
|
|
1275
|
+
case window.YT.PlayerState.ENDED:
|
|
1276
|
+
this.state.isPlaying = false;
|
|
1277
|
+
this.state.isPaused = true;
|
|
1278
|
+
this.state.isEnded = true;
|
|
1279
|
+
this.emit('onEnded');
|
|
1280
|
+
break;
|
|
1281
|
+
|
|
1282
|
+
case window.YT.PlayerState.CUED:
|
|
1283
|
+
this.state.duration = this.youtubePlayer.getDuration();
|
|
1284
|
+
break;
|
|
1222
1285
|
}
|
|
1286
|
+
}
|
|
1223
1287
|
|
|
1224
|
-
|
|
1288
|
+
private onYouTubePlayerError(event: any): void {
|
|
1289
|
+
const errorCode = event.data;
|
|
1290
|
+
let errorMessage = 'YouTube player error';
|
|
1291
|
+
|
|
1292
|
+
switch (errorCode) {
|
|
1293
|
+
case 2:
|
|
1294
|
+
errorMessage = 'Invalid video ID';
|
|
1295
|
+
break;
|
|
1296
|
+
case 5:
|
|
1297
|
+
errorMessage = 'HTML5 player error';
|
|
1298
|
+
break;
|
|
1299
|
+
case 100:
|
|
1300
|
+
errorMessage = 'Video not found or private';
|
|
1301
|
+
break;
|
|
1302
|
+
case 101:
|
|
1303
|
+
case 150:
|
|
1304
|
+
errorMessage = 'Video cannot be embedded';
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
this.handleError({
|
|
1309
|
+
code: 'YOUTUBE_ERROR',
|
|
1310
|
+
message: errorMessage,
|
|
1311
|
+
type: 'media',
|
|
1312
|
+
fatal: true,
|
|
1313
|
+
details: { errorCode }
|
|
1314
|
+
});
|
|
1225
1315
|
}
|
|
1226
1316
|
|
|
1317
|
+
private youtubeTimeTrackingInterval: NodeJS.Timeout | null = null;
|
|
1318
|
+
|
|
1319
|
+
private startYouTubeTimeTracking(): void {
|
|
1320
|
+
if (this.youtubeTimeTrackingInterval) {
|
|
1321
|
+
clearInterval(this.youtubeTimeTrackingInterval);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
this.youtubeTimeTrackingInterval = setInterval(() => {
|
|
1325
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1326
|
+
try {
|
|
1327
|
+
const currentTime = this.youtubePlayer.getCurrentTime();
|
|
1328
|
+
const duration = this.youtubePlayer.getDuration();
|
|
1329
|
+
const buffered = this.youtubePlayer.getVideoLoadedFraction() * 100;
|
|
1330
|
+
|
|
1331
|
+
this.state.currentTime = currentTime || 0;
|
|
1332
|
+
this.state.duration = duration || 0;
|
|
1333
|
+
this.state.bufferedPercentage = buffered || 0;
|
|
1334
|
+
|
|
1335
|
+
this.emit('onTimeUpdate', this.state.currentTime);
|
|
1336
|
+
this.emit('onProgress', this.state.bufferedPercentage);
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
// Ignore errors during tracking
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}, 250); // Update every 250ms
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
|
|
1227
1345
|
protected loadScript(src: string): Promise<void> {
|
|
1228
1346
|
return new Promise((resolve, reject) => {
|
|
1229
1347
|
const script = document.createElement('script');
|
|
@@ -1708,6 +1826,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
1708
1826
|
}
|
|
1709
1827
|
|
|
1710
1828
|
async play(): Promise<void> {
|
|
1829
|
+
// Handle YouTube player
|
|
1830
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1831
|
+
this.youtubePlayer.playVideo();
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1711
1835
|
if (!this.video) throw new Error('Video element not initialized');
|
|
1712
1836
|
|
|
1713
1837
|
// Prevent playback when in fallback poster mode (no valid sources)
|
|
@@ -1773,6 +1897,12 @@ export class WebPlayer extends BasePlayer {
|
|
|
1773
1897
|
}
|
|
1774
1898
|
|
|
1775
1899
|
pause(): void {
|
|
1900
|
+
// Handle YouTube player
|
|
1901
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1902
|
+
this.youtubePlayer.pauseVideo();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1776
1906
|
if (!this.video) return;
|
|
1777
1907
|
|
|
1778
1908
|
const now = Date.now();
|
|
@@ -1840,6 +1970,14 @@ export class WebPlayer extends BasePlayer {
|
|
|
1840
1970
|
}
|
|
1841
1971
|
|
|
1842
1972
|
seek(time: number): void {
|
|
1973
|
+
// Handle YouTube player
|
|
1974
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
1975
|
+
this.youtubePlayer.seekTo(time, true);
|
|
1976
|
+
this.emit('onSeeking');
|
|
1977
|
+
setTimeout(() => this.emit('onSeeked'), 500);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1843
1981
|
if (!this.video) return;
|
|
1844
1982
|
|
|
1845
1983
|
// Validate input time
|
|
@@ -1866,18 +2004,34 @@ export class WebPlayer extends BasePlayer {
|
|
|
1866
2004
|
}
|
|
1867
2005
|
|
|
1868
2006
|
setVolume(level: number): void {
|
|
2007
|
+
// Handle YouTube player
|
|
2008
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2009
|
+
const volumePercent = level * 100;
|
|
2010
|
+
this.youtubePlayer.setVolume(volumePercent);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1869
2013
|
if (!this.video) return;
|
|
1870
2014
|
this.video.volume = Math.max(0, Math.min(1, level));
|
|
1871
2015
|
super.setVolume(level);
|
|
1872
2016
|
}
|
|
1873
2017
|
|
|
1874
2018
|
mute(): void {
|
|
2019
|
+
// Handle YouTube player
|
|
2020
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2021
|
+
this.youtubePlayer.mute();
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1875
2024
|
if (!this.video) return;
|
|
1876
2025
|
this.video.muted = true;
|
|
1877
2026
|
super.mute();
|
|
1878
2027
|
}
|
|
1879
2028
|
|
|
1880
2029
|
unmute(): void {
|
|
2030
|
+
// Handle YouTube player
|
|
2031
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2032
|
+
this.youtubePlayer.unMute();
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1881
2035
|
if (!this.video) return;
|
|
1882
2036
|
this.video.muted = false;
|
|
1883
2037
|
super.unmute();
|
|
@@ -1885,6 +2039,11 @@ export class WebPlayer extends BasePlayer {
|
|
|
1885
2039
|
|
|
1886
2040
|
|
|
1887
2041
|
getCurrentTime(): number {
|
|
2042
|
+
// Handle YouTube player
|
|
2043
|
+
if (this.youtubePlayer && this.youtubePlayerReady) {
|
|
2044
|
+
return this.youtubePlayer.getCurrentTime();
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1888
2047
|
if (this.video && typeof this.video.currentTime === 'number') {
|
|
1889
2048
|
return this.video.currentTime;
|
|
1890
2049
|
}
|