mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-14 18:43:31 +01:00
VoiceConnection rework (#1183)
* VoiceConnection rework - improves codebase - removes concept of pending connections - attempts to fix memory leaks by removing EventEmitter listeners - makes voice connections keep track of its own channel when it is moved by another user - allows voice connections to reconnect when Discord falls back to another voice server or a region change occurs - adds events for some of the aforementioned events * Removed unused code * More clean up / bugfixes * Added typedefs to Status and VoiceStatus constants
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const VoiceWebSocket = require('./VoiceWebSocket');
|
||||
const VoiceUDP = require('./VoiceUDPClient');
|
||||
const mergeDefault = require('../../util/MergeDefault');
|
||||
const Constants = require('../../util/Constants');
|
||||
const AudioPlayer = require('./player/AudioPlayer');
|
||||
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
||||
@@ -17,14 +18,20 @@ const Prism = require('prism-media');
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class VoiceConnection extends EventEmitter {
|
||||
constructor(pendingConnection) {
|
||||
constructor(voiceManager, channel) {
|
||||
super();
|
||||
|
||||
/**
|
||||
* The Voice Manager that instantiated this connection
|
||||
* The voice manager that instantiated this connection
|
||||
* @type {ClientVoiceManager}
|
||||
*/
|
||||
this.voiceManager = pendingConnection.voiceManager;
|
||||
this.voiceManager = voiceManager;
|
||||
|
||||
/**
|
||||
* The client that instantiated this connection
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = voiceManager.client;
|
||||
|
||||
/**
|
||||
* @external Prism
|
||||
@@ -41,7 +48,13 @@ class VoiceConnection extends EventEmitter {
|
||||
* The voice channel this connection is currently serving
|
||||
* @type {VoiceChannel}
|
||||
*/
|
||||
this.channel = pendingConnection.channel;
|
||||
this.channel = channel;
|
||||
|
||||
/**
|
||||
* The current status of the voice connection
|
||||
* @type {number}
|
||||
*/
|
||||
this.status = Constants.VoiceStatus.AUTHENTICATING;
|
||||
|
||||
/**
|
||||
* Whether we're currently transmitting audio
|
||||
@@ -60,7 +73,7 @@ class VoiceConnection extends EventEmitter {
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.authentication = pendingConnection.data;
|
||||
this.authentication = {};
|
||||
|
||||
/**
|
||||
* The audio player for this voice connection
|
||||
@@ -93,20 +106,14 @@ class VoiceConnection extends EventEmitter {
|
||||
*/
|
||||
this.ssrcMap = new Map();
|
||||
|
||||
/**
|
||||
* Whether this connection is ready
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.ready = false;
|
||||
|
||||
/**
|
||||
* Object that wraps contains the `ws` and `udp` sockets of this voice connection
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.sockets = {};
|
||||
this.connect();
|
||||
|
||||
this.authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,21 +135,169 @@ class VoiceConnection extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the main gateway to join a voice channel
|
||||
* @param {Object} [options] The options to provide
|
||||
*/
|
||||
sendVoiceStateUpdate(options = {}) {
|
||||
options = mergeDefault({
|
||||
guild_id: this.channel.guild.id,
|
||||
channel_id: this.channel.id,
|
||||
self_mute: false,
|
||||
self_deaf: false,
|
||||
}, options);
|
||||
|
||||
this.client.ws.send({
|
||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||
d: options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the token and endpoint required to connect to the the voice servers
|
||||
* @param {string} token The voice token
|
||||
* @param {string} endpoint The voice endpoint
|
||||
* @returns {void}
|
||||
*/
|
||||
setTokenAndEndpoint(token, endpoint) {
|
||||
if (!token) {
|
||||
this.authenticateFailed('Token not provided from voice server packet.');
|
||||
return;
|
||||
}
|
||||
if (!endpoint) {
|
||||
this.authenticateFailed('Endpoint not provided from voice server packet.');
|
||||
return;
|
||||
}
|
||||
|
||||
endpoint = endpoint.match(/([^:]*)/)[0];
|
||||
|
||||
if (!endpoint) {
|
||||
this.authenticateFailed('Failed to find an endpoint.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
|
||||
this.authentication.token = token;
|
||||
this.authentication.endpoint = endpoint;
|
||||
this.checkAuthenticated();
|
||||
} else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) {
|
||||
this.reconnect(token, endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Session ID for the connection
|
||||
* @param {string} sessionID The voice session ID
|
||||
*/
|
||||
setSessionID(sessionID) {
|
||||
if (!sessionID) {
|
||||
this.authenticateFailed('Session ID not supplied.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
|
||||
this.authentication.sessionID = sessionID;
|
||||
this.checkAuthenticated();
|
||||
} else if (sessionID !== this.authentication.sessionID) {
|
||||
this.authentication.sessionID = sessionID;
|
||||
/**
|
||||
* Emitted when a new session ID is received
|
||||
* @event VoiceConnection#newSession
|
||||
* @private
|
||||
*/
|
||||
this.emit('newSession', sessionID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the voice connection is authenticated
|
||||
* @private
|
||||
*/
|
||||
checkAuthenticated() {
|
||||
const { token, endpoint, sessionID } = this.authentication;
|
||||
|
||||
if (token && endpoint && sessionID) {
|
||||
clearTimeout(this.connectTimeout);
|
||||
this.status = Constants.VoiceStatus.CONNECTING;
|
||||
/**
|
||||
* Emitted when we successfully initiate a voice connection
|
||||
* @event VoiceConnection#authenticated
|
||||
*/
|
||||
this.emit('authenticated');
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when we fail to initiate a voice connection
|
||||
* @param {string} reason The reason for failure
|
||||
* @private
|
||||
*/
|
||||
authenticateFailed(reason) {
|
||||
clearTimeout(this.connectTimeout);
|
||||
this.status = Constants.VoiceStatus.DISCONNECTED;
|
||||
if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
|
||||
/**
|
||||
* Emitted when we fail to initiate a voice connection
|
||||
* @event VoiceConnection#failed
|
||||
* @param {Error} error The encountered error
|
||||
*/
|
||||
this.emit('failed', new Error(reason));
|
||||
} else {
|
||||
this.emit('error', new Error(reason));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to a different voice channel in the same guild
|
||||
* @param {VoiceChannel} channel The channel to move to
|
||||
* @private
|
||||
*/
|
||||
updateChannel(channel) {
|
||||
this.channel = channel;
|
||||
this.sendVoiceStateUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to authenticate to the voice server
|
||||
* @private
|
||||
*/
|
||||
authenticate() {
|
||||
this.sendVoiceStateUpdate();
|
||||
this.connectTimeout = this.client.setTimeout(
|
||||
() => this.authenticateFailed(new Error('Connection not established within 15 seconds.')), 15000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to reconnect to the voice server (typically after a region change)
|
||||
* @param {string} token The voice token
|
||||
* @param {string} endpoint The voice endpoint
|
||||
* @private
|
||||
*/
|
||||
reconnect(token, endpoint) {
|
||||
this.authentication.token = token;
|
||||
this.authentication.endpoint = endpoint;
|
||||
|
||||
this.status = Constants.VoiceStatus.RECONNECTING;
|
||||
/**
|
||||
* Emitted when the voice connection is reconnecting (typically after a region change)
|
||||
* @event VoiceConnection#reconnecting
|
||||
*/
|
||||
this.emit('reconnecting');
|
||||
this.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the voice connection, causing a disconnect and closing event to be emitted.
|
||||
*/
|
||||
disconnect() {
|
||||
this.emit('closing');
|
||||
this.voiceManager.client.ws.send({
|
||||
op: Constants.OPCodes.VOICE_STATE_UPDATE,
|
||||
d: {
|
||||
guild_id: this.channel.guild.id,
|
||||
channel_id: null,
|
||||
self_mute: false,
|
||||
self_deaf: false,
|
||||
},
|
||||
this.sendVoiceStateUpdate({
|
||||
channel_id: null,
|
||||
});
|
||||
this.player.destroy();
|
||||
this.cleanup();
|
||||
this.status = Constants.VoiceStatus.DISCONNECTED;
|
||||
/**
|
||||
* Emitted when the voice connection disconnects
|
||||
* @event VoiceConnection#disconnect
|
||||
@@ -150,70 +305,108 @@ class VoiceConnection extends EventEmitter {
|
||||
this.emit('disconnect');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up after disconnect
|
||||
* @private
|
||||
*/
|
||||
cleanup() {
|
||||
const { ws, udp } = this.sockets;
|
||||
ws.removeAllListeners('error');
|
||||
udp.removeAllListeners('error');
|
||||
ws.removeAllListeners('ready');
|
||||
ws.removeAllListeners('sessionDescription');
|
||||
ws.removeAllListeners('speaking');
|
||||
this.sockets.ws = null;
|
||||
this.sockets.udp = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the voice connection
|
||||
* @private
|
||||
*/
|
||||
connect() {
|
||||
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
|
||||
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
|
||||
if (this.status !== Constants.VoiceStatus.RECONNECTING) {
|
||||
if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
|
||||
if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
|
||||
}
|
||||
|
||||
if (this.sockets.ws) this.sockets.ws.shutdown();
|
||||
if (this.sockets.udp) this.sockets.udp.shutdown();
|
||||
|
||||
this.sockets.ws = new VoiceWebSocket(this);
|
||||
this.sockets.udp = new VoiceUDP(this);
|
||||
this.sockets.ws.on('error', e => this.emit('error', e));
|
||||
this.sockets.udp.on('error', e => this.emit('error', e));
|
||||
this.sockets.ws.on('ready', d => {
|
||||
this.authentication.port = d.port;
|
||||
this.authentication.ssrc = d.ssrc;
|
||||
/**
|
||||
* Emitted whenever the connection encounters an error.
|
||||
* @event VoiceConnection#error
|
||||
* @param {Error} error the encountered error
|
||||
*/
|
||||
this.sockets.udp.findEndpointAddress()
|
||||
.then(address => {
|
||||
this.sockets.udp.createUDPSocket(address);
|
||||
}, e => this.emit('error', e));
|
||||
});
|
||||
this.sockets.ws.on('sessionDescription', (mode, secret) => {
|
||||
this.authentication.encryptionMode = mode;
|
||||
this.authentication.secretKey = secret;
|
||||
/**
|
||||
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
|
||||
* the connection will already be ready.
|
||||
* @event VoiceConnection#ready
|
||||
*/
|
||||
this.emit('ready');
|
||||
this.ready = true;
|
||||
});
|
||||
this.sockets.ws.on('speaking', data => {
|
||||
const guild = this.channel.guild;
|
||||
const user = this.voiceManager.client.users.get(data.user_id);
|
||||
this.ssrcMap.set(+data.ssrc, user);
|
||||
if (!data.speaking) {
|
||||
for (const receiver of this.receivers) {
|
||||
const opusStream = receiver.opusStreams.get(user.id);
|
||||
const pcmStream = receiver.pcmStreams.get(user.id);
|
||||
if (opusStream) {
|
||||
opusStream.push(null);
|
||||
opusStream.open = false;
|
||||
receiver.opusStreams.delete(user.id);
|
||||
}
|
||||
if (pcmStream) {
|
||||
pcmStream.push(null);
|
||||
pcmStream.open = false;
|
||||
receiver.pcmStreams.delete(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
const { ws, udp } = this.sockets;
|
||||
|
||||
ws.on('error', err => this.emit('error', err));
|
||||
udp.on('error', err => this.emit('error', err));
|
||||
ws.on('ready', this.onReady.bind(this));
|
||||
ws.on('sessionDescription', this.onSessionDescription.bind(this));
|
||||
ws.on('speaking', this.onSpeaking.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the voice websocket is ready
|
||||
* @param {Object} data The received data
|
||||
* @private
|
||||
*/
|
||||
onReady({ port, ssrc }) {
|
||||
this.authentication.port = port;
|
||||
this.authentication.ssrc = ssrc;
|
||||
|
||||
const udp = this.sockets.udp;
|
||||
/**
|
||||
* Emitted whenever the connection encounters an error.
|
||||
* @event VoiceConnection#error
|
||||
* @param {Error} error The encountered error
|
||||
*/
|
||||
udp.findEndpointAddress()
|
||||
.then(address => {
|
||||
udp.createUDPSocket(address);
|
||||
}, e => this.emit('error', e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a session description is received
|
||||
* @param {string} mode The encryption mode
|
||||
* @param {string} secret The secret key
|
||||
* @private
|
||||
*/
|
||||
onSessionDescription(mode, secret) {
|
||||
this.authentication.encryptionMode = mode;
|
||||
this.authentication.secretKey = secret;
|
||||
|
||||
this.status = Constants.VoiceStatus.CONNECTED;
|
||||
/**
|
||||
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
|
||||
* the connection will already be ready.
|
||||
* @event VoiceConnection#ready
|
||||
*/
|
||||
this.emit('ready');
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when a speaking event is received
|
||||
* @param {Object} data The received data
|
||||
* @private
|
||||
*/
|
||||
onSpeaking({ user_id, ssrc, speaking }) {
|
||||
const guild = this.channel.guild;
|
||||
const user = this.client.users.get(user_id);
|
||||
this.ssrcMap.set(+ssrc, user);
|
||||
if (!speaking) {
|
||||
for (const receiver of this.receivers) {
|
||||
receiver.stoppedSpeaking(user);
|
||||
}
|
||||
/**
|
||||
* Emitted whenever a user starts/stops speaking
|
||||
* @event VoiceConnection#speaking
|
||||
* @param {User} user The user that has started/stopped speaking
|
||||
* @param {boolean} speaking Whether or not the user is speaking
|
||||
*/
|
||||
if (this.ready) this.emit('speaking', user, data.speaking);
|
||||
guild._memberSpeakUpdate(data.user_id, data.speaking);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Emitted whenever a user starts/stops speaking
|
||||
* @event VoiceConnection#speaking
|
||||
* @param {User} user The user that has started/stopped speaking
|
||||
* @param {boolean} speaking Whether or not the user is speaking
|
||||
*/
|
||||
if (this.status === Constants.Status.CONNECTED) this.emit('speaking', user, speaking);
|
||||
guild._memberSpeakUpdate(user_id, speaking);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user