ffmpeg-toolkit
Version:
A modern FFmpeg toolkit for Node.js
158 lines • 5.5 kB
JavaScript
import { unlinkSync, writeFileSync } from 'fs-extra';
import { join } from 'path';
import { generateId } from '../utils/index.js';
export class AudioModule {
constructor(base) {
this.base = base;
}
async concatAudios(options) {
const inputs = options.inputPaths.map((path) => `file '${path}'`);
const tmpFile = join(this.base.getOutputFolder(), generateId('audio') + '.txt');
writeFileSync(tmpFile, inputs.join('\n'));
return await this.base.process({
callback: () => this.base
.ffmpeg()
.input(tmpFile)
.inputOptions(['-f', 'concat', '-safe', '0'])
.output(options.pathOutput),
data: {
outputPath: options.pathOutput,
},
onEnd: async () => unlinkSync(tmpFile),
onError: async () => unlinkSync(tmpFile),
});
}
async hasAudio(inputVideo) {
return await new Promise((resolve, reject) => {
this.base.ffmpeg.ffprobe(inputVideo, (err, data) => {
if (err) {
return reject(err);
}
const audioStream = data.streams.find((s) => s.codec_type === 'audio');
resolve(!!audioStream);
});
});
}
getMixAudioFilters(volumeScaled) {
return [
'[0:a]volume=1[a0]',
`[1:a]volume=${volumeScaled}[a1]`,
'[a0][a1]amix=inputs=2:duration=shortest[aout]',
];
}
getSingleAudioFilters(volumeScaled) {
return [`[1:a]volume=${volumeScaled}[aout]`];
}
getDefaultOutputOptions(needShortest = false) {
const options = [
'-map',
'0:v',
'-map',
'[aout]',
'-c:v',
'copy',
'-c:a',
'aac',
'-b:a',
'192k',
];
if (needShortest) {
options.push('-shortest');
}
return options;
}
async mergeAudioToVideo(options) {
const { inputPath, audioPath, pathOutput, volume = 1, isNoAudio = true } = options;
const volumeScaled = (volume ** 2).toFixed(2);
const hasAudio = await this.hasAudio(inputPath);
const command = this.base.ffmpeg().input(inputPath).input(audioPath);
let filters;
let needShortest = false;
if (isNoAudio) {
filters = this.getSingleAudioFilters(volumeScaled);
needShortest = true;
}
else if (hasAudio) {
filters = [
'[0:a]volume=1[a0]',
`[1:a]volume=${volumeScaled}[a1]`,
'[a0][a1]amix=inputs=2:duration=longest[aout]',
];
needShortest = true;
}
else {
filters = this.getSingleAudioFilters(volumeScaled);
needShortest = true;
}
command
.complexFilter(filters)
.outputOptions(this.getDefaultOutputOptions(needShortest))
.output(pathOutput);
return await this.base.process({
callback: () => command,
data: {
outputPath: pathOutput,
},
isDefaultOptions: false,
});
}
getAudioSettings(format) {
const settings = {
mp3: { format: 'mp3', codec: 'libmp3lame', bitrate: '192k' },
wav: { format: 'wav', codec: 'pcm_s16le', bitrate: '1411k' },
aac: { format: 'aac', codec: 'aac', bitrate: '192k' },
ogg: {
format: 'ogg',
codec: 'libvorbis',
bitrate: '192k',
options: ['-ac', '2', '-ar', '44100', '-q:a', '6'],
},
wma: {
format: 'asf',
codec: 'wmav2',
bitrate: '192k',
options: ['-ac', '2', '-ar', '44100', '-b:a', '192k'],
},
flac: { format: 'flac', codec: 'flac', bitrate: '0' },
m4a: { format: 'mp4', codec: 'aac', bitrate: '192k', options: ['-vn'] },
aiff: { format: 'aiff', codec: 'pcm_s16be', bitrate: '1411k' },
alac: {
format: 'alac',
codec: 'alac',
bitrate: '0',
options: ['-vn', '-ac', '2', '-ar', '44100', '-compression_level', '0'],
},
opus: { format: 'opus', codec: 'libopus', bitrate: '128k' },
};
return settings[format];
}
async convertAudio(options) {
const { inputPath, pathOutput, outputFormat } = options;
const command = this.base.ffmpeg().input(inputPath);
const settings = this.getAudioSettings(outputFormat);
command.toFormat(settings.format).audioCodec(settings.codec).audioBitrate(settings.bitrate);
if (settings.options) {
command.outputOptions(settings.options);
}
command.outputOptions([
'-y',
'-strict',
'experimental',
'-threads',
'4',
'-max_muxing_queue_size',
'8192',
'-movflags',
'+faststart',
'-shortest',
]);
return await this.base.process({
callback: () => command.output(pathOutput),
data: {
outputPath: pathOutput,
},
isDefaultOptions: false,
});
}
}
//# sourceMappingURL=audio.js.map