UNPKG

n8n-nodes-mediafx

Version:

N8N custom nodes for video editing and media processing

241 lines (240 loc) 11.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.executeOverlayVideo = void 0; const n8n_workflow_1 = require("n8n-workflow"); const ffmpeg = require("fluent-ffmpeg"); const utils_1 = require("../utils"); const fs = __importStar(require("fs-extra")); async function executeOverlayVideo(mainVideoPath, overlayVideoPath, options, itemIndex) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w; // Verify FFmpeg is available before proceeding try { (0, utils_1.verifyFfmpegAvailability)(); } catch (error) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `FFmpeg is not available: ${error.message}`, { itemIndex }); } // Extract output format from options (default to mp4) const outputFormat = options.outputFormat || 'mp4'; const outputPath = (0, utils_1.getTempFile)(`.${outputFormat}`); // Extract options with defaults // Position options const positionMode = (_a = options.positionMode) !== null && _a !== void 0 ? _a : 'alignment'; const horizontalAlign = (_b = options.horizontalAlign) !== null && _b !== void 0 ? _b : 'center'; const verticalAlign = (_c = options.verticalAlign) !== null && _c !== void 0 ? _c : 'middle'; const paddingX = (_d = options.paddingX) !== null && _d !== void 0 ? _d : 0; const paddingY = (_e = options.paddingY) !== null && _e !== void 0 ? _e : 0; const customX = (_f = options.x) !== null && _f !== void 0 ? _f : '0'; const customY = (_g = options.y) !== null && _g !== void 0 ? _g : '0'; // Size options const sizeMode = (_h = options.sizeMode) !== null && _h !== void 0 ? _h : 'percentage'; const widthPercent = (_j = options.widthPercent) !== null && _j !== void 0 ? _j : 50; const heightMode = (_k = options.heightMode) !== null && _k !== void 0 ? _k : 'auto'; const heightPercent = (_l = options.heightPercent) !== null && _l !== void 0 ? _l : 50; const widthPixels = (_m = options.widthPixels) !== null && _m !== void 0 ? _m : -1; const heightPixels = (_o = options.heightPixels) !== null && _o !== void 0 ? _o : -1; const opacity = (_p = options.opacity) !== null && _p !== void 0 ? _p : 1.0; const enableTimeControl = (_q = options.enableTimeControl) !== null && _q !== void 0 ? _q : false; const startTime = (_r = options.startTime) !== null && _r !== void 0 ? _r : 0; const endTime = (_s = options.endTime) !== null && _s !== void 0 ? _s : 0; const blendMode = (_t = options.blendMode) !== null && _t !== void 0 ? _t : 'normal'; const audioHandling = (_u = options.audioHandling) !== null && _u !== void 0 ? _u : 'main'; const mainVolume = (_v = options.mainVolume) !== null && _v !== void 0 ? _v : 1.0; const overlayVolume = (_w = options.overlayVolume) !== null && _w !== void 0 ? _w : 1.0; try { // Get durations for calculations const mainDuration = await (0, utils_1.getDuration)(mainVideoPath); const overlayDuration = await (0, utils_1.getDuration)(overlayVideoPath); // Get main video resolution for percentage calculations const mainVideoInfo = await (0, utils_1.getVideoStreamInfo)(mainVideoPath); const mainWidth = (mainVideoInfo === null || mainVideoInfo === void 0 ? void 0 : mainVideoInfo.width) || 1920; const mainHeight = (mainVideoInfo === null || mainVideoInfo === void 0 ? void 0 : mainVideoInfo.height) || 1080; // Calculate actual overlay dimensions based on size mode let scaleWidth = -1; let scaleHeight = -1; if (sizeMode === 'percentage') { // Calculate width as percentage of main video scaleWidth = Math.round(mainWidth * (widthPercent / 100)); if (heightMode === 'auto') { // Keep aspect ratio - use -1 for FFmpeg to auto-calculate scaleHeight = -1; } else { // Calculate height as percentage of main video scaleHeight = Math.round(mainHeight * (heightPercent / 100)); } } else if (sizeMode === 'pixels') { scaleWidth = widthPixels; scaleHeight = heightPixels; } // If sizeMode === 'original', leave both as -1 (no scaling) const needsScaling = sizeMode !== 'original'; // Calculate position based on position mode let posX; let posY; if (positionMode === 'alignment') { // Use FFmpeg expressions for alignment // overlay_w and overlay_h refer to the scaled overlay dimensions switch (horizontalAlign) { case 'left': posX = String(paddingX); break; case 'center': posX = `(main_w-overlay_w)/2`; break; case 'right': posX = `main_w-overlay_w-${paddingX}`; break; default: posX = String(paddingX); } switch (verticalAlign) { case 'top': posY = String(paddingY); break; case 'middle': posY = `(main_h-overlay_h)/2`; break; case 'bottom': posY = `main_h-overlay_h-${paddingY}`; break; default: posY = String(paddingY); } // Add padding offset for center alignment if (horizontalAlign === 'center' && paddingX !== 0) { posX = `(main_w-overlay_w)/2+${paddingX}`; } if (verticalAlign === 'middle' && paddingY !== 0) { posY = `(main_h-overlay_h)/2+${paddingY}`; } } else { // Custom coordinates mode - use provided x, y values posX = String(customX); posY = String(customY); } // Calculate actual end time const actualEndTime = enableTimeControl ? (endTime > 0 ? endTime : mainDuration) : mainDuration; // Build the video filter chain let videoFilterChain = ''; // Process overlay video (scale if needed) const needsOpacity = opacity < 1.0; let overlayProcessing = '[1:v]'; if (needsScaling) { overlayProcessing += `scale=${scaleWidth}:${scaleHeight}`; } if (needsOpacity) { if (needsScaling) { overlayProcessing += ','; } overlayProcessing += `colorchannelmixer=aa=${opacity}`; } // Apply blend mode filter if not normal if (blendMode !== 'normal' && blendMode !== 'over') { if (needsScaling || needsOpacity) { overlayProcessing += ','; } // For blend modes, we'll use the blend filter differently } if (needsScaling || needsOpacity) { overlayProcessing += '[ovr]'; videoFilterChain = overlayProcessing + ';'; videoFilterChain += '[0:v][ovr]'; } else { videoFilterChain = '[0:v][1:v]'; } // Build overlay filter with position // eof_action=pass: continue showing main video after overlay ends // repeatlast=0: don't repeat the last frame of overlay videoFilterChain += `overlay=x=${posX}:y=${posY}:eof_action=pass:repeatlast=0`; // Add time control if enabled if (enableTimeControl) { videoFilterChain += `:enable='between(t,${startTime},${actualEndTime})'`; } videoFilterChain += '[outv]'; // Build audio filter chain based on audio handling option let audioFilterChain = ''; let outputMaps = ['-map', '[outv]']; switch (audioHandling) { case 'main': // Use only main video's audio outputMaps.push('-map', '0:a?'); break; case 'overlay': // Use only overlay video's audio outputMaps.push('-map', '1:a?'); break; case 'mix': // Mix both audio tracks (use longest duration) audioFilterChain = `;[0:a]volume=${mainVolume}[a0];[1:a]volume=${overlayVolume}[a1];[a0][a1]amix=inputs=2:duration=longest[outa]`; outputMaps.push('-map', '[outa]'); break; case 'none': // No audio output break; default: outputMaps.push('-map', '0:a?'); } const fullFilterComplex = videoFilterChain + audioFilterChain; console.log('=== OVERLAY VIDEO DEBUG ==='); console.log('Main video:', mainVideoPath); console.log('Overlay video:', overlayVideoPath); console.log('Main duration:', mainDuration); console.log('Overlay duration:', overlayDuration); console.log('Main resolution:', { mainWidth, mainHeight }); console.log('Position mode:', positionMode); console.log('Calculated position:', { posX, posY }); console.log('Size mode:', sizeMode); console.log('Calculated scale:', { scaleWidth, scaleHeight, needsScaling }); console.log('Options:', { opacity, enableTimeControl, startTime, actualEndTime, blendMode, audioHandling }); console.log('Filter complex:', fullFilterComplex); console.log('Output maps:', outputMaps); console.log('==========================='); const command = ffmpeg() .input(mainVideoPath) .input(overlayVideoPath) .complexFilter(fullFilterComplex) .outputOptions(outputMaps) .outputOptions(['-c:v', 'libx264', '-preset', 'fast', '-crf', '23']) .output(outputPath); await (0, utils_1.runFfmpeg)(command); return outputPath; } catch (error) { // Clean up output file if creation failed await fs.remove(outputPath).catch(() => { }); console.error('=== OVERLAY VIDEO ERROR ==='); console.error('Error details:', error); console.error('==========================='); throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to overlay video: ${error instanceof Error ? error.message : 'Unknown error'}`, { itemIndex }); } } exports.executeOverlayVideo = executeOverlayVideo;