From 343dda7b3c54627727cb2fd137f322c302e8b85e Mon Sep 17 00:00:00 2001 From: Vasyl Ilba Date: Mon, 26 Feb 2024 14:56:49 +0200 Subject: [PATCH] adds audio option to video generation process --- cli/displayAdsRecorder.js | 6 +++++- src/index.js | 19 +++++++++++++++- src/util/addAudioToVideo.js | 37 ++++++++++++++++++++++++++++++++ src/util/renderVideoFromFiles.js | 1 + 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 src/util/addAudioToVideo.js diff --git a/cli/displayAdsRecorder.js b/cli/displayAdsRecorder.js index a751f28..b1415ab 100755 --- a/cli/displayAdsRecorder.js +++ b/cli/displayAdsRecorder.js @@ -20,6 +20,8 @@ const findAdsInDirectory = require("../src/util/findAdsInDirectory"); new program.Option("-g, --gif [loop]", "If you want to output animated gifs and loop them or play once").choices(['once', 'loop']) ) .option("-m, --mp4", "If you want to output video") + .option("-au, --audio ", "If you want to add audio") + .option("-v, --volume ", "When adding audio you can specify volume") .addOption( new program.Option("-f, --fps ", "fps for gif and/or mp4").choices(['15', '30', '60']) ) @@ -64,7 +66,9 @@ const findAdsInDirectory = require("../src/util/findAdsInDirectory"); output: outputChoices.map(e => e.value).filter(e => options[e]), gifLoopOptions: gifLoopOptionsMap[options.gif], fps: options.fps, - jpgMaxFileSize: options.jpg + jpgMaxFileSize: options.jpg, + audio: options.audio, + volume: options.volume, } // inquirer questions diff --git a/src/index.js b/src/index.js index 58d2900..e796acf 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ const recordAd = require("./util/recordDisplayAd"); +const addAudioToVideo = require("./util/addAudioToVideo"); const renderVideo = require("./util/renderVideoFromFiles"); const getBackupImage = require("./util/getBackupImage"); const renderGIf = require("./util/renderGifFromVideoFile"); @@ -43,6 +44,10 @@ module.exports = async function displayAdsRecorder(options, chunkSize = 10) { await runWithChunks(recordGif, "making GIFs") } + if (adSelection.output.includes("mp4") && adSelection.audio) { + await runWithChunks(addAudio, "adding audio track to videos") + } + await runWithChunks(clearScreenshots, "clearing tmp files") } @@ -99,6 +104,18 @@ module.exports = async function displayAdsRecorder(options, chunkSize = 10) { }); } + async function addAudio(adLocation) { + const [url, htmlBaseDirName] = urlFromAdLocation(adLocation); + + const video = path.join(targetDir, `${htmlBaseDirName}.mp4`) + const audio = path.join(targetDir, adSelection.audio) + await addAudioToVideo( + video, + audio, + adSelection.volume + ); + } + async function clearScreenshots(adLocation) { const screenshots = path.join(path.dirname(adLocation), ".cache/"); await fs.rm(screenshots, { force: true, recursive: true }) @@ -108,7 +125,7 @@ module.exports = async function displayAdsRecorder(options, chunkSize = 10) { const startTime = new Date().getTime(); const progressBar = new cliProgress.SingleBar({ format: - `${name}${' '.repeat(25 - name.length)}[{bar}] {percentage}% | ETA: {eta}s | {value}/{total}`, + `${name}${' '.repeat(30 - name.length)}[{bar}] {percentage}% | ETA: {eta}s | {value}/{total}`, }, cliProgress.Presets.shades_classic); progressBar.start(adSelection.location.length, 0); diff --git a/src/util/addAudioToVideo.js b/src/util/addAudioToVideo.js new file mode 100644 index 0000000..8b1ef52 --- /dev/null +++ b/src/util/addAudioToVideo.js @@ -0,0 +1,37 @@ +const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path +const ffmpeg = require("fluent-ffmpeg") +const fs = require('fs/promises') +ffmpeg.setFfmpegPath(ffmpegPath) + +module.exports = async function addAudioToVideo( + video, + audio, + volume = 1 +) { + const output = video.replace('.mp4', '-audio.mp4') + + return new Promise((resolve, reject) => { + ffmpeg() + .addInput(video) + .addInput(audio) + .withOptions([ + `-af volume=${volume}`, + // '-filter:a loudnorm' // normalize audio volume + ]) + .output(output) + .outputOptions([ + '-map 0:v', + '-map 1:a', + '-c:v copy', + '-shortest' // comment if your audio is shorter + ]) + .on('error', reject) + .on("end", async () => { + // since we can't save to the same file we're working with - using tmp file + await fs.copyFile(output, video) + await fs.unlink(output) + resolve(video); + }) + .run() + }) +} diff --git a/src/util/renderVideoFromFiles.js b/src/util/renderVideoFromFiles.js index addeba5..3ff76c1 100644 --- a/src/util/renderVideoFromFiles.js +++ b/src/util/renderVideoFromFiles.js @@ -18,6 +18,7 @@ module.exports = async function renderVideoFromFiles( process.fps(fps); process.videoBitrate(10000); process.output(output); + process.outputOptions(['-pix_fmt yuv420p']) process.on("end", () => { resolve(output); });