n8n-nodes-mediafx
Version:
N8N custom nodes for video editing and media processing
122 lines (121 loc) • 5.57 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.executeMerge = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const ffmpeg = require("fluent-ffmpeg");
const fs = __importStar(require("fs-extra"));
const utils_1 = require("../utils");
async function normalizeVideo(inputPath, refInfo) {
const normalizedPath = (0, utils_1.getTempFile)('.ts');
const mainCleanup = () => fs.remove(normalizedPath);
let silentAudioCleanup = null;
const hasAudio = await (0, utils_1.fileHasAudio)(inputPath);
const command = ffmpeg(inputPath);
if (!hasAudio) {
const duration = await (0, utils_1.getDuration)(inputPath);
const { filePath: silentAudioPath, cleanup } = await (0, utils_1.createSilentAudio)(duration);
command.addInput(silentAudioPath);
silentAudioCleanup = cleanup;
}
const targetWidth = refInfo.width;
const targetHeight = refInfo.height;
const targetSar = (refInfo.sample_aspect_ratio && refInfo.sample_aspect_ratio !== 'N/A') ? refInfo.sample_aspect_ratio : '1:1';
const targetFrameRate = refInfo.r_frame_rate || '30';
const targetPixFmt = 'yuv420p';
const videoFilter = `[0:v]scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:-1:-1:color=black,setsar=${targetSar},format=${targetPixFmt},fps=${targetFrameRate},setpts=PTS-STARTPTS[v_out]`;
const audioFilter = hasAudio
? `[0:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,asetpts=PTS-STARTPTS[a_out]`
: `[1:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,asetpts=PTS-STARTPTS[a_out]`;
command
.complexFilter([videoFilter, audioFilter])
.outputOptions(['-map', '[v_out]', '-map', '[a_out]'])
.videoCodec('libx264')
.audioCodec('aac')
.save(normalizedPath);
await (0, utils_1.runFfmpeg)(command);
const combinedCleanup = async () => {
await mainCleanup();
if (silentAudioCleanup) {
await silentAudioCleanup();
}
};
return { normalizedPath, cleanup: combinedCleanup };
}
async function executeMerge(inputs, outputFormat, itemIndex) {
// 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 });
}
if (inputs.length < 1) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Merge operation requires at least one source.', {
itemIndex,
});
}
const intermediateFiles = [];
let outputPath = null;
const finalCleanup = async () => {
for (const file of intermediateFiles) {
await file.cleanup();
}
};
try {
const videoInfos = await Promise.all(inputs.map(utils_1.getVideoStreamInfo));
const refInfo = videoInfos.find((info) => info !== undefined);
if (!refInfo || !refInfo.width || !refInfo.height) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Could not determine reference video properties for merging. At least one input must be a valid video.', { itemIndex });
}
// 1. Normalization Stage
for (const inputPath of inputs) {
const { normalizedPath, cleanup } = await normalizeVideo.call(this, inputPath, refInfo);
intermediateFiles.push({ path: normalizedPath, cleanup });
}
// 2. Merging Stage - Using the concat protocol for stability
outputPath = (0, utils_1.getTempFile)(`.${outputFormat}`);
const normalizedPaths = intermediateFiles.map((f) => f.path);
const concatString = `concat:${normalizedPaths.join('|')}`;
const command = ffmpeg()
.input(concatString)
.outputOptions('-c', 'copy')
.save(outputPath);
await (0, utils_1.runFfmpeg)(command);
return outputPath;
}
catch (error) {
// Clean up output file if creation failed
if (outputPath) {
await fs.remove(outputPath).catch(() => { });
}
await finalCleanup();
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error merging videos. Please ensure all source videos are valid. FFmpeg error: ${error.message}`, { itemIndex });
}
finally {
await finalCleanup();
}
}
exports.executeMerge = executeMerge;