unified-video-framework
Version:
Cross-platform video player framework supporting iOS, Android, Web, Smart TVs (Samsung/LG), Roku, and more
163 lines (142 loc) • 5.36 kB
text/typescript
/**
* YouTube Video Extractor and Stream Fetcher
* Handles YouTube URL detection, video ID extraction, and fetches direct video streams
*/
export interface YouTubeVideoInfo {
videoId: string;
title: string;
duration: number;
thumbnail: string;
streamUrl: string;
format: 'mp4' | 'webm';
}
export class YouTubeExtractor {
private static readonly YOUTUBE_REGEX = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/|live\/)?([a-zA-Z0-9_-]{11})/;
private static readonly YOUTUBE_NOEMBED_API = 'https://noembed.com/embed?url=';
private static readonly YOUTUBE_API_ENDPOINT = 'https://www.youtube.com/oembed?url=';
/**
* Detect if URL is a valid YouTube URL
*/
static isYouTubeUrl(url: string): boolean {
return this.YOUTUBE_REGEX.test(url);
}
/**
* Extract video ID from YouTube URL
*/
static extractVideoId(url: string): string | null {
const match = url.match(this.YOUTUBE_REGEX);
return match ? match[1] : null;
}
/**
* Get YouTube video metadata using oembed API
* This works without CORS issues
*/
static async getVideoMetadata(url: string): Promise<{
title: string;
thumbnail: string;
duration?: number;
}> {
try {
// Use noembed API which has better CORS support
const apiUrl = `${this.YOUTUBE_NOEMBED_API}${encodeURIComponent(url)}`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error('Failed to fetch YouTube metadata');
}
const data = await response.json();
return {
title: data.title || 'YouTube Video',
thumbnail: data.thumbnail_url || `https://img.youtube.com/vi/${this.extractVideoId(url)}/maxresdefault.jpg`,
duration: data.duration || undefined
};
} catch (error) {
console.warn('Failed to fetch YouTube metadata:', error);
// Return fallback metadata
const videoId = this.extractVideoId(url);
return {
title: 'YouTube Video',
thumbnail: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
duration: undefined
};
}
}
/**
* Convert YouTube URL to embeddable iframe URL
* This is used for getting the video stream through various methods
* @param videoId - The YouTube video ID
* @param showControls - Whether to show YouTube native controls (default: false)
*/
static getEmbedUrl(videoId: string, showControls: boolean = false): string {
const controls = showControls ? 1 : 0;
return `https://www.youtube.com/embed/${videoId}?modestbranding=1&rel=0&controls=${controls}`;
}
/**
* Get direct video stream using py-youtube or similar service
* Note: This requires a backend service as direct YouTube downloads violate ToS
*
* For client-side, we recommend using:
* 1. YouTube IFrame API with custom controls
* 2. Backend service that extracts video streams
* 3. HLS variant of YouTube if available
*/
static async getDirectStreamUrl(videoId: string, backendEndpoint?: string): Promise<string | null> {
if (!backendEndpoint) {
console.warn('No backend endpoint provided for YouTube video extraction. Using fallback method.');
return this.getFallbackStreamUrl(videoId);
}
try {
const response = await fetch(backendEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ videoId })
});
if (!response.ok) {
throw new Error('Backend failed to extract stream');
}
const data = await response.json();
return data.streamUrl || null;
} catch (error) {
console.error('Failed to get direct stream URL:', error);
return this.getFallbackStreamUrl(videoId);
}
}
/**
* Fallback method: Return YouTube watch URL with adaptive streaming
* This uses YouTube's own HLS/DASH streams if available
*/
private static getFallbackStreamUrl(videoId: string): string {
// This returns the standard YouTube URL which has built-in HLS support
// For actual stream extraction, you need a backend service or use YouTube IFrame API
return `https://www.youtube.com/watch?v=${videoId}`;
}
/**
* Create a YouTube-compatible player configuration
* This prepares the source object for our custom player
*/
static async prepareYouTubeSource(url: string, backendEndpoint?: string) {
const videoId = this.extractVideoId(url);
if (!videoId) {
throw new Error('Invalid YouTube URL');
}
const metadata = await this.getVideoMetadata(url);
// For direct streaming, we need a backend service
// This could be implemented using yt-dlp, pytube, or similar
const streamUrl = await this.getDirectStreamUrl(videoId, backendEndpoint);
return {
url: streamUrl || url, // Fallback to original URL
type: 'youtube',
title: metadata.title,
thumbnail: metadata.thumbnail,
duration: metadata.duration,
videoId: videoId,
isYouTube: true,
metadata: {
source: 'youtube',
videoId: videoId
}
};
}
}
export default YouTubeExtractor;