298 lines
11 KiB
JavaScript
298 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const util = require("util");
|
|
const Base = require("../structures/Base");
|
|
const DCAOpusTransformer = require("./streams/DCAOpusTransformer");
|
|
const FFmpegOggTransformer = require("./streams/FFmpegOggTransformer");
|
|
const FFmpegPCMTransformer = require("./streams/FFmpegPCMTransformer");
|
|
const FS = require("fs");
|
|
const HTTP = require("http");
|
|
const HTTPS = require("https");
|
|
const OggOpusTransformer = require("./streams/OggOpusTransformer");
|
|
const PassThroughStream = require("stream").PassThrough;
|
|
const PCMOpusTransformer = require("./streams/PCMOpusTransformer");
|
|
const Stream = require("stream").Stream;
|
|
const VolumeTransformer = require("./streams/VolumeTransformer");
|
|
const WebmOpusTransformer = require("./streams/WebmOpusTransformer");
|
|
|
|
let EventEmitter;
|
|
try {
|
|
EventEmitter = require("eventemitter3");
|
|
} catch(err) {
|
|
EventEmitter = require("events").EventEmitter;
|
|
}
|
|
|
|
class Piper extends EventEmitter {
|
|
constructor(converterCommand, opusFactory) {
|
|
super();
|
|
|
|
this.reset();
|
|
|
|
this.converterCommand = converterCommand;
|
|
this._dataPackets = [];
|
|
this._dataPacketMax = 30;
|
|
this._dataPacketMin = 15;
|
|
this.encoding = false;
|
|
this.libopus = true;
|
|
|
|
this.opusFactory = opusFactory;
|
|
this.opus = null;
|
|
|
|
this.volumeLevel = 1;
|
|
|
|
this._retransformer = [];
|
|
|
|
this.addDataPacket = this.addDataPacket.bind(this);
|
|
}
|
|
|
|
get dataPacketCount() {
|
|
return this._dataPackets.length;
|
|
}
|
|
|
|
addDataPacket(packet) {
|
|
if(!this.encoding) {
|
|
return;
|
|
}
|
|
if(this._dataPackets.push(packet) < this._dataPacketMax && this._endStream && this._endStream.manualCB) {
|
|
process.nextTick(() => {
|
|
if(this._endStream && this._endStream.manualCB) {
|
|
this._endStream.transformCB();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
encode(source, options) {
|
|
if(this.encoding || this.streams.length) {
|
|
this.emit("error", new Error("Already encoding"));
|
|
return false;
|
|
}
|
|
|
|
if(typeof source === "string") {
|
|
if(options.format === "dca" || options.format === "ogg" || options.format === "webm" || options.format === "pcm") {
|
|
if(source.startsWith("http://") || source.startsWith("https://")) {
|
|
const passThrough = new PassThroughStream();
|
|
if(source.startsWith("http://")) {
|
|
HTTP.get(source, (res) => res.pipe(passThrough)).once("error", (e) => this.stop(e));
|
|
} else {
|
|
HTTPS.get(source, (res) => res.pipe(passThrough)).once("error", (e) => this.stop(e));
|
|
}
|
|
source = passThrough;
|
|
} else {
|
|
try {
|
|
FS.statSync(source);
|
|
} catch(err) {
|
|
if(err.code === "ENOENT") {
|
|
this.emit("error", new Error("That file does not exist."));
|
|
} else {
|
|
this.emit("error", new Error("An error occured trying to access that file."));
|
|
}
|
|
this.reset();
|
|
return false;
|
|
}
|
|
source = FS.createReadStream(source);
|
|
}
|
|
}
|
|
} else if(!(source instanceof Stream) || !source.pipe) {
|
|
this.emit("error", new Error("Invalid source type"));
|
|
return false;
|
|
}
|
|
|
|
this._dataPacketMax = 30;
|
|
this._dataPacketMin = 15;
|
|
|
|
if(typeof source !== "string") {
|
|
this.streams.push(source.once("error", (e) => this.stop(e)));
|
|
}
|
|
|
|
if(options.format === "opusPackets") { // eslint-disable no-empty
|
|
} else if(options.format === "dca") {
|
|
this.streams.push(source.pipe(new DCAOpusTransformer()).once("error", (e) => this.stop(e)));
|
|
} else if(options.format === "ogg") {
|
|
this.streams.push(source.pipe(new OggOpusTransformer()).once("error", (e) => this.stop(e)));
|
|
} else if(options.format === "webm") {
|
|
this.streams.push(source.pipe(new WebmOpusTransformer()).once("error", (e) => this.stop(e)));
|
|
} else if(!options.format || options.format === "pcm") {
|
|
if(options.inlineVolume) {
|
|
if(!options.format) {
|
|
if(!this.converterCommand) {
|
|
this.emit("error", new Error("FFmpeg/avconv was not found on this system. Playback of this audio format is impossible"));
|
|
this.reset();
|
|
return false;
|
|
}
|
|
if(typeof source === "string") {
|
|
this.streams.push(source = new FFmpegPCMTransformer({
|
|
command: this.converterCommand,
|
|
input: source,
|
|
encoderArgs: options.encoderArgs,
|
|
inputArgs: options.inputArgs
|
|
}).once("error", (e) => this.stop(e)));
|
|
} else {
|
|
this.streams.push(source = source.pipe(new FFmpegPCMTransformer({
|
|
command: this.converterCommand,
|
|
encoderArgs: options.encoderArgs,
|
|
inputArgs: options.inputArgs
|
|
})).once("error", (e) => this.stop(e)));
|
|
}
|
|
}
|
|
this.streams.push(this.volume = source = source.pipe(new VolumeTransformer()).once("error", (e) => this.stop(e)));
|
|
this.volume.setVolume(this.volumeLevel);
|
|
this.streams.push(this.volume.pipe(new PCMOpusTransformer({
|
|
opusFactory: this.opusFactory,
|
|
frameSize: options.frameSize,
|
|
pcmSize: options.pcmSize
|
|
})).once("error", (e) => this.stop(e)));
|
|
this._dataPacketMax = 1; // Live volume updating
|
|
this._dataPacketMin = 4;
|
|
} else {
|
|
if(this.libopus) {
|
|
if(typeof source === "string") {
|
|
this.streams.push(source = new FFmpegOggTransformer({
|
|
command: this.converterCommand,
|
|
input: source,
|
|
encoderArgs: options.encoderArgs,
|
|
inputArgs: options.inputArgs,
|
|
format: options.format,
|
|
frameDuration: options.frameDuration
|
|
}).once("error", (e) => this.stop(e)));
|
|
} else {
|
|
this.streams.push(source = source.pipe(new FFmpegOggTransformer({
|
|
command: this.converterCommand,
|
|
encoderArgs: options.encoderArgs,
|
|
inputArgs: options.inputArgs,
|
|
format: options.format,
|
|
frameDuration: options.frameDuration
|
|
})).once("error", (e) => this.stop(e)));
|
|
}
|
|
this.streams.push(source.pipe(new OggOpusTransformer()).once("error", (e) => this.stop(e)));
|
|
} else {
|
|
if(typeof source === "string") {
|
|
this.streams.push(source = new FFmpegPCMTransformer({
|
|
command: this.converterCommand,
|
|
input: source,
|
|
encoderArgs: options.encoderArgs,
|
|
inputArgs: options.inputArgs
|
|
}).once("error", (e) => this.stop(e)));
|
|
} else {
|
|
this.streams.push(source = source.pipe(new FFmpegPCMTransformer({
|
|
command: this.converterCommand,
|
|
encoderArgs: options.encoderArgs,
|
|
inputArgs: options.inputArgs
|
|
})).once("error", (e) => this.stop(e)));
|
|
}
|
|
this.streams.push(source.pipe(new PCMOpusTransformer({
|
|
opusFactory: this.opusFactory,
|
|
frameSize: options.frameSize,
|
|
pcmSize: options.pcmSize
|
|
})).once("error", (e) => this.stop(e)));
|
|
}
|
|
}
|
|
} else {
|
|
this.emit("error", new Error("Unrecognized format"));
|
|
this.reset();
|
|
return false;
|
|
}
|
|
|
|
this._endStream = this.streams[this.streams.length - 1];
|
|
if(this._endStream.hasOwnProperty("manualCB")) {
|
|
this._endStream.manualCB = true;
|
|
}
|
|
|
|
this._endStream.on("data", this.addDataPacket);
|
|
this._endStream.once("end", () => this.stop(null, source));
|
|
|
|
this.emit("start");
|
|
|
|
return (this.encoding = true);
|
|
}
|
|
|
|
getDataPacket() {
|
|
if(this._dataPackets.length < this._dataPacketMin && this._endStream && this._endStream.manualCB) {
|
|
this._endStream.transformCB();
|
|
}
|
|
if(this._retransformer.length === 0) {
|
|
return this._dataPackets.shift();
|
|
} else {
|
|
// If we don't have an opus instance yet, create one.
|
|
if(!this.opus) {
|
|
this.opus = this.opusFactory();
|
|
}
|
|
|
|
const packet = this.opus.decode(this._dataPackets.shift());
|
|
for(let i = 0, num; i < packet.length - 1; i += 2) {
|
|
num = ~~(this._retransformer.shift() * packet.readInt16LE(i));
|
|
packet.writeInt16LE(num >= 32767 ? 32767 : num <= -32767 ? -32767 : num, i);
|
|
}
|
|
return this.opus.encode(packet, 3840 / 2 / 2);
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
if(this.streams) {
|
|
for(const stream of this.streams) {
|
|
if(typeof stream.destroy === "function") {
|
|
stream.destroy();
|
|
} else {
|
|
stream.unpipe();
|
|
}
|
|
}
|
|
}
|
|
|
|
this.streams = [];
|
|
this._endStream = null;
|
|
this.volume = null;
|
|
}
|
|
|
|
resetPackets() {
|
|
// We no longer need this to convert inline volume, so... let it go.
|
|
if(this.opus) {
|
|
this.opus.delete && this.opus.delete();
|
|
this.opus = null;
|
|
}
|
|
this._dataPackets = [];
|
|
}
|
|
|
|
setVolume(volume) {
|
|
this.volumeLevel = volume;
|
|
if(!this.volume) {
|
|
return;
|
|
}
|
|
this.volume.setVolume(volume);
|
|
}
|
|
|
|
stop(e, source) {
|
|
if(source && !this.streams.includes(source)) {
|
|
return;
|
|
}
|
|
|
|
if(e) {
|
|
this.emit("error", e);
|
|
}
|
|
|
|
if(this.throttleTimeout) {
|
|
clearTimeout(this.throttleTimeout);
|
|
this.throttleTimeout = null;
|
|
}
|
|
|
|
if(this.streams.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if(this._endStream) {
|
|
this._endStream.removeAllListeners("data");
|
|
}
|
|
|
|
this.reset();
|
|
if(this.encoding) {
|
|
this.encoding = false;
|
|
this.emit("stop");
|
|
}
|
|
}
|
|
|
|
[util.inspect.custom]() {
|
|
return Base.prototype[util.inspect.custom].call(this);
|
|
}
|
|
}
|
|
|
|
module.exports = Piper;
|