UNPKG

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
/** * 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;