UNPKG

n8n-nodes-mediafx

Version:

N8N custom nodes for video editing and media processing

142 lines (141 loc) 6.7 kB
"use strict"; 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;