unified-video-framework 1.4.241 → 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.
@@ -0,0 +1,38 @@
1
+ export interface YouTubeVideoInfo {
2
+ videoId: string;
3
+ title: string;
4
+ duration: number;
5
+ thumbnail: string;
6
+ streamUrl: string;
7
+ format: 'mp4' | 'webm';
8
+ }
9
+ export declare class YouTubeExtractor {
10
+ private static readonly YOUTUBE_REGEX;
11
+ private static readonly YOUTUBE_NOEMBED_API;
12
+ private static readonly YOUTUBE_API_ENDPOINT;
13
+ static isYouTubeUrl(url: string): boolean;
14
+ static extractVideoId(url: string): string | null;
15
+ static getVideoMetadata(url: string): Promise<{
16
+ title: string;
17
+ thumbnail: string;
18
+ duration?: number;
19
+ }>;
20
+ static getEmbedUrl(videoId: string): string;
21
+ static getDirectStreamUrl(videoId: string, backendEndpoint?: string): Promise<string | null>;
22
+ private static getFallbackStreamUrl;
23
+ static prepareYouTubeSource(url: string, backendEndpoint?: string): Promise<{
24
+ url: string;
25
+ type: string;
26
+ title: string;
27
+ thumbnail: string;
28
+ duration: number | undefined;
29
+ videoId: string;
30
+ isYouTube: boolean;
31
+ metadata: {
32
+ source: string;
33
+ videoId: string;
34
+ };
35
+ }>;
36
+ }
37
+ export default YouTubeExtractor;
38
+ //# sourceMappingURL=YouTubeExtractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YouTubeExtractor.d.ts","sourceRoot":"","sources":["../../src/utils/YouTubeExtractor.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;CACxB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAyG;IAC9I,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAoC;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAyC;IAKrF,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAOzC,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;WASpC,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAClD,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IAiCF,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;WAa9B,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA+BlG,OAAO,CAAC,MAAM,CAAC,oBAAoB;WAUtB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM;;;;;;;;;;;;;CA2BxE;AAED,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,89 @@
1
+ export class YouTubeExtractor {
2
+ static isYouTubeUrl(url) {
3
+ return this.YOUTUBE_REGEX.test(url);
4
+ }
5
+ static extractVideoId(url) {
6
+ const match = url.match(this.YOUTUBE_REGEX);
7
+ return match ? match[1] : null;
8
+ }
9
+ static async getVideoMetadata(url) {
10
+ try {
11
+ const apiUrl = `${this.YOUTUBE_NOEMBED_API}${encodeURIComponent(url)}`;
12
+ const response = await fetch(apiUrl);
13
+ if (!response.ok) {
14
+ throw new Error('Failed to fetch YouTube metadata');
15
+ }
16
+ const data = await response.json();
17
+ return {
18
+ title: data.title || 'YouTube Video',
19
+ thumbnail: data.thumbnail_url || `https://img.youtube.com/vi/${this.extractVideoId(url)}/maxresdefault.jpg`,
20
+ duration: data.duration || undefined
21
+ };
22
+ }
23
+ catch (error) {
24
+ console.warn('Failed to fetch YouTube metadata:', error);
25
+ const videoId = this.extractVideoId(url);
26
+ return {
27
+ title: 'YouTube Video',
28
+ thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
29
+ duration: undefined
30
+ };
31
+ }
32
+ }
33
+ static getEmbedUrl(videoId) {
34
+ return `https://www.youtube.com/embed/${videoId}?modestbranding=1&rel=0&controls=0`;
35
+ }
36
+ static async getDirectStreamUrl(videoId, backendEndpoint) {
37
+ if (!backendEndpoint) {
38
+ console.warn('No backend endpoint provided for YouTube video extraction. Using fallback method.');
39
+ return this.getFallbackStreamUrl(videoId);
40
+ }
41
+ try {
42
+ const response = await fetch(backendEndpoint, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json'
46
+ },
47
+ body: JSON.stringify({ videoId })
48
+ });
49
+ if (!response.ok) {
50
+ throw new Error('Backend failed to extract stream');
51
+ }
52
+ const data = await response.json();
53
+ return data.streamUrl || null;
54
+ }
55
+ catch (error) {
56
+ console.error('Failed to get direct stream URL:', error);
57
+ return this.getFallbackStreamUrl(videoId);
58
+ }
59
+ }
60
+ static getFallbackStreamUrl(videoId) {
61
+ return `https://www.youtube.com/watch?v=${videoId}`;
62
+ }
63
+ static async prepareYouTubeSource(url, backendEndpoint) {
64
+ const videoId = this.extractVideoId(url);
65
+ if (!videoId) {
66
+ throw new Error('Invalid YouTube URL');
67
+ }
68
+ const metadata = await this.getVideoMetadata(url);
69
+ const streamUrl = await this.getDirectStreamUrl(videoId, backendEndpoint);
70
+ return {
71
+ url: streamUrl || url,
72
+ type: 'youtube',
73
+ title: metadata.title,
74
+ thumbnail: metadata.thumbnail,
75
+ duration: metadata.duration,
76
+ videoId: videoId,
77
+ isYouTube: true,
78
+ metadata: {
79
+ source: 'youtube',
80
+ videoId: videoId
81
+ }
82
+ };
83
+ }
84
+ }
85
+ YouTubeExtractor.YOUTUBE_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/)?([a-zA-Z0-9_-]{11})/;
86
+ YouTubeExtractor.YOUTUBE_NOEMBED_API = 'https://noembed.com/embed?url=';
87
+ YouTubeExtractor.YOUTUBE_API_ENDPOINT = 'https://www.youtube.com/oembed?url=';
88
+ export default YouTubeExtractor;
89
+ //# sourceMappingURL=YouTubeExtractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"YouTubeExtractor.js","sourceRoot":"","sources":["../../src/utils/YouTubeExtractor.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import {
20
20
  VideoSegment,
