mirror of
https://github.com/danbulant/discord.js
synced 2026-06-05 07:42:09 +00:00
146 lines
4.1 KiB
JavaScript
146 lines
4.1 KiB
JavaScript
const VolumeInterface = require('../util/VolumeInterface');
|
|
const VoiceBroadcast = require('../VoiceBroadcast');
|
|
const { VoiceStatus } = require('../../../util/Constants');
|
|
const { Writable } = require('stream');
|
|
|
|
const secretbox = require('../util/Secretbox');
|
|
|
|
const FRAME_LENGTH = 20;
|
|
const CHANNELS = 2;
|
|
const TIMESTAMP_INC = (48000 / 100) * CHANNELS;
|
|
|
|
const nonce = Buffer.alloc(24);
|
|
nonce.fill(0);
|
|
|
|
/**
|
|
* The class that sends voice packet data to the voice connection.
|
|
* ```js
|
|
* // Obtained using:
|
|
* voiceChannel.join().then(connection => {
|
|
* // You can play a file or a stream here:
|
|
* const dispatcher = connection.playFile('./file.mp3');
|
|
* });
|
|
* ```
|
|
* @implements {VolumeInterface}
|
|
*/
|
|
class StreamDispatcher extends Writable {
|
|
constructor(player, streamOptions) {
|
|
super(streamOptions);
|
|
/**
|
|
* The Audio Player that controls this dispatcher
|
|
* @type {AudioPlayer}
|
|
*/
|
|
this.player = player;
|
|
this.streamOptions = streamOptions;
|
|
|
|
this.pausedSince = null;
|
|
this._writeCallback = null;
|
|
|
|
this.pausedTime = 0;
|
|
this.count = 0;
|
|
|
|
this.on('error', this.destroy.bind(this));
|
|
this.on('finish', () => {
|
|
this.destroy.bind(this);
|
|
// Still emitting end for backwards compatibility, probably remove it in the future!
|
|
this.emit('end');
|
|
});
|
|
}
|
|
|
|
get _sdata() {
|
|
return this.player.streamingData;
|
|
}
|
|
|
|
_write(chunk, enc, done) {
|
|
if (!this.startTime) this.startTime = Date.now();
|
|
this._playChunk(chunk);
|
|
this._step(done);
|
|
}
|
|
|
|
_destroy(err, cb) {
|
|
if (this.player.dispatcher !== this) return;
|
|
this.player.dispatcher = null;
|
|
const streams = this.player.streams;
|
|
if (streams.opus) streams.opus.unpipe(this);
|
|
if (streams.ffmpeg) streams.ffmpeg.destroy();
|
|
super._destroy(err, cb);
|
|
}
|
|
|
|
pause() {
|
|
this.pausedSince = Date.now();
|
|
}
|
|
|
|
unpause() {
|
|
this.pausedTime += Date.now() - this.pausedSince;
|
|
this.pausedSince = null;
|
|
if (this._writeCallback) this._writeCallback();
|
|
}
|
|
|
|
_step(done) {
|
|
if (this.pausedSince) {
|
|
this._writeCallback = done;
|
|
return;
|
|
}
|
|
const next = FRAME_LENGTH + (this.count * FRAME_LENGTH) - (Date.now() - this.startTime - this.pausedTime);
|
|
setTimeout(done.bind(this), next);
|
|
if (this._sdata.sequence === (2 ** 16) - 1) this._sdata.sequence = -1;
|
|
if (this._sdata.timestamp === (2 ** 32) - 1) this._sdata.timestamp = -TIMESTAMP_INC;
|
|
this._sdata.sequence++;
|
|
this._sdata.timestamp += TIMESTAMP_INC;
|
|
this.count++;
|
|
}
|
|
|
|
_playChunk(chunk) {
|
|
if (this.player.dispatcher !== this) return;
|
|
this.setSpeaking(true);
|
|
this.sendPacket(this.createPacket(this._sdata.sequence, this._sdata.timestamp, chunk));
|
|
}
|
|
|
|
createPacket(sequence, timestamp, buffer) {
|
|
const packetBuffer = Buffer.alloc(buffer.length + 28);
|
|
packetBuffer.fill(0);
|
|
packetBuffer[0] = 0x80;
|
|
packetBuffer[1] = 0x78;
|
|
|
|
packetBuffer.writeUIntBE(sequence, 2, 2);
|
|
packetBuffer.writeUIntBE(timestamp, 4, 4);
|
|
packetBuffer.writeUIntBE(this.player.voiceConnection.authentication.ssrc, 8, 4);
|
|
|
|
packetBuffer.copy(nonce, 0, 0, 12);
|
|
buffer = secretbox.methods.close(buffer, nonce, this.player.voiceConnection.authentication.secretKey.key);
|
|
for (let i = 0; i < buffer.length; i++) packetBuffer[i + 12] = buffer[i];
|
|
|
|
return packetBuffer;
|
|
}
|
|
|
|
sendPacket(packet) {
|
|
let repeats = 1;
|
|
/**
|
|
* Emitted whenever the dispatcher has debug information.
|
|
* @event StreamDispatcher#debug
|
|
* @param {string} info The debug info
|
|
*/
|
|
this.setSpeaking(true);
|
|
while (repeats--) {
|
|
this.player.voiceConnection.sockets.udp.send(packet)
|
|
.catch(e => {
|
|
this.setSpeaking(false);
|
|
this.emit('debug', `Failed to send a packet ${e}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
setSpeaking(value) {
|
|
if (this.speaking === value) return;
|
|
if (this.player.voiceConnection.status !== VoiceStatus.CONNECTED) return;
|
|
this.speaking = value;
|
|
/**
|
|
* Emitted when the dispatcher starts/stops speaking.
|
|
* @event StreamDispatcher#speaking
|
|
* @param {boolean} value Whether or not the dispatcher is speaking
|
|
*/
|
|
this.emit('speaking', value);
|
|
}
|
|
}
|
|
|
|
module.exports = StreamDispatcher;
|