import type { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile } from '@ffmpeg/util'; import { loadFFmpeg } from '@/lib/wasm/wasmLoader'; import type { ConversionOptions, ProgressCallback, ConversionResult } from '@/types/conversion'; /** * Convert video/audio using FFmpeg */ export async function convertWithFFmpeg( file: File, outputFormat: string, options: ConversionOptions = {}, onProgress?: ProgressCallback ): Promise { const startTime = Date.now(); try { // Load FFmpeg instance const ffmpeg: FFmpeg = await loadFFmpeg(); // Set up progress tracking if (onProgress) { ffmpeg.on('progress', ({ progress }) => { onProgress(Math.round(progress * 100)); }); } // Input filename const inputName = file.name; const outputName = `output.${outputFormat}`; // Write input file to FFmpeg virtual file system await ffmpeg.writeFile(inputName, await fetchFile(file)); // Build FFmpeg command based on format and options const args = buildFFmpegArgs(inputName, outputName, outputFormat, options); console.log('[FFmpeg] Running command:', args.join(' ')); // Execute FFmpeg command await ffmpeg.exec(args); // Read output file const data = await ffmpeg.readFile(outputName); const blob = new Blob([data as BlobPart], { type: getMimeType(outputFormat) }); // Clean up virtual file system await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); const duration = Date.now() - startTime; return { success: true, blob, duration, }; } catch (error) { console.error('[FFmpeg] Conversion error:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown conversion error', duration: Date.now() - startTime, }; } } /** * Build FFmpeg command arguments */ function buildFFmpegArgs( inputName: string, outputName: string, outputFormat: string, options: ConversionOptions ): string[] { const args = ['-i', inputName]; // Video codec if (options.videoCodec) { args.push('-c:v', options.videoCodec); } // Video bitrate if (options.videoBitrate) { args.push('-b:v', options.videoBitrate); } // Video resolution if (options.videoResolution) { args.push('-s', options.videoResolution); } // Video FPS if (options.videoFps) { args.push('-r', options.videoFps.toString()); } // Audio codec if (options.audioCodec) { args.push('-c:a', options.audioCodec); } // Audio bitrate if (options.audioBitrate) { args.push('-b:a', options.audioBitrate); } // Audio sample rate if (options.audioSampleRate) { args.push('-ar', options.audioSampleRate.toString()); } // Audio channels if (options.audioChannels) { args.push('-ac', options.audioChannels.toString()); } // Format-specific settings switch (outputFormat) { case 'webm': // Use VP8 by default (less memory-intensive than VP9) if (!options.videoCodec) args.push('-c:v', 'libvpx'); if (!options.audioCodec) args.push('-c:a', 'libvorbis'); // Optimize for faster encoding and lower memory usage args.push('-deadline', 'realtime'); args.push('-cpu-used', '8'); // Set quality/bitrate if not specified if (!options.videoBitrate) args.push('-b:v', '1M'); if (!options.audioBitrate) args.push('-b:a', '128k'); break; case 'mp4': if (!options.videoCodec) args.push('-c:v', 'libx264'); if (!options.audioCodec) args.push('-c:a', 'aac'); // Use faster preset for browser encoding args.push('-preset', 'ultrafast'); args.push('-tune', 'zerolatency'); if (!options.videoBitrate) args.push('-b:v', '1M'); if (!options.audioBitrate) args.push('-b:a', '128k'); break; case 'mp3': if (!options.audioCodec) args.push('-c:a', 'libmp3lame'); args.push('-vn'); // No video break; case 'wav': if (!options.audioCodec) args.push('-c:a', 'pcm_s16le'); args.push('-vn'); // No video break; case 'ogg': if (!options.audioCodec) args.push('-c:a', 'libvorbis'); args.push('-vn'); // No video break; case 'gif': // For GIF, use filter to optimize args.push('-vf', 'fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse'); break; } // Output file args.push(outputName); return args; } /** * Get MIME type for output format */ function getMimeType(format: string): string { const mimeTypes: Record = { mp4: 'video/mp4', webm: 'video/webm', avi: 'video/x-msvideo', mov: 'video/quicktime', mkv: 'video/x-matroska', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', aac: 'audio/aac', flac: 'audio/flac', gif: 'image/gif', }; return mimeTypes[format] || 'application/octet-stream'; } /** * Extract audio from video */ export async function extractAudio( file: File, outputFormat: string = 'mp3', onProgress?: ProgressCallback ): Promise { return convertWithFFmpeg( file, outputFormat, { audioCodec: outputFormat === 'mp3' ? 'libmp3lame' : undefined, audioBitrate: '192k', }, onProgress ); } /** * Convert video to GIF */ export async function videoToGif( file: File, fps: number = 15, width: number = 480, onProgress?: ProgressCallback ): Promise { return convertWithFFmpeg( file, 'gif', { videoFps: fps, videoResolution: `${width}:-1`, }, onProgress ); }