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
- // 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,222 @@ 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.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
- this.debugLog('✅ YouTube iframe proxy created for video:', videoId);
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
  }