Skip to content
This repository was archived by the owner on Jul 12, 2023. It is now read-only.

Commit 687e0e8

Browse files
amishshahkyranet
andauthored
feat(VoiceReceive)!: improve usability (#136)
* refactor(VoiceReceiver): begin refactor * feat(SSRCMap): resolve function * feat(VoiceConnection): close all streams in non-ready state * refactor(VoiceReceiver): map by user ID * feat(VoiceReceiver): allow specifying end type for streams * feat(VoiceReceiver): add SpeakingMap * refactor(SSRCMap): remove unused resolve method * test(VoiceReceiver): add test for SpeakingMap * test(VoiceReceiver): add tests for AudioReceiveStream * test(AudioReceiver): strengthen AudioReceiveStream tests * test(VoiceReceiver): remove inapplicable tests * test(VoiceConnection): fix test errors * test(VoiceConnection): test receiver bindings tracking * test(VoiceReceiver): decrypt * chore: remove unused code * fix(AudioReceiveStream): close normally * feat(Examples): update receiver example * feat(Examples): create recorder example * docs(VoiceReceiver): add docs for receive classes * refactor: suggestions from code review Co-authored-by: Antonio Román <kyradiscord@gmail.com> Co-authored-by: Antonio Román <kyradiscord@gmail.com>
1 parent 07e751a commit 687e0e8

23 files changed

Lines changed: 764 additions & 251 deletions

examples/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Examples
22

33
| Example | Description |
4-
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
4+
|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
55
| [Basic](./basic) | A simple "Hello World" TypeScript example that plays an mp3 file. Notably, it works with discord.js v12 and so it also contains an example of creating an adapter |
66
| [Radio Bot](./radio-bot) | A fun JavaScript example of what you can create using @discordjs/voice. A radio bot that plays output from your speakers in a Discord voice channel |
77
| [Music Bot](./music-bot) | A TypeScript example of a YouTube music bot. Demonstrates how queues can be implemented and how to implement "good" disconnect/reconnection logic |
8+
| [Recorder](./recorder) | An example of using voice receive to create a bot that can record audio from users |

examples/recorder/.eslintrc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"root": true,
3+
"extends": "../../.eslintrc.json",
4+
"parserOptions": {
5+
"project": "./tsconfig.eslint.json"
6+
}
7+
}

examples/recorder/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package-lock.json
2+
auth.json
3+
tsconfig.tsbuildinfo
4+
recordings/*.ogg

examples/recorder/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# 👂 Recorder Bot
2+
3+
This example shows how you can use the voice receive functionality in @discordjs/voice to record users in voice channels
4+
and save the audio to local Ogg files.
5+
6+
## Usage
7+
8+
```sh-session
9+
# Clone the main repository, and then run:
10+
$ npm install
11+
$ npm run build
12+
13+
# Open this example and install dependencies
14+
$ cd examples/recorder
15+
$ npm install
16+
17+
# Set a bot token (see auth.example.json)
18+
$ cp auth.example.json auth.json
19+
$ nano auth.json
20+
21+
# Start the bot!
22+
$ npm start
23+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"token": "Your Discord bot token here"
3+
}

examples/recorder/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "receiver-bot",
3+
"version": "0.0.1",
4+
"description": "An example receiver bot written using @discordjs/voice",
5+
"scripts": {
6+
"start": "npm run build && node -r tsconfig-paths/register dist/bot",
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"lint": "eslint src --ext .ts",
9+
"lint:fix": "eslint src --ext .ts --fix",
10+
"prettier": "prettier --write **/*.{ts,js,json,yml,yaml}",
11+
"build": "tsc",
12+
"build:check": "tsc --noEmit --incremental false"
13+
},
14+
"author": "Amish Shah <contact@shah.gg>",
15+
"license": "MIT",
16+
"dependencies": {
17+
"@discordjs/opus": "^0.5.3",
18+
"discord-api-types": "^0.22.0",
19+
"discord.js": "^13.0.1",
20+
"libsodium-wrappers": "^0.7.9",
21+
"node-crc": "^1.3.2",
22+
"prism-media": "^2.0.0-alpha.0"
23+
},
24+
"devDependencies": {
25+
"tsconfig-paths": "^3.10.1",
26+
"typescript": "~4.3.5"
27+
}
28+
}

