mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-17 12:03:31 +01:00
Audio bitrate support (#1439)
* Audio bitrate support Note: not implemented for VoiceBroadcasts * Fix default args, auto bitrate * Late night typos are the best * Changes bitrate to kbps for VoiceChannel stuff * Add methods to manipulate bitrate while encoding
This commit is contained in:
@@ -143,8 +143,8 @@ class VoiceBroadcast extends VolumeInterface {
|
|||||||
* })
|
* })
|
||||||
* .catch(console.error);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
playStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
playStream(stream, options = {}) {
|
||||||
const options = { seek, volume, passes, stream };
|
this.setVolume(options.volume || 1);
|
||||||
return this._playTranscodable(stream, options);
|
return this._playTranscodable(stream, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,19 +164,17 @@ class VoiceBroadcast extends VolumeInterface {
|
|||||||
* })
|
* })
|
||||||
* .catch(console.error);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
playFile(file, { seek = 0, volume = 1, passes = 1 } = {}) {
|
playFile(file, options = {}) {
|
||||||
const options = { seek, volume, passes };
|
this.setVolume(options.volume || 1);
|
||||||
return this._playTranscodable(`file:${file}`, options);
|
return this._playTranscodable(`file:${file}`, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
_playTranscodable(media, options) {
|
_playTranscodable(media, options) {
|
||||||
OpusEncoders.guaranteeOpusEngine();
|
|
||||||
|
|
||||||
this.killCurrentTranscoder();
|
this.killCurrentTranscoder();
|
||||||
const transcoder = this.prism.transcode({
|
const transcoder = this.prism.transcode({
|
||||||
type: 'ffmpeg',
|
type: 'ffmpeg',
|
||||||
media,
|
media,
|
||||||
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek)]),
|
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
|
||||||
});
|
});
|
||||||
/**
|
/**
|
||||||
* Emitted whenever an error occurs.
|
* Emitted whenever an error occurs.
|
||||||
@@ -206,31 +204,28 @@ class VoiceBroadcast extends VolumeInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays a stream of 16-bit signed stereo PCM at 48KHz.
|
* Plays a stream of 16-bit signed stereo PCM.
|
||||||
* @param {ReadableStream} stream The audio stream to play
|
* @param {ReadableStream} stream The audio stream to play
|
||||||
* @param {StreamOptions} [options] Options for playing the stream
|
* @param {StreamOptions} [options] Options for playing the stream
|
||||||
* @returns {VoiceBroadcast}
|
* @returns {VoiceBroadcast}
|
||||||
*/
|
*/
|
||||||
playConvertedStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
playConvertedStream(stream, options = {}) {
|
||||||
OpusEncoders.guaranteeOpusEngine();
|
|
||||||
|
|
||||||
this.killCurrentTranscoder();
|
this.killCurrentTranscoder();
|
||||||
const options = { seek, volume, passes, stream };
|
this.setVolume(options.volume || 1);
|
||||||
this.currentTranscoder = { options };
|
this.currentTranscoder = { options: { stream } };
|
||||||
stream.once('readable', () => this._startPlaying());
|
stream.once('readable', () => this._startPlaying());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays an Opus encoded stream at 48KHz.
|
* Plays an Opus encoded stream.
|
||||||
* <warn>Note that inline volume is not compatible with this method.</warn>
|
* <warn>Note that inline volume is not compatible with this method.</warn>
|
||||||
* @param {ReadableStream} stream The Opus audio stream to play
|
* @param {ReadableStream} stream The Opus audio stream to play
|
||||||
* @param {StreamOptions} [options] Options for playing the stream
|
* @param {StreamOptions} [options] Options for playing the stream
|
||||||
* @returns {StreamDispatcher}
|
* @returns {StreamDispatcher}
|
||||||
*/
|
*/
|
||||||
playOpusStream(stream, { seek = 0, passes = 1 } = {}) {
|
playOpusStream(stream) {
|
||||||
const options = { seek, passes, stream };
|
this.currentTranscoder = { options: { stream }, opus: true };
|
||||||
this.currentTranscoder = { options, opus: true };
|
|
||||||
stream.once('readable', () => this._startPlaying());
|
stream.once('readable', () => this._startPlaying());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -241,8 +236,9 @@ class VoiceBroadcast extends VolumeInterface {
|
|||||||
* @param {StreamOptions} [options] Options for playing the stream
|
* @param {StreamOptions} [options] Options for playing the stream
|
||||||
* @returns {VoiceBroadcast}
|
* @returns {VoiceBroadcast}
|
||||||
*/
|
*/
|
||||||
playArbitraryInput(input, { seek = 0, volume = 1, passes = 1 } = {}) {
|
playArbitraryInput(input, options = {}) {
|
||||||
const options = { seek, volume, passes, input };
|
this.setVolume(options.volume || 1);
|
||||||
|
options.input = input;
|
||||||
return this._playTranscodable(input, options);
|
return this._playTranscodable(input, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -431,6 +431,8 @@ class VoiceConnection extends EventEmitter {
|
|||||||
* @property {number} [seek=0] The time to seek to
|
* @property {number} [seek=0] The time to seek to
|
||||||
* @property {number} [volume=1] The volume to play at
|
* @property {number} [volume=1] The volume to play at
|
||||||
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
|
* @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
|
||||||
|
* @property {number|string} [bitrate=48000] The bitrate (quality) of the audio.
|
||||||
|
* If set to 'auto', the voice channel's bitrate will be used
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -481,7 +483,7 @@ class VoiceConnection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays a stream of 16-bit signed stereo PCM at 48KHz.
|
* Plays a stream of 16-bit signed stereo PCM.
|
||||||
* @param {ReadableStream} stream The audio stream to play
|
* @param {ReadableStream} stream The audio stream to play
|
||||||
* @param {StreamOptions} [options] Options for playing the stream
|
* @param {StreamOptions} [options] Options for playing the stream
|
||||||
* @returns {StreamDispatcher}
|
* @returns {StreamDispatcher}
|
||||||
@@ -491,7 +493,7 @@ class VoiceConnection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays an Opus encoded stream at 48KHz.
|
* Plays an Opus encoded stream.
|
||||||
* <warn>Note that inline volume is not compatible with this method.</warn>
|
* <warn>Note that inline volume is not compatible with this method.</warn>
|
||||||
* @param {ReadableStream} stream The Opus audio stream to play
|
* @param {ReadableStream} stream The Opus audio stream to play
|
||||||
* @param {StreamOptions} [options] Options for playing the stream
|
* @param {StreamOptions} [options] Options for playing the stream
|
||||||
@@ -504,6 +506,7 @@ class VoiceConnection extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Plays a voice broadcast.
|
* Plays a voice broadcast.
|
||||||
* @param {VoiceBroadcast} broadcast The broadcast to play
|
* @param {VoiceBroadcast} broadcast The broadcast to play
|
||||||
|
* @param {StreamOptions} [options] Options for playing the stream
|
||||||
* @returns {StreamDispatcher}
|
* @returns {StreamDispatcher}
|
||||||
* @example
|
* @example
|
||||||
* // Play a broadcast
|
* // Play a broadcast
|
||||||
@@ -512,8 +515,8 @@ class VoiceConnection extends EventEmitter {
|
|||||||
* .playFile('./test.mp3');
|
* .playFile('./test.mp3');
|
||||||
* const dispatcher = voiceConnection.playBroadcast(broadcast);
|
* const dispatcher = voiceConnection.playBroadcast(broadcast);
|
||||||
*/
|
*/
|
||||||
playBroadcast(broadcast) {
|
playBroadcast(broadcast, options) {
|
||||||
return this.player.playBroadcast(broadcast);
|
return this.player.playBroadcast(broadcast, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -119,6 +119,16 @@ class StreamDispatcher extends VolumeInterface {
|
|||||||
this.emit('speaking', value);
|
this.emit('speaking', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the bitrate of the current Opus encoder.
|
||||||
|
* @param {number} bitrate New bitrate, in kbps.
|
||||||
|
* If set to 'auto', the voice channel's bitrate will be used
|
||||||
|
*/
|
||||||
|
setBitrate(bitrate) {
|
||||||
|
this.player.setBitrate(bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
sendBuffer(buffer, sequence, timestamp, opusPacket) {
|
sendBuffer(buffer, sequence, timestamp, opusPacket) {
|
||||||
opusPacket = opusPacket || this.player.opusEncoder.encode(buffer);
|
opusPacket = opusPacket || this.player.opusEncoder.encode(buffer);
|
||||||
const packet = this.createPacket(sequence, timestamp, opusPacket);
|
const packet = this.createPacket(sequence, timestamp, opusPacket);
|
||||||
|
|||||||
@@ -4,21 +4,38 @@
|
|||||||
*/
|
*/
|
||||||
class BaseOpus {
|
class BaseOpus {
|
||||||
/**
|
/**
|
||||||
* @param {Object} [options] The options to apply to the Opus engine
|
* @param {Object} [options] The options to apply to the Opus engine.
|
||||||
* @param {boolean} [options.fec] Whether to enable forward error correction (defaults to false)
|
* @param {number} [options.bitrate=48] The desired bitrate (kbps).
|
||||||
* @param {number} [options.plp] The expected packet loss percentage (0-1 inclusive, defaults to 0)
|
* @param {boolean} [options.fec=false] Whether to enable forward error correction.
|
||||||
|
* @param {number} [options.plp=0] The expected packet loss percentage.
|
||||||
*/
|
*/
|
||||||
constructor(options = {}) {
|
constructor({ bitrate = 48, fec = false, plp = 0 } = {}) {
|
||||||
this.ctl = {
|
this.ctl = {
|
||||||
|
BITRATE: 4002,
|
||||||
FEC: 4012,
|
FEC: 4012,
|
||||||
PLP: 4014,
|
PLP: 4014,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.options = options;
|
this.samplingRate = 48000;
|
||||||
|
this.channels = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The desired bitrate (kbps)
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.bitrate = bitrate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Miscellaneous Opus options
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
this.options = { fec, plp };
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
try {
|
try {
|
||||||
|
this.setBitrate(this.bitrate);
|
||||||
|
|
||||||
// Set FEC (forward error correction)
|
// Set FEC (forward error correction)
|
||||||
if (this.options.fec) this.setFEC(this.options.fec);
|
if (this.options.fec) this.setFEC(this.options.fec);
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ class NodeOpusEngine extends OpusEngine {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
this.encoder = new opus.OpusEncoder(48000, 2);
|
this.encoder = new opus.OpusEncoder(this.samplingRate, this.channels);
|
||||||
super.init();
|
super.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBitrate(bitrate) {
|
||||||
|
this.encoder.applyEncoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
setFEC(enabled) {
|
setFEC(enabled) {
|
||||||
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
|
this.encoder.applyEncoderCTL(this.ctl.FEC, enabled ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ const list = [
|
|||||||
require('./OpusScriptEngine'),
|
require('./OpusScriptEngine'),
|
||||||
];
|
];
|
||||||
|
|
||||||
let opusEngineFound;
|
|
||||||
|
|
||||||
function fetch(Encoder, engineOptions) {
|
function fetch(Encoder, engineOptions) {
|
||||||
try {
|
try {
|
||||||
return new Encoder(engineOptions);
|
return new Encoder(engineOptions);
|
||||||
@@ -25,10 +23,9 @@ exports.fetch = engineOptions => {
|
|||||||
const fetched = fetch(encoder, engineOptions);
|
const fetched = fetch(encoder, engineOptions);
|
||||||
if (fetched) return fetched;
|
if (fetched) return fetched;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.guaranteeOpusEngine = () => {
|
exports.guaranteeOpusEngine = () => {
|
||||||
if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch());
|
if (typeof opusEngineFound === 'undefined') opusEngineFound = Boolean(exports.fetch());
|
||||||
if (!opusEngineFound) throw new Error('Couldn\'t find an Opus engine.');
|
if (!opusEngineFound) throw new Error('Couldn\'t find an Opus engine.');
|
||||||
|
throw new Error('OPUS_ENGINE_MISSING');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ class OpusScriptEngine extends OpusEngine {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
this.encoder = new OpusScript(48000, 2);
|
this.encoder = new OpusScript(this.samplingRate, this.channels);
|
||||||
super.init();
|
super.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBitrate(bitrate) {
|
||||||
|
this.encoder.encoderCTL(this.ctl.BITRATE, Math.min(128, Math.max(16, bitrate)) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
setFEC(enabled) {
|
setFEC(enabled) {
|
||||||
this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0);
|
this.encoder.encoderCTL(this.ctl.FEC, enabled ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ class AudioPlayer extends EventEmitter {
|
|||||||
* @type {Prism}
|
* @type {Prism}
|
||||||
*/
|
*/
|
||||||
this.prism = new Prism();
|
this.prism = new Prism();
|
||||||
/**
|
|
||||||
* The opus encoder that the player uses
|
|
||||||
* @type {NodeOpusEngine|OpusScriptEngine}
|
|
||||||
*/
|
|
||||||
this.opusEncoder = OpusEncoders.fetch();
|
|
||||||
this.streams = new Collection();
|
this.streams = new Collection();
|
||||||
this.currentStream = {};
|
this.currentStream = {};
|
||||||
this.streamingData = {
|
this.streamingData = {
|
||||||
@@ -67,6 +62,7 @@ class AudioPlayer extends EventEmitter {
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.opusEncoder) this.opusEncoder.destroy();
|
if (this.opusEncoder) this.opusEncoder.destroy();
|
||||||
|
this.opusEncoder = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroyCurrentStream() {
|
destroyCurrentStream() {
|
||||||
@@ -83,13 +79,25 @@ class AudioPlayer extends EventEmitter {
|
|||||||
this.currentStream = {};
|
this.currentStream = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
/**
|
||||||
OpusEncoders.guaranteeOpusEngine();
|
* Set the bitrate of the current Opus encoder.
|
||||||
const options = { seek, volume, passes };
|
* @param {number} value New bitrate, in kbps.
|
||||||
|
* If set to 'auto', the voice channel's bitrate will be used
|
||||||
|
*/
|
||||||
|
setBitrate(value) {
|
||||||
|
if (!value) return;
|
||||||
|
if (!this.opusEncoder) return;
|
||||||
|
const bitrate = value === 'auto' ? this.voiceConnection.channel.bitrate : value;
|
||||||
|
this.opusEncoder.setBitrate(bitrate);
|
||||||
|
}
|
||||||
|
|
||||||
|
playUnknownStream(stream, options = {}) {
|
||||||
|
this.destroy();
|
||||||
|
this.opusEncoder = OpusEncoders.fetch(options);
|
||||||
const transcoder = this.prism.transcode({
|
const transcoder = this.prism.transcode({
|
||||||
type: 'ffmpeg',
|
type: 'ffmpeg',
|
||||||
media: stream,
|
media: stream,
|
||||||
ffmpegArguments: ffmpegArguments.concat(['-ss', String(seek)]),
|
ffmpegArguments: ffmpegArguments.concat(['-ss', String(options.seek || 0)]),
|
||||||
});
|
});
|
||||||
this.destroyCurrentStream();
|
this.destroyCurrentStream();
|
||||||
this.currentStream = {
|
this.currentStream = {
|
||||||
@@ -105,9 +113,10 @@ class AudioPlayer extends EventEmitter {
|
|||||||
return this.playPCMStream(transcoder.output, options, true);
|
return this.playPCMStream(transcoder.output, options, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}, fromUnknown = false) {
|
playPCMStream(stream, options = {}, fromUnknown = false) {
|
||||||
OpusEncoders.guaranteeOpusEngine();
|
this.destroy();
|
||||||
const options = { seek, volume, passes };
|
this.opusEncoder = OpusEncoders.fetch(options);
|
||||||
|
this.setBitrate(options.bitrate);
|
||||||
const dispatcher = this.createDispatcher(stream, options);
|
const dispatcher = this.createDispatcher(stream, options);
|
||||||
if (fromUnknown) {
|
if (fromUnknown) {
|
||||||
this.currentStream.dispatcher = dispatcher;
|
this.currentStream.dispatcher = dispatcher;
|
||||||
@@ -122,8 +131,8 @@ class AudioPlayer extends EventEmitter {
|
|||||||
return dispatcher;
|
return dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
playOpusStream(stream, { seek = 0, passes = 1 } = {}) {
|
playOpusStream(stream, options = {}) {
|
||||||
const options = { seek, passes, opus: true };
|
options.opus = true;
|
||||||
this.destroyCurrentStream();
|
this.destroyCurrentStream();
|
||||||
const dispatcher = this.createDispatcher(stream, options);
|
const dispatcher = this.createDispatcher(stream, options);
|
||||||
this.currentStream = {
|
this.currentStream = {
|
||||||
@@ -134,8 +143,7 @@ class AudioPlayer extends EventEmitter {
|
|||||||
return dispatcher;
|
return dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
playBroadcast(broadcast, { volume = 1, passes = 1 } = {}) {
|
playBroadcast(broadcast, options) {
|
||||||
const options = { volume, passes };
|
|
||||||
this.destroyCurrentStream();
|
this.destroyCurrentStream();
|
||||||
const dispatcher = this.createDispatcher(broadcast, options);
|
const dispatcher = this.createDispatcher(broadcast, options);
|
||||||
this.currentStream = {
|
this.currentStream = {
|
||||||
@@ -148,7 +156,9 @@ class AudioPlayer extends EventEmitter {
|
|||||||
return dispatcher;
|
return dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
createDispatcher(stream, options) {
|
createDispatcher(stream, { seek = 0, volume = 1, passes = 1 } = {}) {
|
||||||
|
const options = { seek, volume, passes };
|
||||||
|
|
||||||
const dispatcher = new StreamDispatcher(this, stream, options);
|
const dispatcher = new StreamDispatcher(this, stream, options);
|
||||||
dispatcher.on('end', () => this.destroyCurrentStream());
|
dispatcher.on('end', () => this.destroyCurrentStream());
|
||||||
dispatcher.on('error', () => this.destroyCurrentStream());
|
dispatcher.on('error', () => this.destroyCurrentStream());
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class VoiceChannel extends GuildChannel {
|
|||||||
* The bitrate of this voice channel
|
* The bitrate of this voice channel
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
this.bitrate = data.bitrate;
|
this.bitrate = data.bitrate * 0.001;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum amount of users allowed in this channel - 0 means unlimited.
|
* The maximum amount of users allowed in this channel - 0 means unlimited.
|
||||||
@@ -76,16 +76,17 @@ class VoiceChannel extends GuildChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the bitrate of the channel.
|
* Sets the bitrate of the channel (in kbps).
|
||||||
* @param {number} bitrate The new bitrate
|
* @param {number} bitrate The new bitrate
|
||||||
* @returns {Promise<VoiceChannel>}
|
* @returns {Promise<VoiceChannel>}
|
||||||
* @example
|
* @example
|
||||||
* // Set the bitrate of a voice channel
|
* // Set the bitrate of a voice channel
|
||||||
* voiceChannel.setBitrate(48000)
|
* voiceChannel.setBitrate(48)
|
||||||
* .then(vc => console.log(`Set bitrate to ${vc.bitrate} for ${vc.name}`))
|
* .then(vc => console.log(`Set bitrate to ${vc.bitrate}kbps for ${vc.name}`))
|
||||||
* .catch(console.error);
|
* .catch(console.error);
|
||||||
*/
|
*/
|
||||||
setBitrate(bitrate) {
|
setBitrate(bitrate) {
|
||||||
|
bitrate *= 1000;
|
||||||
return this.edit({ bitrate });
|
return this.edit({ bitrate });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user