import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; import fs from 'fs'; const execAsync = promisify(exec); export interface GifOptions { fps?: number; // Frames per second (default: 10) width?: number; // Width in pixels (default: 300) duration?: number; // Duration in seconds to capture (default: 3) startTime?: number; // Start time in seconds (default: 0) } /** * Generate a GIF from a video file using ffmpeg * @param videoPath - Absolute path to the video file * @param outputPath - Absolute path where GIF should be saved * @param options - GIF generation options * @returns Promise - Path to generated GIF */ export async function generateGifFromVideo( videoPath: string, outputPath: string, options: GifOptions = {} ): Promise { const { fps = 10, width = 300, duration = 3, startTime = 0, } = options; // Ensure output directory exists const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Build ffmpeg command // -ss: start time // -t: duration // -vf: video filters (fps, scale, palettegen/paletteuse for better quality) const paletteFile = path.join(outputDir, `palette-${Date.now()}.png`); try { // Step 1: Generate color palette for better GIF quality const paletteCmd = `ffmpeg -ss ${startTime} -t ${duration} -i "${videoPath}" -vf "fps=${fps},scale=${width}:-1:flags=lanczos,palettegen" -y "${paletteFile}"`; await execAsync(paletteCmd); // Step 2: Generate GIF using the palette const gifCmd = `ffmpeg -ss ${startTime} -t ${duration} -i "${videoPath}" -i "${paletteFile}" -lavfi "fps=${fps},scale=${width}:-1:flags=lanczos[x];[x][1:v]paletteuse" -y "${outputPath}"`; await execAsync(gifCmd); // Clean up palette file if (fs.existsSync(paletteFile)) { fs.unlinkSync(paletteFile); } return outputPath; } catch (error: any) { // Clean up on error if (fs.existsSync(paletteFile)) { fs.unlinkSync(paletteFile); } if (fs.existsSync(outputPath)) { fs.unlinkSync(outputPath); } throw new Error(`Failed to generate GIF: ${error.message}`); } } /** * Get video duration in seconds using ffprobe * @param videoPath - Absolute path to the video file * @returns Promise - Duration in seconds */ export async function getVideoDuration(videoPath: string): Promise { try { const cmd = `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`; const { stdout } = await execAsync(cmd); return parseFloat(stdout.trim()); } catch (error: any) { throw new Error(`Failed to get video duration: ${error.message}`); } }