n8n-nodes-mediafx
Version:
N8N custom nodes for video editing and media processing
142 lines (141 loc) • 6.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.executeMixAudio = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const utils_1 = require("../utils");
const ffmpeg = require("fluent-ffmpeg");
async function executeMixAudio(videoPath, audioPath, videoVolume, audioVolume, matchLength, advancedMixing, 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 });
}
const outputPath = (0, utils_1.getTempFile)('.mp4');
const { enablePartialMix = false, startTime = 0, duration, loop = false, enableFadeIn = false, fadeInDuration = 1, enableFadeOut = false, fadeOutDuration = 1, } = advancedMixing;
// Check if both inputs have audio
const videoHasAudio = await (0, utils_1.fileHasAudio)(videoPath);
const audioFileHasAudio = await (0, utils_1.fileHasAudio)(audioPath);
// If the "audio" file doesn't have audio, throw an error
if (!audioFileHasAudio) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'The secondary audio source does not contain any audio stream', { itemIndex });
}
// If video doesn't have audio, we'll create a silent audio track for it
let actualVideoPath = videoPath;
let videoCleanup = null;
if (!videoHasAudio) {
const videoDuration = await (0, utils_1.getDuration)(videoPath);
const { filePath: silentAudioPath, cleanup } = await (0, utils_1.createSilentAudio)(videoDuration);
videoCleanup = cleanup;
// Create a temporary video with silent audio
const tempVideoWithAudio = (0, utils_1.getTempFile)('.mp4');
const addSilentCommand = ffmpeg()
.input(videoPath)
.input(silentAudioPath)
.outputOptions(['-map', '0:v', '-map', '1:a', '-c:v', 'copy', '-shortest'])
.save(tempVideoWithAudio);
await (0, utils_1.runFfmpeg)(addSilentCommand);
await cleanup(); // Clean up the silent audio file
actualVideoPath = tempVideoWithAudio;
videoCleanup = () => require('fs-extra').remove(tempVideoWithAudio);
}
const command = ffmpeg().input(actualVideoPath).input(audioPath);
if (enablePartialMix) {
// Get audio duration to determine processing strategy
const audioDuration = await (0, utils_1.getDuration)(audioPath);
// If duration is not provided (null/undefined), use audio duration
const actualDuration = duration || audioDuration;
let audioProcessingChain = '[1:a]';
// Step 1: Handle looping if needed
if (loop && audioDuration < actualDuration) {
audioProcessingChain += 'aloop=loop=-1:size=2e9,';
}
// Step 2: Trim to duration
if (loop && audioDuration < actualDuration || audioDuration >= actualDuration) {
audioProcessingChain += `atrim=duration=${actualDuration},`;
}
// Step 3: Reset timestamps
audioProcessingChain += 'asetpts=PTS-STARTPTS,';
// Step 4: Apply fade effects
const fadeFilters = [];
if (enableFadeIn) {
fadeFilters.push(`afade=t=in:st=0:d=${fadeInDuration}`);
}
if (enableFadeOut) {
const fadeOutStart = Math.max(0, actualDuration - fadeOutDuration);
fadeFilters.push(`afade=t=out:st=${fadeOutStart}:d=${fadeOutDuration}`);
}
if (fadeFilters.length > 0) {
audioProcessingChain += fadeFilters.join(',') + ',';
}
// Step 5: Set volume and add delay
audioProcessingChain += `volume=${audioVolume},adelay=${startTime * 1000}|${startTime * 1000}[overlay_audio]`;
const filterComplex = audioProcessingChain + ';' +
`[0:a]volume=${videoVolume}[main_audio];` +
`[main_audio][overlay_audio]amix=inputs=2:duration=first:dropout_transition=0[mixed_audio]`;
command
.complexFilter(filterComplex)
.outputOptions(['-map', '0:v', '-map', '[mixed_audio]', '-c:v copy']);
}
else {
// Standard full audio mix with fade effects
let audioProcessingChain = '[1:a]';
// Calculate the effective duration based on matchLength
const videoDuration = await (0, utils_1.getDuration)(actualVideoPath);
const audioDuration = await (0, utils_1.getDuration)(audioPath);
let effectiveDuration;
switch (matchLength) {
case 'shortest':
effectiveDuration = Math.min(videoDuration, audioDuration);
break;
case 'longest':
effectiveDuration = Math.max(videoDuration, audioDuration);
break;
case 'first':
default:
effectiveDuration = videoDuration;
break;
}
// Apply fade effects if enabled
const fadeFilters = [];
if (enableFadeIn) {
fadeFilters.push(`afade=t=in:st=0:d=${fadeInDuration}`);
}
if (enableFadeOut) {
// Calculate fade out start based on effective duration, not original audio duration
const fadeOutStart = Math.max(0, effectiveDuration - fadeOutDuration);
fadeFilters.push(`afade=t=out:st=${fadeOutStart}:d=${fadeOutDuration}`);
}
if (fadeFilters.length > 0) {
audioProcessingChain += fadeFilters.join(',') + ',';
}
// Apply volume and create labeled output
audioProcessingChain += `volume=${audioVolume}[a1]`;
const filterComplex = audioProcessingChain + ';' +
`[0:a]volume=${videoVolume}[a0];` +
`[a0][a1]amix=inputs=2:duration=${matchLength}[a]`;
command
.complexFilter(filterComplex)
.outputOptions(['-map', '0:v', '-map', '[a]', '-c:v copy']);
}
command.save(outputPath);
try {
await (0, utils_1.runFfmpeg)(command);
// Clean up temporary video file if we created one
if (videoCleanup) {
await videoCleanup();
}
return outputPath;
}
catch (error) {
// Clean up output file if creation failed
await require('fs-extra').remove(outputPath).catch(() => { });
// Clean up temporary video file if we created one
if (videoCleanup) {
await videoCleanup().catch(() => { });
}
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error mixing audio: ${error.message}`, { itemIndex });
}
}
exports.executeMixAudio = executeMixAudio;