2023-02-13 12:41:30 +00:00
|
|
|
import { Readable } from "stream";
|
|
|
|
|
import ffmpeg from "fluent-ffmpeg";
|
|
|
|
|
import * as R from "remeda";
|
|
|
|
|
|
|
|
|
|
const requiredCodecs = ["mp3", "webm", "wav"];
|
|
|
|
|
|
|
|
|
|
export interface AudioConvertOpts {
|
|
|
|
|
bitrate?: string;
|
|
|
|
|
audioCodec?: string;
|
|
|
|
|
format?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultAudioConvertOpts = {
|
|
|
|
|
bitrate: "32k",
|
|
|
|
|
audioCodec: "libmp3lame",
|
|
|
|
|
format: "mp3",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Converts an audio file to a different format. defaults to converting to mp3 with a 32k bitrate using the libmp3lame codec
|
|
|
|
|
*
|
|
|
|
|
* @param input the buffer containing the binary data of the input file
|
|
|
|
|
* @param opts options to control how the audio file is converted
|
|
|
|
|
* @return resolves to a buffer containing the binary data of the converted file
|
|
|
|
|
**/
|
|
|
|
|
export const convert = (
|
|
|
|
|
input: Buffer,
|
2025-01-22 17:50:38 +01:00
|
|
|
opts?: AudioConvertOpts,
|
2023-02-13 12:41:30 +00:00
|
|
|
): Promise<Buffer> => {
|
|
|
|
|
const settings = { ...defaultAudioConvertOpts, ...opts };
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const stream = Readable.from(input);
|
|
|
|
|
let out = Buffer.alloc(0);
|
|
|
|
|
const cmd = ffmpeg(stream)
|
|
|
|
|
.audioCodec(settings.audioCodec)
|
|
|
|
|
.audioBitrate(settings.bitrate)
|
|
|
|
|
.toFormat(settings.format)
|
2025-01-22 17:50:38 +01:00
|
|
|
.on("error", (err, _stdout, _stderr) => {
|
2023-02-13 12:41:30 +00:00
|
|
|
console.error(err.message);
|
|
|
|
|
reject(err);
|
|
|
|
|
})
|
|
|
|
|
.on("end", () => {
|
|
|
|
|
resolve(out);
|
|
|
|
|
});
|
|
|
|
|
const outstream = cmd.pipe();
|
|
|
|
|
outstream.on("data", (chunk: Buffer) => {
|
|
|
|
|
out = Buffer.concat([out, chunk]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if ffmpeg is installed and usable. Checks for required codecs and a working ffmpeg installation.
|
|
|
|
|
*
|
|
|
|
|
* @return resolves to true if ffmpeg is installed and usable
|
|
|
|
|
* */
|
|
|
|
|
export const selfCheck = (): Promise<boolean> => {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
ffmpeg.getAvailableFormats((err, codecs) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
console.error("FFMPEG error:", err);
|
|
|
|
|
resolve(false);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-22 17:50:38 +01:00
|
|
|
const preds = R.map(
|
|
|
|
|
requiredCodecs,
|
|
|
|
|
(codec) => (available: any) =>
|
|
|
|
|
available[codec] &&
|
|
|
|
|
available[codec].canDemux &&
|
|
|
|
|
available[codec].canMux,
|
2023-02-13 12:41:30 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
resolve(R.allPass(codecs, preds));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const assertFfmpegAvailable = async (): Promise<void> => {
|
|
|
|
|
const r = await selfCheck();
|
|
|
|
|
if (!r)
|
|
|
|
|
throw new Error(
|
2025-01-22 17:50:38 +01:00
|
|
|
`ffmpeg is not installed, could not be located, or does not support the required codecs: ${requiredCodecs}`,
|
2023-02-13 12:41:30 +00:00
|
|
|
);
|
|
|
|
|
};
|