Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Voice): implement support for @discordjs/voice #5402

Merged
merged 13 commits into from
Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,6 @@ discord.js is a powerful [Node.js](https://nodejs.org) module that allows you to
**Node.js 14.0.0 or newer is required.**
Ignore any warnings about unmet peer dependencies, as they're all optional.

Without voice support: `npm install discord.js`
With voice support ([@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus)): `npm install discord.js @discordjs/opus`
With voice support ([opusscript](https://www.npmjs.com/package/opusscript)): `npm install discord.js opusscript`

### Audio engines

The preferred audio engine is @discordjs/opus, as it performs significantly better than opusscript. When both are available, discord.js will automatically choose @discordjs/opus.
Using opusscript is only recommended for development environments where @discordjs/opus is tough to get working.
For production bots, using @discordjs/opus should be considered a necessity, especially if they're going to be running on multiple servers.

### Optional packages

- [zlib-sync](https://www.npmjs.com/package/zlib-sync) for WebSocket data compression and inflation (`npm install zlib-sync`)
Expand All @@ -63,6 +53,7 @@ For production bots, using @discordjs/opus should be considered a necessity, esp
- [libsodium.js](https://www.npmjs.com/package/libsodium-wrappers) (`npm install libsodium-wrappers`)
- [bufferutil](https://www.npmjs.com/package/bufferutil) for a much faster WebSocket connection (`npm install bufferutil`)
- [utf-8-validate](https://www.npmjs.com/package/utf-8-validate) in combination with `bufferutil` for much faster WebSocket processing (`npm install utf-8-validate`)
- [@discordjs/voice](https://github.com/discordjs/voice) for interacting with the Discord Voice API

## Example usage

Expand Down
55 changes: 42 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,13 @@
"abort-controller": "^3.0.0",
"discord-api-types": "^0.18.1",
"node-fetch": "^2.6.1",
"prism-media": "^1.2.9",
"tweetnacl": "^1.0.3",
"ws": "^7.4.6"
},
"devDependencies": {
"@commitlint/cli": "^12.1.4",
"@commitlint/config-angular": "^12.1.4",
"@discordjs/docgen": "^0.10.0",
"@discordjs/voice": "^0.3.0",
"@types/node": "^12.12.6",
"conventional-changelog-cli": "^2.1.1",
"cross-env": "^7.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/client/actions/GuildDelete.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class GuildDeleteAction extends Action {
}

for (const channel of guild.channels.cache.values()) this.client.channels.remove(channel.id);
guild.me?.voice.connection?.disconnect();
client.voice.adapters.get(data.id)?.destroy();

// Delete guild
client.guilds.cache.delete(guild.id);
Expand Down
104 changes: 16 additions & 88 deletions src/client/voice/ClientVoiceManager.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
'use strict';

const VoiceBroadcast = require('./VoiceBroadcast');
const VoiceConnection = require('./VoiceConnection');
const { Error } = require('../../errors');
const Collection = require('../../util/Collection');
const { Events } = require('../../util/Constants');

/**
* Manages voice connections for the client
Expand All @@ -19,98 +16,29 @@ class ClientVoiceManager {
Object.defineProperty(this, 'client', { value: client });

/**
* A collection mapping connection IDs to the Connection objects
* @type {Collection<Snowflake, VoiceConnection>}
* Maps guild IDs to voice adapters created for use with @discordjs/voice.
* @type {Map<Snowflake, Object>}
*/
this.connections = new Collection();
this.adapters = new Map();
amishshah marked this conversation as resolved.
Show resolved Hide resolved

/**
* Active voice broadcasts that have been created
* @type {VoiceBroadcast[]}
*/
this.broadcasts = [];
}

/**
* Creates a voice broadcast.
* @returns {VoiceBroadcast}
*/
createBroadcast() {
const broadcast = new VoiceBroadcast(this.client);
this.broadcasts.push(broadcast);
return broadcast;
client.on(Events.SHARD_DISCONNECT, (_, shardID) => {
for (const [guildID, adapter] of this.adapters.entries()) {
if (client.guilds.cache.get(guildID)?.shardID === shardID) {
adapter.destroy();
}
}
});
}

onVoiceServer({ guild_id, token, endpoint }) {
this.client.emit('debug', `[VOICE] voiceServer guild: ${guild_id} token: ${token} endpoint: ${endpoint}`);
const connection = this.connections.get(guild_id);
if (connection) connection.setTokenAndEndpoint(token, endpoint);
onVoiceServer(payload) {
this.adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
}

onVoiceStateUpdate({ guild_id, session_id, channel_id }) {
const connection = this.connections.get(guild_id);
this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`);
if (!connection) return;
if (!channel_id) {
connection._disconnect();
this.connections.delete(guild_id);
return;
}
const channel = this.client.channels.cache.get(channel_id);
if (channel) {
connection.channel = channel;
connection.setSessionID(session_id);
} else {
this.client.emit('debug', `[VOICE] disconnecting from guild ${guild_id} as channel ${channel_id} is uncached`);
connection.disconnect();
onVoiceStateUpdate(payload) {
if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
}
}

/**
* Sets up a request to join a voice or stage channel.
* @param {VoiceChannel|StageChannel} channel The channel to join
* @returns {Promise<VoiceConnection>}
* @private
*/
joinChannel(channel) {
return new Promise((resolve, reject) => {
if (!channel.joinable) {
throw new Error('VOICE_JOIN_CHANNEL', channel.full);
}

let connection = this.connections.get(channel.guild.id);

if (connection) {
if (connection.channel.id !== channel.id) {
this.connections.get(channel.guild.id).updateChannel(channel);
}
resolve(connection);
return;
} else {
connection = new VoiceConnection(this, channel);
connection.on('debug', msg =>
this.client.emit('debug', `[VOICE (${channel.guild.id}:${connection.status})]: ${msg}`),
);
connection.authenticate();
this.connections.set(channel.guild.id, connection);
}

connection.once('failed', reason => {
this.connections.delete(channel.guild.id);
reject(reason);
});

connection.on('error', reject);

connection.once('authenticated', () => {
connection.once('ready', () => {
resolve(connection);
connection.removeListener('error', reject);
});
connection.once('disconnect', () => this.connections.delete(channel.guild.id));
});
});
}
}

module.exports = ClientVoiceManager;
Loading