166 lines
3.8 KiB
JavaScript
166 lines
3.8 KiB
JavaScript
const { Readable } = require("stream");
|
|
const binding = require("./js-binding");
|
|
|
|
class OpusEncoder {
|
|
constructor(sampleRate, channels) {
|
|
return binding.OpusEncoder.create(sampleRate, channels);
|
|
}
|
|
|
|
static create(sampleRate, channels) {
|
|
return binding.OpusEncoder.create(sampleRate, channels);
|
|
}
|
|
|
|
static [Symbol.hasInstance](target) {
|
|
return target instanceof binding.OpusEncoder;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef StreamProbeResult
|
|
* @property {Readable} stream The source stream
|
|
* @property {binding.ProbeResult} result The probe result
|
|
*/
|
|
|
|
/**
|
|
* Attempt to probe a Readable stream
|
|
* @param {Readable} stream The readable stream to probe
|
|
* @param {Object} options The options
|
|
* @param {number} [options.probeSize=2 * 1024 * 1024] The amount of bytes to read from the stream. Defaults to 2 MB.
|
|
* @param {boolean} [options.sync=false] Whether to probe synchronously. Defaults to false.
|
|
* @returns {Promise<StreamProbeResult>}
|
|
*/
|
|
async function probeStream(
|
|
stream,
|
|
options = {
|
|
probeSize: 2 * 1024 * 1024,
|
|
sync: false,
|
|
},
|
|
) {
|
|
const { probeSize = 2 * 1024 * 1024, sync = false } = options;
|
|
return new Promise((resolve, reject) => {
|
|
if (stream.readableObjectMode) {
|
|
reject(new Error("Cannot probe a readable stream in object mode"));
|
|
return;
|
|
}
|
|
|
|
if (stream.readableEnded) {
|
|
reject(new Error("Cannot probe a stream that has ended"));
|
|
return;
|
|
}
|
|
|
|
let readBuffer = Buffer.alloc(0);
|
|
|
|
/**
|
|
* @type {binding.ProbeResult}
|
|
*/
|
|
let resolved = null;
|
|
|
|
const finish = (data) => {
|
|
stream.off("data", onData);
|
|
stream.off("close", onClose);
|
|
stream.off("end", onClose);
|
|
stream.pause();
|
|
|
|
resolved = data;
|
|
|
|
if (stream.readableEnded) {
|
|
resolve({
|
|
stream: Readable.from(readBuffer),
|
|
result: data,
|
|
});
|
|
} else {
|
|
if (readBuffer.length > 0) {
|
|
stream.push(readBuffer);
|
|
}
|
|
|
|
resolve({
|
|
stream,
|
|
result: data,
|
|
});
|
|
}
|
|
};
|
|
|
|
const onClose = async () => {
|
|
try {
|
|
const probeInner = sync ? probeSync : probe;
|
|
const result = await probeInner(readBuffer);
|
|
if (result != null) resolved = result;
|
|
} catch {}
|
|
|
|
finish(resolved || null);
|
|
};
|
|
|
|
const onData = (buffer) => {
|
|
readBuffer = Buffer.concat([readBuffer, buffer]);
|
|
|
|
if (readBuffer.length >= probeSize) {
|
|
stream.off("data", onData);
|
|
stream.pause();
|
|
process.nextTick(onClose);
|
|
}
|
|
};
|
|
|
|
stream.once("error", reject);
|
|
stream.on("data", onData);
|
|
stream.once("close", onClose);
|
|
stream.once("end", onClose);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reads metadata tags
|
|
* @param {import('./js-binding').ProbeResult} result The probe result to read metadata from
|
|
*/
|
|
function readMetadata(result) {
|
|
const res = {
|
|
title: null,
|
|
author: null,
|
|
album: null,
|
|
genre: null,
|
|
year: null,
|
|
duration: null,
|
|
composer: null,
|
|
bpm: null,
|
|
};
|
|
|
|
if (!result.metadata?.length) return res;
|
|
|
|
result.metadata.forEach((m) => {
|
|
if (!m.value) return;
|
|
|
|
if (m.name === "TIT2") {
|
|
res.title = m.value;
|
|
} else if (m.name === "TPE1" || m.name === "TPUB") {
|
|
res.author = m.value;
|
|
} else if (m.name === "TALB") {
|
|
res.album = m.value;
|
|
} else if (m.name === "TCON") {
|
|
res.genre = m.value;
|
|
} else if (m.name === "TYER") {
|
|
res.year = m.value;
|
|
} else if (m.name === "TLEN") {
|
|
res.duration = parseInt(m.value);
|
|
} else if (m.name === "TCOM") {
|
|
res.composer = m.value;
|
|
} else if (m.name === "TBPM") {
|
|
res.bpm = parseInt(m.value);
|
|
}
|
|
});
|
|
|
|
return res;
|
|
}
|
|
|
|
const { CodecType, probe, probeSync, getOpusVersion } = binding;
|
|
const { version } = require("./package.json");
|
|
|
|
module.exports = {
|
|
CodecType,
|
|
probe,
|
|
probeSync,
|
|
probeStream,
|
|
readMetadata,
|
|
OpusEncoder,
|
|
getOpusVersion,
|
|
version,
|
|
};
|