import { execFile } from 'child_process'; import { promisify } from 'util'; const execFileAsync = promisify(execFile); export const runFFmpeg = async (args, options = {}) => { const { stdio = 'pipe', ...otherOptions } = options; try { const { stdout, stderr } = await execFileAsync('ffmpeg', args, { stdio, ...otherOptions, }); return { stdout, stderr }; } catch (error) { throw new Error(`FFmpeg error: ${error.message}\nStderr: ${error.stderr}`); } }; const runFFprobe = async (args) => { try { const { stdout } = await execFileAsync('ffprobe', args); return { stdout }; } catch (error) { throw new Error(`FFprobe error: ${error.message}\nStderr: ${error.stderr}`); } }; export const getMediaDuration = async (inputPath) => { const args = [ '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', inputPath, ]; const { stdout } = await runFFprobe(args); return parseFloat(stdout.trim()); }; /** * Return structured media metadata for an input file. * Result shape: * { fps, codec, resolution, durationMs, fileSizeBytes } * Any field may be null if ffprobe cannot determine it. */ export const getMediaInfo = async (inputPath) => { const args = [ '-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', inputPath, ]; const { stdout } = await runFFprobe(args); const info = JSON.parse(stdout); const videoStream = (info.streams || []).find(s => s.codec_type === 'video'); const fmt = info.format || {}; let fps = null; if (videoStream?.r_frame_rate) { const [num, den] = videoStream.r_frame_rate.split('/').map(Number); if (den > 0) fps = Math.round((num / den) * 1000) / 1000; } return { fps, codec: videoStream?.codec_name || null, resolution: videoStream ? `${videoStream.width}x${videoStream.height}` : null, durationMs: fmt.duration ? Math.round(parseFloat(fmt.duration) * 1000) : null, fileSizeBytes: fmt.size ? parseInt(fmt.size, 10) : null, }; }; export const extractFrameAtTime = async (inputPath, outputPath, timeCode) => { const args = [ '-ss', timeCode, '-i', inputPath, '-vframes', '1', '-q:v', '2', '-y', outputPath, ]; await runFFmpeg(args); }; export const transcodeVideo = async (inputPath, outputPath, options = {}) => { const { videoCodec = 'libx264', videoPreset = 'fast', videoBitrate = '10M', audioCodec = 'aac', audioBitrate = '192k', } = options; const args = [ '-i', inputPath, '-c:v', videoCodec, '-preset', videoPreset, '-b:v', videoBitrate, '-c:a', audioCodec, '-b:a', audioBitrate, '-movflags', '+faststart', '-y', outputPath, ]; await runFFmpeg(args); }; export const trimSegment = async (inputPath, outputPath, inPoint, outPoint) => { const args = [ '-i', inputPath, '-ss', inPoint, '-to', outPoint, '-c', 'copy', '-y', outputPath, ]; await runFFmpeg(args); }; export const concatSegments = async (segmentListFile, outputPath) => { const args = [ '-f', 'concat', '-safe', '0', '-i', segmentListFile, '-c', 'copy', '-y', outputPath, ]; await runFFmpeg(args); };