unified-video-framework 1.4.242 → 1.4.244

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
- // For YouTube, we create a custom stream by using the youtube-nocookie embed
1120
- // and falling back to native HLS if available, or creating a blob stream
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,273 @@ export class WebPlayer extends BasePlayer {
1131
1126
  }
1132
1127
  }
1133
1128
 
1134
- private async loadYouTubeIframe(videoId: string, embedUrl: string): Promise<void> {
1135
- if (!this.video) throw new Error('Video element not initialized');
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
- // Fallback: Use YouTube's native player capabilities
1165
- // Load the video through native support or error
1166
- this.video.src = '';
1167
- this.video.innerHTML = `
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
- private createYouTubeIframeProxy(videoId: string): void {
1192
- // This creates a hidden iframe that plays the YouTube video
1193
- // while our custom player UI controls it (when possible)
1194
- const container = this.playerWrapper || this.video?.parentElement;
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 iframe = document.createElement('iframe');
1199
- iframe.id = `youtube-iframe-${videoId}`;
1200
- iframe.style.cssText = `
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
- container.appendChild(iframe);
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 Iframe API
1164
+ // Load YouTube IFrame API if not loaded
1218
1165
  if (!window.YT) {
1219
- const tag = document.createElement('script');
1220
- tag.src = 'https://www.youtube.com/iframe_api';
1221
- document.body.appendChild(tag);
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.updateYouTubeUI('playing');
1261
+ this.emit('onPlay');
1262
+ break;
1263
+
1264
+ case window.YT.PlayerState.PAUSED:
1265
+ this.state.isPlaying = false;
1266
+ this.state.isPaused = true;
1267
+ this.state.isBuffering = false;
1268
+ this.updateYouTubeUI('paused');
1269
+ this.emit('onPause');
1270
+ break;
1271
+
1272
+ case window.YT.PlayerState.BUFFERING:
1273
+ this.state.isBuffering = true;
1274
+ this.updateYouTubeUI('buffering');
1275
+ this.emit('onBuffering', true);
1276
+ break;
1277
+
1278
+ case window.YT.PlayerState.ENDED:
1279
+ this.state.isPlaying = false;
1280
+ this.state.isPaused = true;
1281
+ this.state.isEnded = true;
1282
+ this.updateYouTubeUI('ended');
1283
+ this.emit('onEnded');
1284
+ break;
1285
+
1286
+ case window.YT.PlayerState.CUED:
1287
+ this.state.duration = this.youtubePlayer.getDuration();
1288
+ this.updateYouTubeUI('cued');
1289
+ break;
1222
1290
  }
1291
+ }
1292
+
1293
+ private updateYouTubeUI(state: string): void {
1294
+ const playIcon = document.getElementById('uvf-play-icon');
1295
+ const pauseIcon = document.getElementById('uvf-pause-icon');
1296
+ const centerPlay = document.getElementById('uvf-center-play');
1297
+
1298
+ if (state === 'playing' || state === 'buffering') {
1299
+ if (playIcon) playIcon.style.display = 'none';
1300
+ if (pauseIcon) pauseIcon.style.display = 'block';
1301
+ if (centerPlay) centerPlay.classList.add('hidden');
1302
+ } else if (state === 'paused' || state === 'cued' || state === 'ended') {
1303
+ if (playIcon) playIcon.style.display = 'block';
1304
+ if (pauseIcon) pauseIcon.style.display = 'none';
1305
+ if (centerPlay) centerPlay.classList.remove('hidden');
1306
+ }
1307
+ }
1308
+
1309
+ private onYouTubePlayerError(event: any): void {
1310
+ const errorCode = event.data;
1311
+ let errorMessage = 'YouTube player error';
1312
+
1313
+ switch (errorCode) {
1314
+ case 2:
1315
+ errorMessage = 'Invalid video ID';
1316
+ break;
1317
+ case 5:
1318
+ errorMessage = 'HTML5 player error';
1319
+ break;
1320
+ case 100:
1321
+ errorMessage = 'Video not found or private';
1322
+ break;
1323
+ case 101:
1324
+ case 150:
1325
+ errorMessage = 'Video cannot be embedded';
1326
+ break;
1327
+ }
1328
+
1329
+ this.handleError({
1330
+ code: 'YOUTUBE_ERROR',
1331
+ message: errorMessage,
1332
+ type: 'media',
1333
+ fatal: true,
1334
+ details: { errorCode }
1335
+ });
1336
+ }
1337
+
1338
+ private youtubeTimeTrackingInterval: NodeJS.Timeout | null = null;
1339
+
1340
+ private startYouTubeTimeTracking(): void {
1341
+ if (this.youtubeTimeTrackingInterval) {
1342
+ clearInterval(this.youtubeTimeTrackingInterval);
1343
+ }
1344
+
1345
+ this.youtubeTimeTrackingInterval = setInterval(() => {
1346
+ if (this.youtubePlayer && this.youtubePlayerReady) {
1347
+ try {
1348
+ const currentTime = this.youtubePlayer.getCurrentTime();
1349
+ const duration = this.youtubePlayer.getDuration();
1350
+ const buffered = this.youtubePlayer.getVideoLoadedFraction() * 100;
1351
+
1352
+ this.state.currentTime = currentTime || 0;
1353
+ this.state.duration = duration || 0;
1354
+ this.state.bufferedPercentage = buffered || 0;
1355
+
1356
+ // Update UI progress bar
1357
+ this.updateYouTubeProgressBar(currentTime, duration, buffered);
1358
+
1359
+ this.emit('onTimeUpdate', this.state.currentTime);
1360
+ this.emit('onProgress', this.state.bufferedPercentage);
1361
+ } catch (error) {
1362
+ // Ignore errors during tracking
1363
+ }
1364
+ }
1365
+ }, 250); // Update every 250ms
1366
+ }
1223
1367
 
