n8n-nodes-mediafx
Version:
N8N custom nodes for video editing and media processing
241 lines (240 loc) • 11.6 kB
JavaScript
;
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;