This repository has been archived by the owner on Jul 12, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 110
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
Showing
23 changed files
with
764 additions
and
251 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
# Examples | ||
|
||
| Example | Description | | ||
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| [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 | | ||
| [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 | | ||
| [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 | | ||
| [Recorder](./recorder) | An example of using voice receive to create a bot that can record audio from users | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"root": true, | ||
"extends": "../../.eslintrc.json", | ||
"parserOptions": { | ||
"project": "./tsconfig.eslint.json" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package-lock.json | ||
auth.json | ||
tsconfig.tsbuildinfo | ||
recordings/*.ogg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# 👂 Recorder Bot | ||
|
||
This example shows how you can use the voice receive functionality in @discordjs/voice to record users in voice channels | ||
and save the audio to local Ogg files. | ||
|
||
## Usage | ||
|
||
```sh-session | ||
# Clone the main repository, and then run: | ||
$ npm install | ||
$ npm run build | ||
|
||
# Open this example and install dependencies | ||
$ cd examples/recorder | ||
$ npm install | ||
|
||
# Set a bot token (see auth.example.json) | ||
$ cp auth.example.json auth.json | ||
$ nano auth.json | ||
|
||
# Start the bot! | ||
$ npm start | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"token": "Your Discord bot token here" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"name": "receiver-bot", | ||
"version": "0.0.1", | ||
"description": "An example receiver bot written using @discordjs/voice", | ||
"scripts": { | ||
"start": "npm run build && node -r tsconfig-paths/register dist/bot", | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"lint": "eslint src --ext .ts", | ||
"lint:fix": "eslint src --ext .ts --fix", | ||
"prettier": "prettier --write **/*.{ts,js,json,yml,yaml}", | ||
"build": "tsc", | ||
"build:check": "tsc --noEmit --incremental false" | ||
}, | ||
"author": "Amish Shah <contact@shah.gg>", | ||
"license": "MIT", | ||
"dependencies": { | ||
"@discordjs/opus": "^0.5.3", | ||
"discord-api-types": "^0.22.0", | ||
"discord.js": "^13.0.1", | ||
"libsodium-wrappers": "^0.7.9", | ||
"node-crc": "^1.3.2", | ||
"prism-media": "^2.0.0-alpha.0" | ||
}, | ||
"devDependencies": { | ||
"tsconfig-paths": "^3.10.1", | ||
"typescript": "~4.3.5" | ||
} | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import Discord, { Interaction } from 'discord.js'; | ||
import { getVoiceConnection } from '@discordjs/voice'; | ||
import { deploy } from './deploy'; | ||
import { interactionHandlers } from './interactions'; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports | ||
const { token } = require('../auth.json'); | ||
|
||
const client = new Discord.Client({ intents: ['GUILD_VOICE_STATES', 'GUILD_MESSAGES', 'GUILDS'] }); | ||
|
||
client.on('ready', () => console.log('Ready!')); | ||
|
||
client.on('messageCreate', async (message) => { | ||
if (!message.guild) return; | ||
if (!client.application?.owner) await client.application?.fetch(); | ||
|
||
if (message.content.toLowerCase() === '!deploy' && message.author.id === client.application?.owner?.id) { | ||
await deploy(message.guild); | ||
await message.reply('Deployed!'); | ||
} | ||
}); | ||
|
||
/** | ||
* The IDs of the users that can be recorded by the bot. | ||
*/ | ||
const recordable = new Set<string>(); | ||
|
||
client.on('interactionCreate', async (interaction: Interaction) => { | ||
if (!interaction.isCommand() || !interaction.guildId) return; | ||
|
||
const handler = interactionHandlers.get(interaction.commandName); | ||
|
||
try { | ||
if (handler) { | ||
await handler(interaction, recordable, client, getVoiceConnection(interaction.guildId)); | ||
} else { | ||
await interaction.reply('Unknown command'); | ||
} | ||
} catch (error) { | ||
console.warn(error); | ||
} | ||
}); | ||
|
||
client.on('error', console.warn); | ||
|
||
void client.login(token); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { EndBehaviorType, VoiceReceiver } from '@discordjs/voice'; | ||
import { User } from 'discord.js'; | ||
import { createWriteStream } from 'fs'; | ||
import { opus } from 'prism-media'; | ||
import { pipeline } from 'stream'; | ||
|
||
function getDisplayName(userId: string, user?: User) { | ||
return user ? `${user.username}_${user.discriminator}` : userId; | ||
} | ||
|
||
export function createListeningStream(receiver: VoiceReceiver, userId: string, user?: User) { | ||
const opusStream = receiver.subscribe(userId, { | ||
end: { | ||
behavior: EndBehaviorType.AfterSilence, | ||
duration: 100, | ||
}, | ||
}); | ||
|
||
const oggStream = new opus.OggLogicalBitstream({ | ||
opusHead: new opus.OpusHead({ | ||
channelCount: 2, | ||
sampleRate: 48000, | ||
}), | ||
pageSizeControl: { | ||
maxPackets: 10, | ||
}, | ||
}); | ||
|
||
const filename = `./recordings/${Date.now()}-${getDisplayName(userId, user)}.ogg`; | ||
|
||
const out = createWriteStream(filename); | ||
|
||
console.log(`👂 Started recording ${filename}`); | ||
|
||
pipeline(opusStream, oggStream, out, (err) => { | ||
if (err) { | ||
console.warn(`❌ Error recording file ${filename} - ${err.message}`); | ||
} else { | ||
console.log(`✅ Recorded ${filename}`); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { Guild } from 'discord.js'; | ||
|
||
export const deploy = async (guild: Guild) => { | ||
await guild.commands.set([ | ||
{ | ||
name: 'join', | ||
description: 'Joins the voice channel that you are in', | ||
}, | ||
{ | ||
name: 'record', | ||
description: 'Enables recording for a user', | ||
options: [ | ||
{ | ||
name: 'speaker', | ||
type: 'USER' as const, | ||
description: 'The user to record', | ||
required: true, | ||
}, | ||
], | ||
}, | ||
{ | ||
name: 'leave', | ||
description: 'Leave the voice channel', | ||
}, | ||
]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { entersState, joinVoiceChannel, VoiceConnection, VoiceConnectionStatus } from '@discordjs/voice'; | ||
import { Client, CommandInteraction, GuildMember, Snowflake } from 'discord.js'; | ||
import { createListeningStream } from './createListeningStream'; | ||
|
||
async function join( | ||
interaction: CommandInteraction, | ||
recordable: Set<Snowflake>, | ||
client: Client, | ||
connection?: VoiceConnection, | ||
) { | ||
await interaction.deferReply(); | ||
if (!connection) { | ||
if (interaction.member instanceof GuildMember && interaction.member.voice.channel) { | ||
const channel = interaction.member.voice.channel; | ||
connection = joinVoiceChannel({ | ||
channelId: channel.id, | ||
guildId: channel.guild.id, | ||
selfDeaf: false, | ||
selfMute: true, | ||
adapterCreator: channel.guild.voiceAdapterCreator, | ||
}); | ||
} else { | ||
await interaction.followUp('Join a voice channel and then try that again!'); | ||
return; | ||
} | ||
} | ||
|
||
try { | ||
await entersState(connection, VoiceConnectionStatus.Ready, 20e3); | ||
const receiver = connection.receiver; | ||
|
||
receiver.speaking.on('start', (userId) => { | ||
if (recordable.has(userId)) { | ||
createListeningStream(receiver, userId, client.users.cache.get(userId)); | ||
} | ||
}); | ||
} catch (error) { | ||
console.warn(error); | ||
await interaction.followUp('Failed to join voice channel within 20 seconds, please try again later!'); | ||
} | ||
|
||
await interaction.followUp('Ready!'); | ||
} | ||
|
||
async function record( | ||
interaction: CommandInteraction, | ||
recordable: Set<Snowflake>, | ||
client: Client, | ||
connection?: VoiceConnection, | ||
) { | ||
if (connection) { | ||
const userId = interaction.options.get('speaker')!.value! as Snowflake; | ||
recordable.add(userId); | ||
|
||
const receiver = connection.receiver; | ||
if (connection.receiver.speaking.users.has(userId)) { | ||
createListeningStream(receiver, userId, client.users.cache.get(userId)); | ||
} | ||
|
||
await interaction.reply({ ephemeral: true, content: 'Listening!' }); | ||
} else { | ||
await interaction.reply({ ephemeral: true, content: 'Join a voice channel and then try that again!' }); | ||
} | ||
} | ||
|
||
async function leave( | ||
interaction: CommandInteraction, | ||
recordable: Set<Snowflake>, | ||
client: Client, | ||
connection?: VoiceConnection, | ||
) { | ||
if (connection) { | ||
connection.destroy(); | ||
recordable.clear(); | ||
await interaction.reply({ ephemeral: true, content: 'Left the channel!' }); | ||
} else { | ||
await interaction.reply({ ephemeral: true, content: 'Not playing in this server!' }); | ||
} | ||
} | ||
|
||
export const interactionHandlers = new Map< | ||
string, | ||
( | ||
interaction: CommandInteraction, | ||
recordable: Set<Snowflake>, | ||
client: Client, | ||
connection?: VoiceConnection, | ||
) => Promise<void> | ||
>(); | ||
interactionHandlers.set('join', join); | ||
interactionHandlers.set('record', record); | ||
interactionHandlers.set('leave', leave); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "./tsconfig.json" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"baseUrl": ".", | ||
"outDir": "dist", | ||
"paths": { | ||
"@discordjs/voice": ["../../"], | ||
"libsodium-wrappers": ["./node_modules/libsodium-wrappers"] | ||
} | ||
}, | ||
"include": ["src/*.ts"], | ||
"exclude": [""] | ||
} |
Oops, something went wrong.