node-fdkaac
Version:
fdkaac/libfdk-aac AAC/M4A encoder for Node.js
496 lines • 18.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Fdkaac = void 0;
const FdkaacOptions_1 = require("./FdkaacOptions");
const fs_1 = require("fs");
const child_process_1 = require("child_process");
const events_1 = require("events");
/**
* Wrapper for fdkaac for Node
*
* @class Fdkaac
*/
class Fdkaac {
/**
* Creates an instance of Fdkaac and set all options
* @param {Options} options
*/
constructor(options) {
this.status = {
started: false,
finished: false,
progress: undefined,
eta: undefined
};
this.emitter = new events_1.EventEmitter();
this.options = options;
this.args = new FdkaacOptions_1.FdkaacOptions(this.options).getArguments();
}
/**
* Set file path of audio to encode
*
* @param {string} filePath
*/
setFile(path) {
if (!(0, fs_1.existsSync)(path)) {
throw new Error("Audio file (path) dose not exist");
}
this.filePath = path;
this.fileBuffer = undefined;
return this;
}
/**
* Set file buffer of audio to encode
*
* @param {Buffer} file
*
* @memberOf Fdkaac
*/
setBuffer(file) {
if (!Buffer.isBuffer(file)) {
throw new Error("Audio file (buffer) dose not exist");
}
this.fileBuffer = file;
this.filePath = undefined;
return this;
}
/**
* Get encoded file path
*
* @returns {string} Path of encoded/decoded file
*/
getFile() {
if (this.progressedFilePath == undefined) {
throw new Error("Audio is not yet encoded");
}
return this.progressedFilePath;
}
/**
* Get encoded file as buffer
*
* @returns {Buffer} Encoded/Decoded file
*/
getBuffer() {
if (this.progressedBuffer == undefined) {
throw new Error("Audio is not yet encoded");
}
return this.progressedBuffer;
}
/**
* Get event emitter
*
* @returns {EventEmitter}
*/
getEmitter() {
return this.emitter;
}
/**
* Get status of converter
*
* @returns {FdkaacStatus}
*/
getStatus() {
return this.status;
}
/**
* Encode audio file by fdkaac/libfdk-aac
*
* @return {Promise}
*/
encode() {
return this.progress("encode");
}
/**
* Decode audio file by ffmpeg
*
* @return {Promise}
*/
decode() {
return this.progress("decode");
}
/**
* Decode/Encode audio file
*
* @return {Promise}
*/
progress(type) {
if (this.filePath == undefined && this.fileBuffer == undefined) {
throw new Error("Audio file to encode is not set");
}
if (this.fileBuffer != undefined) {
// File buffer is set; write it as temp file
this.fileBufferTempFilePath = this.tempFilePathGenerator("raw", type);
return new Promise((resolve, reject) => {
(0, fs_1.writeFile)(this.fileBufferTempFilePath, this.fileBuffer, err => {
if (err) {
reject(err);
return;
}
resolve(this.fileBufferTempFilePath);
});
})
.then((file) => {
if (type == "encode") {
return this.execEncode(file);
}
else if (type == "decode") {
return this.execDecode(file);
}
else {
throw new Error("node-ffcaac can only 'encode' and 'decode'.");
}
})
.catch((error) => {
this.removeTempFilesOnError();
throw error;
});
}
else {
// File path is set
if (type == "encode") {
return this.execEncode(this.filePath).catch((error) => {
this.removeTempFilesOnError();
throw error;
});
}
else if (type == "decode") {
return this.execDecode(this.filePath).catch((error) => {
this.removeTempFilesOnError();
throw error;
});
}
else {
throw new Error("node-ffcaac can only 'encode' and 'decode'.");
}
}
}
/**
* Execute encoding via spawn fdkaac
*
* @private
* @param {string} inputFilePath Path of input file
*/
execEncode(inputFilePath) {
const args = this.args;
// Add input file to args
args.unshift(inputFilePath);
// Add output file to args, if not undefined in options
if (this.options.output == "buffer") {
const tempOutPath = this.tempFilePathGenerator("encoded", "encode");
args.push(`-o`);
args.push(`${tempOutPath}`);
// Set encoded file path
this.progressedBufferTempFilePath = tempOutPath;
}
else {
// Set encoded file path
this.progressedFilePath = this.options.output;
}
// Spawn instance of encoder and hook output methods
this.status.started = true;
this.status.finished = false;
this.status.progress = 0;
this.status.eta = undefined;
/**
* Handles output of stdout (and stderr)
* Parse data from output into object
*
* @param {(String | Buffer)} data
*/
const encoderStdout = (data) => {
data = data.toString().trim();
// Every output of fdkaac comes as "stderr". Deciding if it is an error or valid data by regex
if (data.length > 10) {
if (data.search("samples processed in") > -1) {
// processing done
this.status.finished = true;
this.status.progress = 100;
this.status.eta = "00:00";
this.emitter.emit("finish");
this.emitter.emit("progress", [
this.status.progress,
this.status.eta
]);
}
else if (data.search(/^\[(1{0,1}[0-9]{1,2})%\] /) > -1) {
// status of processing
const progressMatch = data.match(/^\[(1{0,1}[0-9]{1,2})%\] /);
const etaMatch = data.match(/ETA ([0-9][0-9]:[0-9][0-9])/);
const progress = String(progressMatch[1]);
let eta = null;
if (etaMatch != null) {
eta = etaMatch[1];
}
if (progress != null &&
Number(progress) > this.status.progress) {
this.status.progress = Number(progress);
}
if (eta != null) {
this.status.eta = eta;
}
this.emitter.emit("progress", [
this.status.progress,
this.status.eta
]);
}
else if (data.search(/ETA ([0-9][0-9]:[0-9][0-9])/) > -1) {
// line break of status, eta in next line
const etaMatch = data.match(/ETA ([0-9][0-9]:[0-9][0-9])/);
const eta = etaMatch[1];
if (eta != null) {
this.status.eta = eta;
}
}
else if (data.search(/\(([0-9][0-9]|[0-9])x\)/) > -1 ||
data.search(/^[0-9]{1,2}:[0-9]{1,2}.[0-9]{1,3}\/[0-9]{1,2}:[0-9]{1,2}.[0-9]{1,3}/) > -1) {
// line break of status, unknown in next line => do nothing
}
else {
// Unexpected output => error
if (data.search(/^fdkaac/) == -1) {
data = `fdkaac: ${data}`;
}
this.emitter.emit("error", String(data));
}
}
};
/**
* Handles error throw of fdkaac instance
*
* @param {Error} error
*/
const encoderError = (error) => {
this.emitter.emit("error", error);
};
const instance = (0, child_process_1.spawn)("fdkaac", args);
instance.stdout.on("data", encoderStdout);
instance.stderr.on("data", encoderStdout); // Most output, even non-errors, is on stderr
instance.on("error", encoderError);
// Return promise of finish encoding progress
return new Promise((resolve, reject) => {
this.emitter.on("finish", () => {
// If input was buffer, remove temp file
if (this.fileBufferTempFilePath != undefined) {
(0, fs_1.unlinkSync)(this.fileBufferTempFilePath);
}
// If output should be a buffer, load encoded audio file into object and remove temp file
if (this.options.output == "buffer") {
(0, fs_1.readFile)(this.progressedBufferTempFilePath, null, (error, data) => {
// Remove temp encoded file
(0, fs_1.unlinkSync)(this.progressedBufferTempFilePath);
if (error) {
reject(error);
return;
}
this.progressedBuffer = Buffer.from(data);
this.progressedBufferTempFilePath = undefined;
resolve(this);
});
}
else {
resolve(this);
}
});
this.emitter.on("error", error => {
reject(error);
});
});
}
/**
* Execute decoding via spawn ffmpeg
*
* @private
* @param {string} inputFilePath Path of input file
*/
execDecode(inputFilePath) {
const args = [];
args.push("-loglevel");
args.push("error");
args.push("-stats");
args.push("-i");
// Add input file to args
args.push(inputFilePath);
// Add output file to args, if not undefined in options
if (this.options.output == "buffer") {
const tempOutPath = `${this.tempFilePathGenerator("encoded", "decode")}.wav`;
args.push(`${tempOutPath}`);
// Set decode/encoded file path
this.progressedBufferTempFilePath = tempOutPath;
}
else {
// Set decode/encoded file path
this.progressedFilePath = this.options.output;
args.push(this.progressedFilePath);
}
// Exec ffmpeg to get duration of audio
let duration = null;
const argsDuration = [];
argsDuration.push("-i");
argsDuration.push(inputFilePath);
const instanceDuration = (0, child_process_1.spawn)("ffmpeg", argsDuration);
const durationStdout = (data) => {
if (duration) {
return;
}
data = data.toString().trim();
const match = data.match(/Duration\: [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}.[0-9]{1,2}/);
if (match && match[0]) {
const durationString = String(match[0])
.replace("Duration:", "")
.trim();
const durationArray = durationString.split(":");
duration =
Number(durationArray[0]) * 60 * 60 +
Number(durationArray[1]) * 60 +
Number(String(durationArray[2]).split(".")[0]);
}
};
const durationError = (error) => {
this.emitter.emit("error", error);
};
instanceDuration.stdout.on("data", durationStdout);
instanceDuration.stderr.on("data", durationStdout); // Most output, even non-errors, is on stderr
instanceDuration.on("error", durationError);
// Spawn instance of decoder and hook output methods
this.status.started = true;
this.status.finished = false;
this.status.progress = 0;
this.status.eta = undefined;
const decodeStartTime = new Date().getTime();
/**
* Handles output of stdout (and stderr)
* Parse data from output into object
*
* @param {(String | Buffer)} data
*/
const decoderStdout = (data) => {
data = data.toString().trim();
// Every output of ffmpeg comes as "stderr". Decoding if it is an error or valid data by regex
if (data.search(/^size\=/) > -1) {
// status of processing
if (!duration) {
// Duration as reference point for calculation of progress required
return;
}
const decodeTime = new Date().getTime() - decodeStartTime;
const match = data.match(/speed\= [0-9]{1,3}/);
if (match && match[0]) {
const speedFactor = Number(String(match[0])
.replace("speed=", "")
.trim());
let progress = Math.round((((decodeTime / 1000) * speedFactor) /
(duration / 100)) *
100) / 100;
progress = progress > 100 ? 100 : progress;
const eta = Math.ceil(((100 - progress) * (decodeTime / progress)) / 1000);
const etaString = `${("0" + String(Math.floor(eta / 60))).slice(-2)}:${("0" + String(eta % 60)).slice(-2)}`;
this.status.progress = progress;
this.status.eta = etaString;
this.emitter.emit("progress", [
this.status.progress,
this.status.eta
]);
}
}
else {
// Unexpected output => error
data = `fdkaac: ${data}`;
this.emitter.emit("error", String(data));
}
};
/**
* Handles error throw of ffmpeg instance
*
* @param {Error} error
*/
const decoderError = (error) => {
this.emitter.emit("error", error);
};
const decoderExit = (code) => {
if (code == 0) {
// Finished
this.status.finished = true;
this.status.progress = 100;
this.status.eta = "00:00";
this.emitter.emit("finish");
this.emitter.emit("progress", [
this.status.progress,
this.status.eta
]);
}
else {
this.emitter.emit("error", `fdkaac: unknown error while decoding`);
}
};
const instance = (0, child_process_1.spawn)("ffmpeg", args);
instance.stdout.on("data", decoderStdout);
instance.stderr.on("data", decoderStdout); // Most output, even non-errors, is on stderr
instance.on("error", decoderError);
instance.on("exit", decoderExit);
// Return promise of finish encoding progress
return new Promise((resolve, reject) => {
this.emitter.on("finish", () => {
// If input was buffer, remove temp file
if (this.fileBufferTempFilePath != undefined) {
(0, fs_1.unlinkSync)(this.fileBufferTempFilePath);
}
// If output should be a buffer, load encoded audio file into object and remove temp file
if (this.options.output == "buffer") {
(0, fs_1.readFile)(this.progressedBufferTempFilePath, null, (error, data) => {
// Remove temp encoded file
(0, fs_1.unlinkSync)(this.progressedBufferTempFilePath);
if (error) {
reject(error);
return;
}
this.progressedBuffer = Buffer.from(data);
this.progressedBufferTempFilePath = undefined;
resolve(this);
});
}
else {
resolve(this);
}
});
this.emitter.on("error", error => {
reject(error);
});
});
}
/**
* Generate temp file path
*
* @param {("raw" | "encoded")} type
* @returns {string} Path
*/
tempFilePathGenerator(type, progressType) {
const prefix = `${__dirname}/../.`;
let path = `${prefix}./temp/${type}/`;
let possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
path += possible.charAt(Math.floor(Math.random() * possible.length));
}
if (type == "raw" && progressType == "decode") {
path += `.mp3`;
}
if (!(0, fs_1.existsSync)(`${prefix}./temp/${path}`)) {
return path;
}
else {
return this.tempFilePathGenerator(type, progressType);
}
}
/**
* Remove temp files, if error occurred
*/
removeTempFilesOnError() {
if (this.fileBufferTempFilePath != undefined && (0, fs_1.existsSync)(this.fileBufferTempFilePath)) {
(0, fs_1.unlinkSync)(this.fileBufferTempFilePath);
}
if (this.progressedBufferTempFilePath != undefined && (0, fs_1.existsSync)(this.progressedBufferTempFilePath)) {
(0, fs_1.unlinkSync)(this.progressedBufferTempFilePath);
}
}
}
exports.Fdkaac = Fdkaac;
//# sourceMappingURL=Fdkaac.js.map