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

172 lines 6.4 kB
/** * Video Merger for Director Mode * * Concatenates multiple MP4 video buffers into a single MP4 using FFmpeg's * concat demuxer for lossless concatenation when codecs match. * * Uses the shared FFmpeg adapter for binary resolution, temp file management, * and process execution — following the adapter pattern in `adapters/tts/`. * * @module adapters/video/videoMerger */ import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js"; import { logger } from "../../utils/logger.js"; import { cleanupTempFiles, createTrackedTempDir, FFMPEG_MERGE_MAX_BUFFER, FFMPEG_MERGE_TIMEOUT_MS, isValidMp4Buffer, join, readFile, runFfmpeg, writeFile, } from "./ffmpegAdapter.js"; import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js"; import { VideoError } from "./vertexVideoHandler.js"; // ============================================================================ // INTERNAL HELPERS // ============================================================================ /** * Write clip buffers to temp files and build the FFmpeg concat list. * * @returns Paths of all written clip files */ async function writeClipsAndBuildConcatList(videoBuffers, tempDir, concatListPath) { const inputPaths = []; const concatLines = []; for (let i = 0; i < videoBuffers.length; i++) { const inputPath = join(tempDir, `clip_${i}.mp4`); await writeFile(inputPath, videoBuffers[i]); inputPaths.push(inputPath); // Normalize backslashes to forward slashes for FFmpeg, then escape single quotes const safePath = inputPath.replace(/\\/g, "/").replace(/'/g, "'\\''"); concatLines.push(`file '${safePath}'`); } await writeFile(concatListPath, concatLines.join("\n")); return inputPaths; } /** * Attempt lossless concat, falling back to H.264 re-encode on failure. */ async function concatWithFallback(concatListPath, outputPath) { const ffmpegOpts = { timeoutMs: FFMPEG_MERGE_TIMEOUT_MS, maxBuffer: FFMPEG_MERGE_MAX_BUFFER, }; try { // Try lossless concat first (fastest — no re-encoding) await runFfmpeg([ "-y", "-f", "concat", "-safe", "0", "-i", concatListPath, "-c", "copy", outputPath, ], ffmpegOpts); } catch (concatError) { // Fallback: re-encode with H.264 if codecs mismatch logger.warn("Lossless concat failed, falling back to H.264 re-encoding", { error: concatError instanceof Error ? concatError.message : String(concatError), }); await runFfmpeg([ "-y", "-f", "concat", "-safe", "0", "-i", concatListPath, "-c:v", "libx264", "-preset", "fast", "-crf", "18", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", outputPath, ], ffmpegOpts); } } // ============================================================================ // PUBLIC API // ============================================================================ /** * Merge multiple MP4 video buffers into a single MP4. * * Uses FFmpeg concat demuxer for lossless concatenation. If codecs don't match * (unlikely since all clips come from Veo), falls back to re-encoding with H.264. * * @param videoBuffers - Array of MP4 video buffers to concatenate in order * @returns Merged MP4 video buffer * @throws {VideoError} If merge fails or no valid buffers provided */ export async function mergeVideoBuffers(videoBuffers) { if (videoBuffers.length === 0) { throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_MERGE_FAILED, message: "No video buffers provided to merge", category: ErrorCategory.VALIDATION, severity: ErrorSeverity.HIGH, retriable: false, }); } // Validate each input buffer for (let i = 0; i < videoBuffers.length; i++) { if (!isValidMp4Buffer(videoBuffers[i])) { throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_MERGE_FAILED, message: `Clip ${i} is not a valid MP4 buffer (missing ftyp header or too small)`, category: ErrorCategory.VALIDATION, severity: ErrorSeverity.HIGH, retriable: false, context: { clipIndex: i, bufferSize: videoBuffers[i].length, headerHex: videoBuffers[i].subarray(0, 12).toString("hex"), }, }); } } if (videoBuffers.length === 1) { return videoBuffers[0]; } const startTime = Date.now(); const tempDir = await createTrackedTempDir("merge"); const concatListPath = join(tempDir, "concat.txt"); const outputPath = join(tempDir, "merged.mp4"); let inputPaths = []; try { inputPaths = await writeClipsAndBuildConcatList(videoBuffers, tempDir, concatListPath); await concatWithFallback(concatListPath, outputPath); const mergedBuffer = await readFile(outputPath); logger.info("Video merge complete", { inputClips: videoBuffers.length, totalInputSize: videoBuffers.reduce((sum, b) => sum + b.length, 0), outputSize: mergedBuffer.length, elapsedMs: Date.now() - startTime, }); return mergedBuffer; } catch (error) { // Re-throw VideoErrors as-is if (error instanceof VideoError) { throw error; } throw new VideoError({ code: VIDEO_ERROR_CODES.DIRECTOR_MERGE_FAILED, message: `Video merge failed: ${error instanceof Error ? error.message : String(error)}`, category: ErrorCategory.EXECUTION, severity: ErrorSeverity.HIGH, retriable: false, context: { clipCount: videoBuffers.length }, originalError: error instanceof Error ? error : undefined, }); } finally { await cleanupTempFiles(tempDir, concatListPath, outputPath, ...inputPaths); } } //# sourceMappingURL=videoMerger.js.map