1224
- this.debugLog('✅ YouTube iframe proxy created for video:', videoId);
1368
+ private updateYouTubeProgressBar(currentTime: number, duration: number, buffered: number): void {
1369
+ if (!duration || duration === 0) return;
1370
+
1371
+ const percent = (currentTime / duration) * 100;
1372
+
1373
+ // Update progress filled
1374
+ const progressFilled = document.getElementById('uvf-progress-filled') as HTMLElement;
1375
+ if (progressFilled && !this.isDragging) {
1376
+ progressFilled.style.width = percent + '%';
1377
+ }
1378
+
1379
+ // Update progress handle
1380
+ const progressHandle = document.getElementById('uvf-progress-handle') as HTMLElement;
1381
+ if (progressHandle && !this.isDragging) {
1382
+ progressHandle.style.left = percent + '%';
1383
+ }
1384
+
1385
+ // Update buffered progress
1386
+ const progressBuffered = document.getElementById('uvf-progress-buffered') as HTMLElement;
1387
+ if (progressBuffered) {
1388
+ progressBuffered.style.width = buffered + '%';
1389
+ }
1390
+
1391
+ // Update time display
1392
+ this.updateTimeDisplay();
1225
1393
  }
1226
1394
 
1395
+
1227
1396
  protected loadScript(src: string): Promise<void> {
1228
1397
  return new Promise((resolve, reject) => {
1229
1398
  const script = document.createElement('script');
@@ -1708,6 +1877,12 @@ export class WebPlayer extends BasePlayer {
1708
1877
  }
1709
1878
 
1710
1879
  async play(): Promise<void> {
1880
+ // Handle YouTube player
1881
+ if (this.youtubePlayer && this.youtubePlayerReady) {
1882
+ this.youtubePlayer.playVideo();
1883
+ return;
1884
+ }
1885
+
1711
1886
  if (!this.video) throw new Error('Video element not initialized');
1712
1887
 
1713
1888
  // Prevent playback when in fallback poster mode (no valid sources)
@@ -1773,6 +1948,12 @@ export class WebPlayer extends BasePlayer {
1773
1948
  }
1774
1949
 
1775
1950
  pause(): void {
1951
+ // Handle YouTube player
1952
+ if (this.youtubePlayer && this.youtubePlayerReady) {
1953
+ this.youtubePlayer.pauseVideo();
1954
+ return;
1955
+ }
1956
+
1776
1957
  if (!this.video) return;
1777
1958
 
1778
1959
  const now = Date.now();
@@ -1840,6 +2021,14 @@ export class WebPlayer extends BasePlayer {
1840
2021
  }
1841
2022
 
1842
2023
  seek(time: number): void {
2024
+ // Handle YouTube player
2025
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2026
+ this.youtubePlayer.seekTo(time, true);
2027
+ this.emit('onSeeking');
2028
+ setTimeout(() => this.emit('onSeeked'), 500);
2029
+ return;
2030
+ }
2031
+
1843
2032
  if (!this.video) return;
1844
2033
 
1845
2034
  // Validate input time
@@ -1866,18 +2055,34 @@ export class WebPlayer extends BasePlayer {
1866
2055
  }
1867
2056
 
1868
2057
  setVolume(level: number): void {
2058
+ // Handle YouTube player
2059
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2060
+ const volumePercent = level * 100;
2061
+ this.youtubePlayer.setVolume(volumePercent);
2062
+ }
2063
+
1869
2064
  if (!this.video) return;
1870
2065
  this.video.volume = Math.max(0, Math.min(1, level));
1871
2066
  super.setVolume(level);
1872
2067
  }
1873
2068
 
1874
2069
  mute(): void {
2070
+ // Handle YouTube player
2071
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2072
+ this.youtubePlayer.mute();
2073
+ }
2074
+
1875
2075
  if (!this.video) return;
1876
2076
  this.video.muted = true;
1877
2077
  super.mute();
1878
2078
  }
1879
2079
 
1880
2080
  unmute(): void {
2081
+ // Handle YouTube player
2082
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2083
+ this.youtubePlayer.unMute();
2084
+ }
2085
+
1881
2086
  if (!this.video) return;
1882
2087
  this.video.muted = false;
1883
2088
  super.unmute();
@@ -1885,6 +2090,11 @@ export class WebPlayer extends BasePlayer {
1885
2090
 
1886
2091
 
1887
2092
  getCurrentTime(): number {
2093
+ // Handle YouTube player
2094
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2095
+ return this.youtubePlayer.getCurrentTime();
2096
+ }
2097
+
1888
2098
  if (this.video && typeof this.video.currentTime === 'number') {
1889
2099
  return this.video.currentTime;
1890
2100
  }
@@ -6591,6 +6801,9 @@ export class WebPlayer extends BasePlayer {
6591
6801
  const fullscreenBtn = document.getElementById('uvf-fullscreen-btn');
6592
6802
  const settingsBtn = document.getElementById('uvf-settings-btn');
6593
6803
 
6804
+ // Get the event target (video element or YouTube player)
6805
+ const getEventTarget = () => this.youtubePlayer && this.youtubePlayerReady ? this.youtubePlayer : this.video;
6806
+
6594
6807
  // Disable right-click context menu
6595
6808
  this.video.addEventListener('contextmenu', (e) => {
6596
6809
  e.preventDefault();