import type { VideoProfile } from '../types'; /** * Get optimal BPP (Bits Per Pixel) based on resolution * Lower resolutions need higher BPP for good quality * Higher resolutions can use lower BPP due to more pixels */ function getBPP(width: number, height: number): number { const pixels = width * height; if (pixels <= 640 * 360) return 0.08; // 360p - higher quality needed if (pixels <= 854 * 480) return 0.075; // 480p if (pixels <= 1280 * 720) return 0.07; // 720p if (pixels <= 1920 * 1080) return 0.065; // 1080p if (pixels <= 2560 * 1440) return 0.06; // 1440p (2K) return 0.055; // 4K - lower BPP but still quality } /** * Calculate optimal video bitrate based on resolution and FPS * Formula: width × height × fps × bpp */ function calculateBitrate( width: number, height: number, fps: number = 30, maxBitrate?: number ): string { const bpp = getBPP(width, height); let bitrate = Math.round((width * height * fps * bpp) / 1000); // Don't exceed source bitrate (no point in upscaling quality) if (maxBitrate && bitrate > maxBitrate) { bitrate = maxBitrate; } return `${bitrate}k`; } /** * Default video quality profiles for 30 FPS */ export const DEFAULT_PROFILES: VideoProfile[] = [ { name: '360p', width: 640, height: 360, videoBitrate: calculateBitrate(640, 360, 30), audioBitrate: '192k' }, { name: '480p', width: 854, height: 480, videoBitrate: calculateBitrate(854, 480, 30), audioBitrate: '192k' }, { name: '720p', width: 1280, height: 720, videoBitrate: calculateBitrate(1280, 720, 30), audioBitrate: '192k' }, { name: '1080p', width: 1920, height: 1080, videoBitrate: calculateBitrate(1920, 1080, 30), audioBitrate: '256k' }, { name: '1440p', width: 2560, height: 1440, videoBitrate: calculateBitrate(2560, 1440, 30), audioBitrate: '256k' }, { name: '2160p', width: 3840, height: 2160, videoBitrate: calculateBitrate(3840, 2160, 30), audioBitrate: '256k' } ]; /** * Select appropriate profiles based on input video resolution * Only creates profiles that are equal to or smaller than input resolution * Always generates 30 FPS profiles by default * For high FPS (>30), user must explicitly specify in customProfiles */ export function selectProfiles( inputWidth: number, inputHeight: number, inputFPS: number = 30, sourceBitrate?: number ): VideoProfile[] { const profiles: VideoProfile[] = []; // Standard 30 FPS profiles (always created) const baseProfiles = DEFAULT_PROFILES.filter(profile => { return profile.width <= inputWidth && profile.height <= inputHeight; }); // Add standard 30fps profiles with bitrate limit for (const profile of baseProfiles) { profiles.push({ ...profile, videoBitrate: calculateBitrate(profile.width, profile.height, 30, sourceBitrate), fps: 30 }); } return profiles; } /** * Create high FPS profile variant * Used for creating 60fps, 90fps, 120fps versions */ export function createHighFPSProfile( baseProfile: VideoProfile, fps: number, maxBitrate?: number ): VideoProfile { return { ...baseProfile, name: `${baseProfile.name}-${fps}`, videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, fps, maxBitrate), fps }; } /** * Parse profile string into resolution and FPS * Examples: * '360' => { resolution: '360p', fps: 30 } * '720@60' => { resolution: '720p', fps: 60 } * '1080-60' => { resolution: '1080p', fps: 60 } * '360p', '720p@60' also supported (with 'p') */ function parseProfileString(profileStr: string): { resolution: string; fps: number } | null { const trimmed = profileStr.trim(); // Match patterns: 360, 720@60, 1080-60, 360p, 720p@60, 1080p-60 const match = trimmed.match(/^(\d+)p?(?:[@-](\d+))?$/i); if (!match) { return null; } const resolution = match[1] + 'p'; // Always add 'p' const fps = match[2] ? parseInt(match[2]) : 30; return { resolution, fps }; } /** * Get profile by resolution name and FPS * Returns VideoProfile or null if not found */ export function getProfileByName( resolution: string, fps: number = 30, maxBitrate?: number ): VideoProfile | null { const baseProfile = DEFAULT_PROFILES.find(p => p.name === resolution); if (!baseProfile) { return null; } if (fps === 30) { return { ...baseProfile, videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, 30, maxBitrate), fps: 30 }; } return createHighFPSProfile(baseProfile, fps, maxBitrate); } /** * Validate if profile can be created from source * Returns object with error, warning, and adjusted FPS */ export function validateProfile( profileStr: string, sourceWidth: number, sourceHeight: number, sourceFPS: number ): { error?: string; warning?: string; adjustedFps?: number } { const parsed = parseProfileString(profileStr); if (!parsed) { return { error: `Invalid profile format: ${profileStr}. Use format like: 360, 720@60, 1080-60` }; } const profile = getProfileByName(parsed.resolution, parsed.fps); if (!profile) { return { error: `Unknown resolution: ${parsed.resolution}. Available: 360, 480, 720, 1080, 1440, 2160` }; } // Check if source supports this resolution if (profile.width > sourceWidth || profile.height > sourceHeight) { return { error: `Source resolution (${sourceWidth}x${sourceHeight}) is lower than ${profileStr} (${profile.width}x${profile.height})` }; } // Check if requested FPS exceeds source FPS const MAX_FPS = 120; let adjustedFps = parsed.fps; let warning: string | undefined; if (parsed.fps > sourceFPS) { // Cap to source FPS (but not more than MAX_FPS) adjustedFps = Math.min(sourceFPS, MAX_FPS); warning = `Requested ${parsed.fps} FPS in ${profileStr}, but source is ${sourceFPS} FPS. Using ${adjustedFps} FPS instead`; } else if (parsed.fps > MAX_FPS) { // Cap to MAX_FPS adjustedFps = MAX_FPS; warning = `Requested ${parsed.fps} FPS in ${profileStr} exceeds maximum ${MAX_FPS} FPS. Using ${adjustedFps} FPS instead`; } return warning ? { warning, adjustedFps } : {}; // Valid } /** * Create profiles from custom string list * Example: ['360p', '720p@60', '1080p'] => VideoProfile[] */ export function createProfilesFromStrings( profileStrings: string[], sourceWidth: number, sourceHeight: number, sourceFPS: number, sourceBitrate?: number ): { profiles: VideoProfile[]; errors: string[]; warnings: string[] } { const profiles: VideoProfile[] = []; const errors: string[] = []; const warnings: string[] = []; for (const profileStr of profileStrings) { // Validate const result = validateProfile(profileStr, sourceWidth, sourceHeight, sourceFPS); if (result.error) { errors.push(result.error); continue; } if (result.warning) { warnings.push(result.warning); } // Parse and create const parsed = parseProfileString(profileStr); if (!parsed) continue; // Already validated, shouldn't happen // Use adjusted FPS if available (when requested FPS > source FPS) const targetFps = result.adjustedFps !== undefined ? result.adjustedFps : parsed.fps; const profile = getProfileByName(parsed.resolution, targetFps, sourceBitrate); if (profile) { profiles.push(profile); } } return { profiles, errors, warnings }; }