/** * Video utilities for handling YouTube embeds and video URLs */ export type VideoPosition = 'top' | 'inline' | 'bottom' | 'none'; export type VideoPlatform = 'youtube' | 'vimeo' | 'dailymotion' | 'twitch' | 'uploaded' | 'external'; /** * Extract YouTube video ID from various URL formats * Supported formats: * - https://www.youtube.com/watch?v=VIDEO_ID * - https://youtu.be/VIDEO_ID * - https://www.youtube.com/embed/VIDEO_ID * - https://www.youtube.com/v/VIDEO_ID * - https://www.youtube.com/shorts/VIDEO_ID */ export function extractYouTubeVideoId(url: string): string | null { if (!url) return null; const patterns = [ // Standard watch URL /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/, // Just the video ID /^([a-zA-Z0-9_-]{11})$/, ]; for (const pattern of patterns) { const match = url.match(pattern); if (match && match[1]) { return match[1]; } } return null; } /** * Check if a URL is a valid YouTube URL */ export function isValidYouTubeUrl(url: string): boolean { return extractYouTubeVideoId(url) !== null; } /** * Get YouTube thumbnail URL for a video ID * Quality options: default, mq (medium), hq (high), sd (standard), maxres (maximum resolution) */ export function getYouTubeThumbnail( videoId: string, quality: 'default' | 'mq' | 'hq' | 'sd' | 'maxres' = 'hq' ): string { const qualityMap = { default: 'default.jpg', mq: 'mqdefault.jpg', hq: 'hqdefault.jpg', sd: 'sddefault.jpg', maxres: 'maxresdefault.jpg', }; return `https://img.youtube.com/vi/${videoId}/${qualityMap[quality]}`; } /** * Detect video platform from URL */ export function detectVideoPlatform(url: string): VideoPlatform { if (!url) return 'external'; if (url.includes('youtube.com') || url.includes('youtu.be')) return 'youtube'; if (url.includes('vimeo.com')) return 'vimeo'; if (url.includes('dailymotion.com')) return 'dailymotion'; if (url.includes('twitch.tv')) return 'twitch'; if (url.match(/\.(mp4|webm|ogg|mov|avi|wmv|flv|mkv)$/i)) return 'uploaded'; return 'external'; } /** * Generate YouTube embed URL with privacy-enhanced mode and parameters */ export function generateYouTubeEmbedUrl( videoId: string, options: { autoplay?: boolean; controls?: boolean; modestbranding?: boolean; rel?: boolean; playsinline?: boolean; start?: number; end?: number; } = {} ): string { const { autoplay = false, controls = true, modestbranding = true, rel = false, playsinline = true, start, end, } = options; const params = new URLSearchParams({ autoplay: autoplay ? '1' : '0', controls: controls ? '1' : '0', modestbranding: modestbranding ? '1' : '0', rel: rel ? '1' : '0', playsinline: playsinline ? '1' : '0', }); if (start !== undefined) params.set('start', start.toString()); if (end !== undefined) params.set('end', end.toString()); // Use privacy-enhanced mode (youtube-nocookie.com) return `https://www.youtube-nocookie.com/embed/${videoId}?${params.toString()}`; } /** * Get CSS classes for video positioning */ export function getVideoPositionClasses(position: VideoPosition): string { switch (position) { case 'top': return 'mb-8'; case 'inline': return 'my-6'; case 'bottom': return 'mt-8'; case 'none': return ''; default: return 'my-6'; } } /** * Validate video URL */ export function validateVideoUrl(url: string): { isValid: boolean; platform: VideoPlatform; error?: string } { if (!url) { return { isValid: false, platform: 'external', error: 'Video URL is required' }; } try { new URL(url); } catch { return { isValid: false, platform: 'external', error: 'Invalid URL format' }; } const platform = detectVideoPlatform(url); if (platform === 'youtube' && !isValidYouTubeUrl(url)) { return { isValid: false, platform: 'youtube', error: 'Invalid YouTube URL' }; } return { isValid: true, platform }; }