mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
263 lines
7.5 KiB
JavaScript
263 lines
7.5 KiB
JavaScript
const VoiceConnectionWebSocket = require('./VoiceConnectionWebSocket');
|
|
const VoiceConnectionUDPClient = require('./VoiceConnectionUDPClient');
|
|
const VoiceReceiver = require('./receiver/VoiceReceiver');
|
|
const Constants = require('../../util/Constants');
|
|
const EventEmitter = require('events').EventEmitter;
|
|
const DefaultPlayer = require('./player/DefaultPlayer');
|
|
|
|
/**
|
|
* Represents a connection to a Voice Channel in Discord
|
|
* @extends {EventEmitter}
|
|
*/
|
|
class VoiceConnection extends EventEmitter {
|
|
constructor(manager, channel, token, sessionID, endpoint, resolve, reject) {
|
|
super();
|
|
/**
|
|
* The voice manager of this connection
|
|
* @type {ClientVoiceManager}
|
|
* @private
|
|
*/
|
|
this.manager = manager;
|
|
/**
|
|
* The player
|
|
* @type {BasePlayer}
|
|
*/
|
|
this.player = new DefaultPlayer(this);
|
|
/**
|
|
* The endpoint of the connection
|
|
* @type {string}
|
|
*/
|
|
this.endpoint = endpoint;
|
|
/**
|
|
* The VoiceChannel for this connection
|
|
* @type {VoiceChannel}
|
|
*/
|
|
this.channel = channel;
|
|
/**
|
|
* The WebSocket connection for this voice connection
|
|
* @type {VoiceConnectionWebSocket}
|
|
* @private
|
|
*/
|
|
this.websocket = new VoiceConnectionWebSocket(this, channel.guild.id, token, sessionID, endpoint);
|
|
/**
|
|
* Whether or not the connection is ready
|
|
* @type {boolean}
|
|
*/
|
|
this.ready = false;
|
|
/**
|
|
* The resolve function for the promise associated with creating this connection
|
|
* @type {function}
|
|
* @private
|
|
*/
|
|
this._resolve = resolve;
|
|
/**
|
|
* The reject function for the promise associated with creating this connection
|
|
* @type {function}
|
|
* @private
|
|
*/
|
|
this._reject = reject;
|
|
this.ssrcMap = new Map();
|
|
this.queue = [];
|
|
this.receivers = [];
|
|
this.bindListeners();
|
|
}
|
|
|
|
/**
|
|
* Executed whenever an error occurs with the UDP/WebSocket sub-client
|
|
* @private
|
|
* @param {Error} err The error that occurred
|
|
*/
|
|
_onError(err) {
|
|
this._reject(err);
|
|
/**
|
|
* Emitted whenever the connection encounters a fatal error.
|
|
* @event VoiceConnection#error
|
|
* @param {Error} error the encountered error
|
|
*/
|
|
this.emit('error', err);
|
|
this._shutdown(err);
|
|
}
|
|
|
|
/**
|
|
* Disconnects the Client from the Voice Channel
|
|
* @param {string} [reason='user requested'] the reason of the disconnection
|
|
*/
|
|
disconnect(reason = 'user requested') {
|
|
this.manager.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._shutdown(reason);
|
|
}
|
|
|
|
_onClose(e) {
|
|
e = e && e.code === 1000 ? null : e;
|
|
return this._shutdown(e);
|
|
}
|
|
|
|
_shutdown(e) {
|
|
if (!this.ready) {
|
|
return;
|
|
}
|
|
this.ready = false;
|
|
this.websocket._shutdown();
|
|
this.player._shutdown();
|
|
if (this.udp) {
|
|
this.udp._shutdown();
|
|
}
|
|
/**
|
|
* Emit once the voice connection has disconnected.
|
|
* @event VoiceConnection#disconnected
|
|
* @param {Error} error the error, if any
|
|
*/
|
|
this.emit('disconnected', e);
|
|
}
|
|
|
|
/**
|
|
* Binds listeners to the WebSocket and UDP sub-clients
|
|
* @private
|
|
*/
|
|
bindListeners() {
|
|
this.websocket.on('error', err => this._onError(err));
|
|
this.websocket.on('close', err => this._onClose(err));
|
|
this.websocket.on('ready-for-udp', data => {
|
|
this.udp = new VoiceConnectionUDPClient(this, data);
|
|
this.data = data;
|
|
this.udp.on('error', err => this._onError(err));
|
|
this.udp.on('close', err => this._onClose(err));
|
|
});
|
|
this.websocket.on('ready', secretKey => {
|
|
this.data.secret = secretKey;
|
|
this.ready = true;
|
|
/**
|
|
* Emitted once the connection is ready (joining voice channels resolves when the connection is ready anyway)
|
|
* @event VoiceConnection#ready
|
|
*/
|
|
this._resolve(this);
|
|
this.emit('ready');
|
|
});
|
|
this.once('ready', () => {
|
|
setImmediate(() => {
|
|
for (const item of this.queue) {
|
|
this.emit(...item);
|
|
}
|
|
this.queue = [];
|
|
});
|
|
});
|
|
this.manager.client.on(Constants.Events.VOICE_STATE_UPDATE, (oldM, newM) => {
|
|
if (oldM.voiceChannel && oldM.voiceChannel.guild.id === this.channel.guild.id && !newM.voiceChannel) {
|
|
const user = newM.user;
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.websocket.on('speaking', data => {
|
|
const guild = this.channel.guild;
|
|
const user = this.manager.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);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* 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);
|
|
} else {
|
|
this.queue.push(['speaking', user, data.speaking]);
|
|
}
|
|
guild._memberSpeakUpdate(data.user_id, data.speaking);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Play the given file in the voice connection
|
|
* @param {string} file the path to the file
|
|
* @returns {StreamDispatcher}
|
|
* @example
|
|
* // play files natively
|
|
* voiceChannel.join()
|
|
* .then(connection => {
|
|
* const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
|
|
* })
|
|
* .catch(console.log);
|
|
*/
|
|
playFile(file) {
|
|
return this.player.playFile(file);
|
|
}
|
|
|
|
/**
|
|
* Play the given stream in the voice connection
|
|
* @param {ReadableStream} stream the audio stream to play
|
|
* @returns {StreamDispatcher}
|
|
* @example
|
|
* // play streams using ytdl-core
|
|
* const ytdl = require('ytdl-core');
|
|
* voiceChannel.join()
|
|
* .then(connection => {
|
|
* const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', {filter : 'audioonly'});
|
|
* const dispatcher = connection.playStream(stream);
|
|
* })
|
|
* .catch(console.log);
|
|
*/
|
|
playStream(stream) {
|
|
return this.player.playStream(stream);
|
|
}
|
|
|
|
/**
|
|
* Plays a stream of PCM data
|
|
* @param {ReadableStream} pcmStream the PCM stream
|
|
* @returns {StreamDispatcher}
|
|
*/
|
|
playPCMStream(pcmStream) {
|
|
this._shutdown();
|
|
const dispatcher = this.player.playPCMStream(pcmStream);
|
|
return dispatcher;
|
|
}
|
|
|
|
/**
|
|
* Creates a VoiceReceiver so you can start listening to voice data. It's recommended to only create one of these.
|
|
* @returns {VoiceReceiver}
|
|
*/
|
|
createReceiver() {
|
|
const rcv = new VoiceReceiver(this);
|
|
this.receivers.push(rcv);
|
|
return rcv;
|
|
}
|
|
}
|
|
|
|
module.exports = VoiceConnection;
|