UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

207 lines 7.83 kB
/** * Shared FFmpeg Adapter for Video Operations * * Centralizes FFmpeg binary resolution, process execution, and temporary file * management for all video adapter modules (frameExtractor, videoMerger). * * Follows the adapter pattern used in `src/lib/adapters/tts/` and * `src/lib/adapters/providerImageAdapter.ts`. * * @module adapters/video/ffmpegAdapter */ import { randomUUID } from "node:crypto"; import { readdirSync, rmdirSync, unlinkSync } from "node:fs"; import { mkdir, readFile, rm, unlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { logger } from "../../utils/logger.js"; // ============================================================================ // CONSTANTS // ============================================================================ /** Timeout for frame-extraction FFmpeg operations (30 seconds) */ export const FFMPEG_FRAME_TIMEOUT_MS = 30_000; /** Timeout for merge/concat FFmpeg operations (2 minutes) */ export const FFMPEG_MERGE_TIMEOUT_MS = 120_000; /** Max stdout/stderr buffer for frame extraction (10 MB) */ export const FFMPEG_FRAME_MAX_BUFFER = 10 * 1024 * 1024; /** Max stdout/stderr buffer for merge operations (50 MB) */ export const FFMPEG_MERGE_MAX_BUFFER = 50 * 1024 * 1024; /** FFmpeg JPEG quality scale (2 = high quality, range 2-31) */ export const JPEG_QUALITY = "2"; /** Seconds before end-of-video to seek when extracting last frame */ export const LAST_FRAME_SEEK_OFFSET = "0.5"; /** Minimum valid MP4 buffer size in bytes (ftyp header = 8 bytes minimum) */ export const MIN_VIDEO_BUFFER_SIZE = 12; /** MP4 ftyp box magic bytes at offset 4: "ftyp" */ const FTYP_MAGIC = Buffer.from([0x66, 0x74, 0x79, 0x70]); // ============================================================================ // TEMP DIRECTORY MANAGEMENT // ============================================================================ /** Track active temp directories for process cleanup */ const activeTempDirs = new Set(); let cleanupRegistered = false; /** * Register process-level cleanup handlers once. * Removes all tracked temp directories on abnormal exit. */ function ensureCleanupRegistered() { if (cleanupRegistered) { return; } cleanupRegistered = true; const cleanup = () => { for (const dir of activeTempDirs) { try { // Sync removal for exit handlers — best-effort only const entries = readdirSync(dir); for (const entry of entries) { try { unlinkSync(join(dir, entry)); } catch { /* best-effort */ } } rmdirSync(dir); } catch { /* best-effort */ } } activeTempDirs.clear(); }; process.on("exit", cleanup); } /** * Create a tracked temporary directory for FFmpeg operations. * * @param prefix - Directory name prefix (e.g. "frame", "merge") * @returns Absolute path to the created directory */ export async function createTrackedTempDir(prefix) { ensureCleanupRegistered(); const dir = join(tmpdir(), `neurolink-${prefix}-${randomUUID()}`); await mkdir(dir, { recursive: true }); activeTempDirs.add(dir); return dir; } /** * Clean up temporary files and their parent directory. * Logs failures at debug level instead of swallowing silently. * * @param tempDir - The temporary directory to remove * @param files - File paths within tempDir to delete */ export async function cleanupTempFiles(tempDir, ...files) { for (const filePath of files) { try { await unlink(filePath); } catch (err) { logger.debug("Failed to clean up temp file", { path: filePath, error: err instanceof Error ? err.message : String(err), }); } } try { await rm(tempDir, { recursive: true, force: true }); } catch (err) { logger.debug("Failed to remove temp directory", { path: tempDir, error: err instanceof Error ? err.message : String(err), }); } activeTempDirs.delete(tempDir); } // ============================================================================ // FFMPEG BINARY RESOLUTION // ============================================================================ /** Cached FFmpeg binary path to avoid repeated resolution */ let cachedFfmpegPath = null; /** * Resolve the FFmpeg binary path. * * Resolution order: * 1. `FFMPEG_PATH` environment variable * 2. `ffmpeg-static` npm package (optional peer dependency) * 3. System `ffmpeg` on PATH * * @returns Absolute or relative path to the FFmpeg binary */ export async function getFfmpegPath() { if (cachedFfmpegPath) { return cachedFfmpegPath; } if (process.env.FFMPEG_PATH) { cachedFfmpegPath = process.env.FFMPEG_PATH; return cachedFfmpegPath; } try { const ffmpegStatic = await import("ffmpeg-static"); const staticPath = ffmpegStatic.default ?? ffmpegStatic; if (typeof staticPath === "string" && staticPath.length > 0) { cachedFfmpegPath = staticPath; return cachedFfmpegPath; } } catch { // ffmpeg-static not installed — fall through to system binary logger.debug("ffmpeg-static not available, using system ffmpeg binary"); } cachedFfmpegPath = "ffmpeg"; logger.warn("Using system ffmpeg binary. If video operations fail with ENOENT, install ffmpeg-static or set FFMPEG_PATH."); return cachedFfmpegPath; } // ============================================================================ // FFMPEG PROCESS EXECUTION // ============================================================================ /** * Run an FFmpeg command via `child_process.execFile`. * * @param args - FFmpeg CLI arguments (without the binary path) * @param options - Timeout and buffer size overrides * @returns stdout and stderr from the process * @throws Error if the process exits with a non-zero code or times out */ export async function runFfmpeg(args, options = {}) { const { execFile } = await import("node:child_process"); const ffmpegPath = await getFfmpegPath(); const timeoutMs = options.timeoutMs ?? FFMPEG_FRAME_TIMEOUT_MS; const maxBuffer = options.maxBuffer ?? FFMPEG_FRAME_MAX_BUFFER; return new Promise((resolve, reject) => { const proc = execFile(ffmpegPath, args, { timeout: timeoutMs, maxBuffer }, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve({ stdout: stdout || "", stderr: stderr || "" }); } }); proc.on("error", reject); }); } // ============================================================================ // BUFFER VALIDATION // ============================================================================ /** * Validate that a buffer looks like a valid MP4 video. * * Checks minimum size and the presence of an `ftyp` box header. * * @param buffer - Buffer to validate * @returns `true` if the buffer passes basic MP4 validation */ export function isValidMp4Buffer(buffer) { if (buffer.length < MIN_VIDEO_BUFFER_SIZE) { return false; } // Check for ftyp box: bytes 4-7 should be "ftyp" return buffer.subarray(4, 8).equals(FTYP_MAGIC); } // ============================================================================ // FILE I/O HELPERS // ============================================================================ export { writeFile, readFile, join }; //# sourceMappingURL=ffmpegAdapter.js.map