21
21
  ChapterEvents
22
22
  } from './chapters/types/ChapterTypes';
23
+ import YouTubeExtractor from './utils/YouTubeExtractor';
23
24
 
24
25
  // Dynamic imports for streaming libraries
25
26
  declare global {
@@ -28,6 +29,7 @@ declare global {
28
29
  dashjs: any;
29
30
  cast?: any;
30
31
  chrome?: any;
32
+ YT?: any;
31
33
  __onGCastApiAvailable?: (isAvailable: boolean) => void;
32
34
  }
33
35
  }
@@ -680,6 +682,9 @@ export class WebPlayer extends BasePlayer {
680
682
  case 'dash':
681
683
  await this.loadDASH(url);
682
684
  break;
685
+ case 'youtube':
686
+ await this.loadYouTube(url, source);
687
+ break;
683
688
  default:
684
689
  await this.loadNative(url);
685
690
  }
@@ -869,7 +874,7 @@ export class WebPlayer extends BasePlayer {
869
874
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
870
875
  <rect x="10" y="16" width="20" height="16" rx="2" />
871
876
  <polygon points="34,20 42,16 42,32 34,28" />
872
- <line x1="10" y1="16" x2="38" y2="40" stroke="currentColor" stroke-width="2"/>
877
+ <line x1="5" y1="8" x2="38" y2="40" stroke="currentColor" stroke-width="2"/>
873
878
  </svg>
874
879
  <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">Video Unavailable</div>
875
880
  <div style="font-size: 14px; opacity: 0.9;">This video cannot be played at the moment.</div>
@@ -899,6 +904,12 @@ export class WebPlayer extends BasePlayer {
899
904
  }
900
905
 
901
906
  const url = source.url.toLowerCase();
907
+
908
+ // Check for YouTube URLs
909
+ if (YouTubeExtractor.isYouTubeUrl(url)) {
910
+ return 'youtube';
911
+ }
912
+
902
913
  if (url.includes('.m3u8')) return 'hls';
903
914
  if (url.includes('.mpd')) return 'dash';
904
915
  if (url.includes('.mp4')) return 'mp4';
@@ -1072,6 +1083,265 @@ export class WebPlayer extends BasePlayer {
1072
1083
  this.video.load();
1073
1084
  }
1074
1085
 
1086
+ private async loadYouTube(url: string, source: any): Promise<void> {
1087
+ try {
1088
+ this.debugLog('Loading YouTube video:', url);
1089
+
1090
+ // Extract video ID and fetch metadata
1091
+ const videoId = YouTubeExtractor.extractVideoId(url);
1092
+ if (!videoId) {
1093
+ throw new Error('Invalid YouTube URL');
1094
+ }
1095
+
1096
+ // Fetch YouTube metadata (title, thumbnail)
1097
+ const metadata = await YouTubeExtractor.getVideoMetadata(url);
1098
+
1099
+ // Store metadata for later use
1100
+ this.source = {
1101
+ url: source.url || url,
1102
+ ...this.source,
1103
+ metadata: {
1104
+ ...source.metadata,
1105
+ title: metadata.title,
1106
+ thumbnail: metadata.thumbnail,
1107
+ duration: metadata.duration,
1108
+ source: 'youtube',
1109
+ videoId: videoId,
1110
+ posterUrl: metadata.thumbnail
1111
+ }
1112
+ };
1113
+
1114
+ // Update player poster with thumbnail
1115
+ if (this.video && metadata.thumbnail) {
1116
+ this.video.poster = metadata.thumbnail;
1117
+ }
1118
+
1119
+ // Create YouTube iframe player with custom controls integration
1120
+ await this.createYouTubePlayer(videoId);
1121
+
1122
+ this.debugLog('✅ YouTube video loaded successfully');
1123
+ } catch (error) {
1124
+ this.debugError('Failed to load YouTube video:', error);
1125
+ throw new Error(`YouTube video loading failed: ${error}`);
1126
+ }
1127
+ }
1128
+
1129
+ private youtubePlayer: any = null;
1130
+ private youtubePlayerReady: boolean = false;
1131
+ private youtubeIframe: HTMLIFrameElement | null = null;
1132
+
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');
1137
+ }
1138
+
1139
+ // Hide the regular video element
1140
+ if (this.video) {
1141
+ this.video.style.display = 'none';
1142
+ }
1143
+
1144
+ // Create iframe container
1145
+ const iframeContainer = document.createElement('div');
1146
+ iframeContainer.id = `youtube-player-${videoId}`;
1147
+ iframeContainer.style.cssText = `
1148
+ position: absolute;
1149
+ top: 0;
1150
+ left: 0;
1151
+ width: 100%;
1152
+ height: 100%;
1153
+ z-index: 1;
1154
+ `;
1155
+
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);
1163
+
1164
+ // Load YouTube IFrame API if not loaded
1165
+ if (!window.YT) {
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;
1285
+ }
1286
+ }
1287
+
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
+ });
1315
+ }
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
+
1075
1345
  protected loadScript(src: string): Promise<void> {
1076
1346
  return new Promise((resolve, reject) => {
1077
1347
  const script = document.createElement('script');
@@ -1556,6 +1826,12 @@ export class WebPlayer extends BasePlayer {
1556
1826
  }
1557
1827
 
1558
1828
  async play(): Promise<void> {
1829
+ // Handle YouTube player
1830
+ if (this.youtubePlayer && this.youtubePlayerReady) {
1831
+ this.youtubePlayer.playVideo();
1832
+ return;
1833
+ }
1834
+
1559
1835
  if (!this.video) throw new Error('Video element not initialized');
1560
1836
 
1561
1837
  // Prevent playback when in fallback poster mode (no valid sources)
@@ -1621,6 +1897,12 @@ export class WebPlayer extends BasePlayer {
1621
1897
  }
1622
1898
 
1623
1899
  pause(): void {
1900
+ // Handle YouTube player
1901
+ if (this.youtubePlayer && this.youtubePlayerReady) {
1902
+ this.youtubePlayer.pauseVideo();
1903
+ return;
1904
+ }
1905
+
1624
1906
  if (!this.video) return;
1625
1907
 
1626
1908
  const now = Date.now();
@@ -1688,6 +1970,14 @@ export class WebPlayer extends BasePlayer {
1688
1970
  }
1689
1971
 
1690
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
+
1691
1981
  if (!this.video) return;
1692
1982
 
1693
1983
  // Validate input time
@@ -1714,18 +2004,34 @@ export class WebPlayer extends BasePlayer {
1714
2004
  }
1715
2005
 
1716
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
+
1717
2013
  if (!this.video) return;
1718
2014
  this.video.volume = Math.max(0, Math.min(1, level));
1719
2015
  super.setVolume(level);
1720
2016
  }
1721
2017
 
1722
2018
  mute(): void {
2019
+ // Handle YouTube player
2020
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2021
+ this.youtubePlayer.mute();
2022
+ }
2023
+
1723
2024
  if (!this.video) return;
1724
2025
  this.video.muted = true;
1725
2026
  super.mute();
1726
2027
  }
1727
2028
 
1728
2029
  unmute(): void {
2030
+ // Handle YouTube player
2031
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2032
+ this.youtubePlayer.unMute();
2033
+ }
2034
+
1729
2035
  if (!this.video) return;
1730
2036
  this.video.muted = false;
1731
2037
  super.unmute();
@@ -1733,6 +2039,11 @@ export class WebPlayer extends BasePlayer {
1733
2039
 
1734
2040
 
1735
2041
  getCurrentTime(): number {
2042
+ // Handle YouTube player
2043
+ if (this.youtubePlayer && this.youtubePlayerReady) {
2044
+ return this.youtubePlayer.getCurrentTime();
2045
+ }
2046
+
1736
2047
  if (this.video && typeof this.video.currentTime === 'number') {
1737
2048
  return this.video.currentTime;
1738
2049
  }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * YouTube Video Extractor and Stream Fetcher
3
+ * Handles YouTube URL detection, video ID extraction, and fetches direct video streams
4
+ */
5
+
6
+ export interface YouTubeVideoInfo {
7
+ videoId: string;
8
+ title: string;
9
+ duration: number;
10
+ thumbnail: string;
11
+ streamUrl: string;
12
+ format: 'mp4' | 'webm';
13
+ }
14
+
15
+ export class YouTubeExtractor {
16
+ private static readonly YOUTUBE_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/)?([a-zA-Z0-9_-]{11})/;
17
+ private static readonly YOUTUBE_NOEMBED_API = 'https://noembed.com/embed?url=';
18
+ private static readonly YOUTUBE_API_ENDPOINT = 'https://www.youtube.com/oembed?url=';
19
+
20
+ /**
21
+ * Detect if URL is a valid YouTube URL
22
+ */
23
+ static isYouTubeUrl(url: string): boolean {
24
+ return this.YOUTUBE_REGEX.test(url);
25
+ }
26
+
27
+ /**
28
+ * Extract video ID from YouTube URL
29
+ */
30
+ static extractVideoId(url: string): string | null {
31
+ const match = url.match(this.YOUTUBE_REGEX);
32
+ return match ? match[1] : null;
33
+ }
34
+
35
+ /**
36
+ * Get YouTube video metadata using oembed API
37
+ * This works without CORS issues
38
+ */
39
+ static async getVideoMetadata(url: string): Promise<{
40
+ title: string;
41
+ thumbnail: string;
42
+ duration?: number;
43
+ }> {
44
+ try {
45
+ // Use noembed API which has better CORS support
46
+ const apiUrl = `${this.YOUTUBE_NOEMBED_API}${encodeURIComponent(url)}`;
47
+ const response = await fetch(apiUrl);
48
+
49
+ if (!response.ok) {
50
+ throw new Error('Failed to fetch YouTube metadata');
51
+ }
52
+
53
+ const data = await response.json();
54
+
55
+ return {
56
+ title: data.title || 'YouTube Video',
57
+ thumbnail: data.thumbnail_url || `https://img.youtube.com/vi/${this.extractVideoId(url)}/maxresdefault.jpg`,
58
+ duration: data.duration || undefined
59
+ };
60
+ } catch (error) {
61
+ console.warn('Failed to fetch YouTube metadata:', error);
62
+ // Return fallback metadata
63
+ const videoId = this.extractVideoId(url);
64
+ return {
65
+ title: 'YouTube Video',
66
+ thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
67
+ duration: undefined
68
+ };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Convert YouTube URL to embeddable iframe URL
74
+ * This is used for getting the video stream through various methods
75
+ */
76
+ static getEmbedUrl(videoId: string): string {
77
+ return `https://www.youtube.com/embed/${videoId}?modestbranding=1&rel=0&controls=0`;
78
+ }
79
+
80
+ /**
81
+ * Get direct video stream using py-youtube or similar service
82
+ * Note: This requires a backend service as direct YouTube downloads violate ToS
83
+ *
84
+ * For client-side, we recommend using:
85
+ * 1. YouTube IFrame API with custom controls
86
+ * 2. Backend service that extracts video streams
87
+ * 3. HLS variant of YouTube if available
88
+ */
89
+ static async getDirectStreamUrl(videoId: string, backendEndpoint?: string): Promise<string | null> {
90
+ if (!backendEndpoint) {
91
+ console.warn('No backend endpoint provided for YouTube video extraction. Using fallback method.');
92
+ return this.getFallbackStreamUrl(videoId);
93
+ }
94
+
95
+ try {
96
+ const response = await fetch(backendEndpoint, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json'
100
+ },
101
+ body: JSON.stringify({ videoId })
102
+ });
103
+
104
+ if (!response.ok) {
105
+ throw new Error('Backend failed to extract stream');
106
+ }
107
+
108
+ const data = await response.json();
109
+ return data.streamUrl || null;
110
+ } catch (error) {
111
+ console.error('Failed to get direct stream URL:', error);
112
+ return this.getFallbackStreamUrl(videoId);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Fallback method: Return YouTube watch URL with adaptive streaming
118
+ * This uses YouTube's own HLS/DASH streams if available
119
+ */
120
+ private static getFallbackStreamUrl(videoId: string): string {
121
+ // This returns the standard YouTube URL which has built-in HLS support
122
+ // For actual stream extraction, you need a backend service or use YouTube IFrame API
123
+ return `https://www.youtube.com/watch?v=${videoId}`;
124
+ }
125
+
126
+ /**
127
+ * Create a YouTube-compatible player configuration
128
+ * This prepares the source object for our custom player
129
+ */
130
+ static async prepareYouTubeSource(url: string, backendEndpoint?: string) {
131
+ const videoId = this.extractVideoId(url);
132
+
133
+ if (!videoId) {
134
+ throw new Error('Invalid YouTube URL');
135
+ }
136
+
137
+ const metadata = await this.getVideoMetadata(url);
138
+
139
+ // For direct streaming, we need a backend service
140
+ // This could be implemented using yt-dlp, pytube, or similar
141
+ const streamUrl = await this.getDirectStreamUrl(videoId, backendEndpoint);
142
+
143
+ return {
144
+ url: streamUrl || url, // Fallback to original URL
145
+ type: 'youtube',
146
+ title: metadata.title,
147
+ thumbnail: metadata.thumbnail,
148
+ duration: metadata.duration,
149
+ videoId: videoId,
150
+ isYouTube: true,
151
+ metadata: {
152
+ source: 'youtube',
153
+ videoId: videoId
154
+ }
155
+ };
156
+ }
157
+ }
158
+
159
+ export default YouTubeExtractor;