examples/recorder/recordings/.gitkeep

Whitespace-only changes.

examples/recorder/src/bot.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Discord, { Interaction } from 'discord.js';
2+
import { getVoiceConnection } from '@discordjs/voice';
3+
import { deploy } from './deploy';
4+
import { interactionHandlers } from './interactions';
5+
6+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
7+
const { token } = require('../auth.json');
8+
9+
const client = new Discord.Client({ intents: ['GUILD_VOICE_STATES', 'GUILD_MESSAGES', 'GUILDS'] });
10+
11+
client.on('ready', () => console.log('Ready!'));
12+
13+
client.on('messageCreate', async (message) => {
14+
if (!message.guild) return;
15+
if (!client.application?.owner) await client.application?.fetch();
16+
17+
if (message.content.toLowerCase() === '!deploy' && message.author.id === client.application?.owner?.id) {
18+
await deploy(message.guild);
19+
await message.reply('Deployed!');
20+
}
21+
});
22+
23+
/**
24+
* The IDs of the users that can be recorded by the bot.
25+
*/
26+
const recordable = new Set<string>();
27+
28+
client.on('interactionCreate', async (interaction: Interaction) => {
29+
if (!interaction.isCommand() || !interaction.guildId) return;
30+
31+
const handler = interactionHandlers.get(interaction.commandName);
32+
33+
try {
34+
if (handler) {
35+
await handler(interaction, recordable, client, getVoiceConnection(interaction.guildId));
36+
} else {
37+
await interaction.reply('Unknown command');
38+
}
39+
} catch (error) {
40+
console.warn(error);
41+
}
42+
});
43+
44+
client.on('error', console.warn);
45+
46+
void client.login(token);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { EndBehaviorType, VoiceReceiver } from '@discordjs/voice';
2+
import { User } from 'discord.js';
3+
import { createWriteStream } from 'fs';
4+
import { opus } from 'prism-media';
5+
import { pipeline } from 'stream';
6+
7+
function getDisplayName(userId: string, user?: User) {
8+
return user ? `${user.username}_${user.discriminator}` : userId;
9+
}
10+
11+
export function createListeningStream(receiver: VoiceReceiver, userId: string, user?: User) {
12+
const opusStream = receiver.subscribe(userId, {
13+
end: {
14+
behavior: EndBehaviorType.AfterSilence,
15+
duration: 100,
16+
},
17+
});
18+
19+
const oggStream = new opus.OggLogicalBitstream({
20+
opusHead: new opus.OpusHead({
21+
channelCount: 2,
22+
sampleRate: 48000,
23+
}),
24+
pageSizeControl: {
25+
maxPackets: 10,
26+
},
27+
});
28+
29+
const filename = `./recordings/${Date.now()}-${getDisplayName(userId, user)}.ogg`;
30+
31+
const out = createWriteStream(filename);
32+
33+
console.log(`👂 Started recording ${filename}`);
34+
35+
pipeline(opusStream, oggStream, out, (err) => {
36+
if (err) {
37+
console.warn(`❌ Error recording file ${filename} - ${err.message}`);
38+
} else {
39+
console.log(`✅ Recorded ${filename}`);
40+
}
41+
});
42+
}

examples/recorder/src/deploy.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Guild } from 'discord.js';
2+
3+
export const deploy = async (guild: Guild) => {
4+
await guild.commands.set([
5+
{
6+
name: 'join',
7+
description: 'Joins the voice channel that you are in',
8+
},
9+
{
10+
name: 'record',
11+
description: 'Enables recording for a user',
12+
options: [
13+
{
14+
name: 'speaker',
15+
type: 'USER' as const,
16+
description: 'The user to record',
17+
required: true,
18+
},
19+
],
20+
},
21+
{
22+
name: 'leave',
23+
description: 'Leave the voice channel',
24+
},
25+
]);
26+
};

0 commit comments

Comments
 (0)