mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-17 03:53:29 +01:00
chore: Redirect voice examples to the new repository (#8737)
This commit is contained in:
@@ -1,2 +1 @@
|
|||||||
*.d.ts
|
*.d.ts
|
||||||
examples
|
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ try installing another.
|
|||||||
- [`FFmpeg`](https://ffmpeg.org/) (installed and added to environment)
|
- [`FFmpeg`](https://ffmpeg.org/) (installed and added to environment)
|
||||||
- `ffmpeg-static`: ^4.2.7 (npm install)
|
- `ffmpeg-static`: ^4.2.7 (npm install)
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
The [voice-examples][voice-examples] repository contains examples on how to use this package. Feel free to check them out if you need a nudge in the right direction.
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [Website][website] ([source][website-source])
|
- [Website][website] ([source][website-source])
|
||||||
@@ -99,3 +103,4 @@ nudge in the right direction, please don't hesitate to join our official [discor
|
|||||||
[npm]: https://www.npmjs.com/package/@discordjs/voice
|
[npm]: https://www.npmjs.com/package/@discordjs/voice
|
||||||
[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries
|
[related-libs]: https://discord.com/developers/docs/topics/community-resources#libraries
|
||||||
[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md
|
[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md
|
||||||
|
[voice-examples]: https://github.com/discordjs/voice-examples
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
| [Recorder](./recorder) | An example of using voice receive to create a bot that can record audio from users |
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
This is free and unencumbered software released into the public domain.
|
|
||||||
|
|
||||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
||||||
distribute this software, either in source code form or as a compiled
|
|
||||||
binary, for any purpose, commercial or non-commercial, and by any
|
|
||||||
means.
|
|
||||||
|
|
||||||
In jurisdictions that recognize copyright laws, the author or authors
|
|
||||||
of this software dedicate any and all copyright interest in the
|
|
||||||
software to the public domain. We make this dedication for the benefit
|
|
||||||
of the public at large and to the detriment of our heirs and
|
|
||||||
successors. We intend this dedication to be an overt act of
|
|
||||||
relinquishment in perpetuity of all present and future rights to this
|
|
||||||
software under copyright law.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
||||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
For more information, please refer to <http://unlicense.org/>
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Basic Example
|
|
||||||
|
|
||||||
This example will demonstrate how to join a voice channel and play resources, with some best practice
|
|
||||||
assistance on making sure you aren't waiting indefinitely for things to happen.
|
|
||||||
|
|
||||||
To achieve this, the example sets some fairly arbitrary time constraints for things such as joining
|
|
||||||
voice channels and audio becoming available.
|
|
||||||
|
|
||||||
## Code snippet
|
|
||||||
|
|
||||||
This code snippet doesn't include any comments for brevity. If you want to see the full source code,
|
|
||||||
check the other files in this folder!
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { Client, VoiceChannel, Intents } from 'discord.js';
|
|
||||||
import {
|
|
||||||
joinVoiceChannel,
|
|
||||||
createAudioPlayer,
|
|
||||||
createAudioResource,
|
|
||||||
entersState,
|
|
||||||
StreamType,
|
|
||||||
AudioPlayerStatus,
|
|
||||||
VoiceConnectionStatus,
|
|
||||||
} from '@discordjs/voice';
|
|
||||||
import { createDiscordJSAdapter } from './adapter';
|
|
||||||
|
|
||||||
const player = createAudioPlayer();
|
|
||||||
|
|
||||||
function playSong() {
|
|
||||||
const resource = createAudioResource('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', {
|
|
||||||
inputType: StreamType.Arbitrary,
|
|
||||||
});
|
|
||||||
|
|
||||||
player.play(resource);
|
|
||||||
|
|
||||||
return entersState(player, AudioPlayerStatus.Playing, 5e3);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectToChannel(channel: VoiceChannel) {
|
|
||||||
const connection = joinVoiceChannel({
|
|
||||||
channelId: channel.id,
|
|
||||||
guildId: channel.guild.id,
|
|
||||||
adapterCreator: createDiscordJSAdapter(channel),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await entersState(connection, VoiceConnectionStatus.Ready, 30e3);
|
|
||||||
return connection;
|
|
||||||
} catch (error) {
|
|
||||||
connection.destroy();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new Client({
|
|
||||||
ws: { intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_VOICE_STATES] },
|
|
||||||
});
|
|
||||||
|
|
||||||
client.login('token here');
|
|
||||||
|
|
||||||
client.on('ready', async () => {
|
|
||||||
console.log('Discord.js client is ready!');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await playSong();
|
|
||||||
console.log('Song is ready to play!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('message', async (message) => {
|
|
||||||
if (!message.guild) return;
|
|
||||||
|
|
||||||
if (message.content === '-join') {
|
|
||||||
const channel = message.member?.voice.channel;
|
|
||||||
|
|
||||||
if (channel) {
|
|
||||||
try {
|
|
||||||
const connection = await connectToChannel(channel);
|
|
||||||
connection.subscribe(player);
|
|
||||||
message.reply('Playing now!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message.reply('Join a voice channel then try again!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { DiscordGatewayAdapterCreator, DiscordGatewayAdapterLibraryMethods } from '../../';
|
|
||||||
import { Snowflake, Client, Guild, VoiceBasedChannel, Status, Events } from 'discord.js';
|
|
||||||
import {
|
|
||||||
GatewayDispatchEvents,
|
|
||||||
GatewayVoiceServerUpdateDispatchData,
|
|
||||||
GatewayVoiceStateUpdateDispatchData,
|
|
||||||
} from 'discord-api-types/v9';
|
|
||||||
|
|
||||||
const adapters = new Map<Snowflake, DiscordGatewayAdapterLibraryMethods>();
|
|
||||||
const trackedClients = new Set<Client>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks a Discord.js client, listening to VOICE_SERVER_UPDATE and VOICE_STATE_UPDATE events
|
|
||||||
*
|
|
||||||
* @param client - The Discord.js Client to track
|
|
||||||
*/
|
|
||||||
function trackClient(client: Client) {
|
|
||||||
if (trackedClients.has(client)) return;
|
|
||||||
trackedClients.add(client);
|
|
||||||
client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (payload: GatewayVoiceServerUpdateDispatchData) => {
|
|
||||||
adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
|
|
||||||
});
|
|
||||||
client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (payload: GatewayVoiceStateUpdateDispatchData) => {
|
|
||||||
if (payload.guild_id && payload.session_id && payload.user_id === client.user?.id) {
|
|
||||||
adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
client.on(Events.ShardDisconnect, (_, shardId) => {
|
|
||||||
const guilds = trackedShards.get(shardId);
|
|
||||||
if (guilds) {
|
|
||||||
for (const guildID of guilds.values()) {
|
|
||||||
adapters.get(guildID)?.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trackedShards.delete(shardId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackedShards = new Map<number, Set<Snowflake>>();
|
|
||||||
|
|
||||||
function trackGuild(guild: Guild) {
|
|
||||||
let guilds = trackedShards.get(guild.shardId);
|
|
||||||
if (!guilds) {
|
|
||||||
guilds = new Set();
|
|
||||||
trackedShards.set(guild.shardId, guilds);
|
|
||||||
}
|
|
||||||
guilds.add(guild.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an adapter for a Voice Channel.
|
|
||||||
*
|
|
||||||
* @param channel - The channel to create the adapter for
|
|
||||||
*/
|
|
||||||
export function createDiscordJSAdapter(channel: VoiceBasedChannel): DiscordGatewayAdapterCreator {
|
|
||||||
return (methods) => {
|
|
||||||
adapters.set(channel.guild.id, methods);
|
|
||||||
trackClient(channel.client);
|
|
||||||
trackGuild(channel.guild);
|
|
||||||
return {
|
|
||||||
sendPayload(data) {
|
|
||||||
if (channel.guild.shard.status === Status.Ready) {
|
|
||||||
channel.guild.shard.send(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
destroy() {
|
|
||||||
return adapters.delete(channel.guild.id);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { Client, VoiceBasedChannel, VoiceChannel } from 'discord.js';
|
|
||||||
import {
|
|
||||||
joinVoiceChannel,
|
|
||||||
createAudioPlayer,
|
|
||||||
createAudioResource,
|
|
||||||
entersState,
|
|
||||||
StreamType,
|
|
||||||
AudioPlayerStatus,
|
|
||||||
VoiceConnectionStatus,
|
|
||||||
} from '@discordjs/voice';
|
|
||||||
import { createDiscordJSAdapter } from './adapter';
|
|
||||||
import { GatewayIntentBits } from 'discord-api-types/v9';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In this example, we are creating a single audio player that plays to a number of voice channels.
|
|
||||||
* The audio player will play a single track.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the audio player. We will use this for all of our connections.
|
|
||||||
*/
|
|
||||||
const player = createAudioPlayer();
|
|
||||||
|
|
||||||
function playSong() {
|
|
||||||
/**
|
|
||||||
* Here we are creating an audio resource using a sample song freely available online
|
|
||||||
* (see https://www.soundhelix.com/audio-examples)
|
|
||||||
*
|
|
||||||
* We specify an arbitrary inputType. This means that we aren't too sure what the format of
|
|
||||||
* the input is, and that we'd like to have this converted into a format we can use. If we
|
|
||||||
* were using an Ogg or WebM source, then we could change this value. However, for now we
|
|
||||||
* will leave this as arbitrary.
|
|
||||||
*/
|
|
||||||
const resource = createAudioResource('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', {
|
|
||||||
inputType: StreamType.Arbitrary,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We will now play this to the audio player. By default, the audio player will not play until
|
|
||||||
* at least one voice connection is subscribed to it, so it is fine to attach our resource to the
|
|
||||||
* audio player this early.
|
|
||||||
*/
|
|
||||||
player.play(resource);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Here we are using a helper function. It will resolve if the player enters the Playing
|
|
||||||
* state within 5 seconds, otherwise it will reject with an error.
|
|
||||||
*/
|
|
||||||
return entersState(player, AudioPlayerStatus.Playing, 5e3);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectToChannel(channel: VoiceBasedChannel) {
|
|
||||||
/**
|
|
||||||
* Here, we try to establish a connection to a voice channel. If we're already connected
|
|
||||||
* to this voice channel, @discordjs/voice will just return the existing connection for us!
|
|
||||||
*/
|
|
||||||
const connection = joinVoiceChannel({
|
|
||||||
channelId: channel.id,
|
|
||||||
guildId: channel.guild.id,
|
|
||||||
adapterCreator: createDiscordJSAdapter(channel),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If we're dealing with a connection that isn't yet Ready, we can set a reasonable
|
|
||||||
* time limit before giving up. In this example, we give the voice connection 30 seconds
|
|
||||||
* to enter the ready state before giving up.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
/**
|
|
||||||
* Allow ourselves 30 seconds to join the voice channel. If we do not join within then,
|
|
||||||
* an error is thrown.
|
|
||||||
*/
|
|
||||||
await entersState(connection, VoiceConnectionStatus.Ready, 30e3);
|
|
||||||
/**
|
|
||||||
* At this point, the voice connection is ready within 30 seconds! This means we can
|
|
||||||
* start playing audio in the voice channel. We return the connection so it can be
|
|
||||||
* used by the caller.
|
|
||||||
*/
|
|
||||||
return connection;
|
|
||||||
} catch (error) {
|
|
||||||
/**
|
|
||||||
* At this point, the voice connection has not entered the Ready state. We should make
|
|
||||||
* sure to destroy it, and propagate the error by throwing it, so that the calling function
|
|
||||||
* is aware that we failed to connect to the channel.
|
|
||||||
*/
|
|
||||||
connection.destroy();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main code
|
|
||||||
* =========
|
|
||||||
* Here we will implement the helper functions that we have defined above.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const client = new Client({
|
|
||||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates],
|
|
||||||
});
|
|
||||||
|
|
||||||
void client.login('token here');
|
|
||||||
|
|
||||||
client.on('ready', async () => {
|
|
||||||
console.log('Discord.js client is ready!');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to get our song ready to play for when the bot joins a voice channel
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
await playSong();
|
|
||||||
console.log('Song is ready to play!');
|
|
||||||
} catch (error) {
|
|
||||||
/**
|
|
||||||
* The song isn't ready to play for some reason :(
|
|
||||||
*/
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('messageCreate', async (message) => {
|
|
||||||
if (!message.guild) return;
|
|
||||||
|
|
||||||
if (message.content === '-join') {
|
|
||||||
const channel = message.member?.voice.channel;
|
|
||||||
|
|
||||||
if (channel) {
|
|
||||||
/**
|
|
||||||
* The user is in a voice channel, try to connect.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
const connection = await connectToChannel(channel);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We have successfully connected! Now we can subscribe our connection to
|
|
||||||
* the player. This means that the player will play audio in the user's
|
|
||||||
* voice channel.
|
|
||||||
*/
|
|
||||||
connection.subscribe(player);
|
|
||||||
await message.reply('Playing now!');
|
|
||||||
} catch (error) {
|
|
||||||
/**
|
|
||||||
* Unable to connect to the voice channel within 30 seconds :(
|
|
||||||
*/
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* The user is not in a voice channel.
|
|
||||||
*/
|
|
||||||
void message.reply('Join a voice channel then try again!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
2
packages/voice/examples/radio-bot/.gitignore
vendored
2
packages/voice/examples/radio-bot/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
config.json
|
|
||||||
package-lock.json
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
# Discord Radio Bot 🎧
|
|
||||||
|
|
||||||
A proof-of-concept radio bot that uses @discordjs/voice and discord.js. Streams audio from an audio output hardware device on your computer over a Discord voice channel.
|
|
||||||
|
|
||||||
**Works on:**
|
|
||||||
|
|
||||||
- Linux (via PulseAudio `pulse`)
|
|
||||||
- Windows (via DirectShow `dshow`)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the main @discordjs/voice repo, then install dependencies and build
|
|
||||||
$ npm install
|
|
||||||
$ npm run build
|
|
||||||
|
|
||||||
# Enter this example's directory, create a config file and start!
|
|
||||||
$ cd examples/radio-bot
|
|
||||||
$ npm install
|
|
||||||
$ nano config.json
|
|
||||||
$ npm start
|
|
||||||
|
|
||||||
# Join a voice channel in Discord, then send "-join"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuring on Windows via `dshow`
|
|
||||||
|
|
||||||
Run `ffmpeg -list_devices true -f dshow -i dummy` and observe output containing something similar:
|
|
||||||
|
|
||||||
```
|
|
||||||
DirectShow audio devices
|
|
||||||
"Stereo Mix (Realtek(R) Audio)"
|
|
||||||
Alternative name "@device_cm_{ID1}\wave_{ID2}"
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, playing the above device will mirror audio from the speaker output of your machine. Your `config.json` should then be considered like so:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"token": "discord_bot_token",
|
|
||||||
"device": "Stereo Mix (Realtek(R) Audio)",
|
|
||||||
"type": "dshow",
|
|
||||||
"maxTransmissionGap": 5000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuring on Linux via `pulse`
|
|
||||||
|
|
||||||
Run `pactl list short sources` and observe output containing something similar:
|
|
||||||
|
|
||||||
```
|
|
||||||
5 alsa_output.pci.3.analog-stereo.monitor module-alsa-card.c s16le 2ch 44100Hz IDLE
|
|
||||||
```
|
|
||||||
|
|
||||||
Then configure your `config.json` with the device you'd like to use:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"token": "discord_bot_token",
|
|
||||||
"device": "alsa_output.pci.3.analog-stereo.monitor",
|
|
||||||
"type": "pulse",
|
|
||||||
"maxTransmissionGap": 5000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"token": "discord_bot_token",
|
|
||||||
"device": "audio_hw_device_id",
|
|
||||||
"type": "pulse",
|
|
||||||
"maxTransmissionGap": 5000
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
require('module-alias/register');
|
|
||||||
|
|
||||||
const { Client } = require('discord.js');
|
|
||||||
const prism = require('prism-media');
|
|
||||||
const config = require('./config.json');
|
|
||||||
const {
|
|
||||||
NoSubscriberBehavior,
|
|
||||||
StreamType,
|
|
||||||
createAudioPlayer,
|
|
||||||
createAudioResource,
|
|
||||||
entersState,
|
|
||||||
AudioPlayerStatus,
|
|
||||||
VoiceConnectionStatus,
|
|
||||||
joinVoiceChannel,
|
|
||||||
} = require('@discordjs/voice');
|
|
||||||
|
|
||||||
const player = createAudioPlayer({
|
|
||||||
behaviors: {
|
|
||||||
noSubscriber: NoSubscriberBehavior.Play,
|
|
||||||
maxMissedFrames: Math.round(config.maxTransmissionGap / 20),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
player.on('stateChange', (oldState, newState) => {
|
|
||||||
if (oldState.status === AudioPlayerStatus.Idle && newState.status === AudioPlayerStatus.Playing) {
|
|
||||||
console.log('Playing audio output on audio player');
|
|
||||||
} else if (newState.status === AudioPlayerStatus.Idle) {
|
|
||||||
console.log('Playback has stopped. Attempting to restart.');
|
|
||||||
attachRecorder();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function attachRecorder() {
|
|
||||||
player.play(
|
|
||||||
createAudioResource(
|
|
||||||
new prism.FFmpeg({
|
|
||||||
args: [
|
|
||||||
'-analyzeduration',
|
|
||||||
'0',
|
|
||||||
'-loglevel',
|
|
||||||
'0',
|
|
||||||
'-f',
|
|
||||||
config.type,
|
|
||||||
'-i',
|
|
||||||
config.type === 'dshow' ? `audio=${config.device}` : config.device,
|
|
||||||
'-acodec',
|
|
||||||
'libopus',
|
|
||||||
'-f',
|
|
||||||
'opus',
|
|
||||||
'-ar',
|
|
||||||
'48000',
|
|
||||||
'-ac',
|
|
||||||
'2',
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
inputType: StreamType.OggOpus,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
console.log('Attached recorder - ready to go!');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectToChannel(channel) {
|
|
||||||
const connection = joinVoiceChannel({
|
|
||||||
channelId: channel.id,
|
|
||||||
guildId: channel.guild.id,
|
|
||||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await entersState(connection, VoiceConnectionStatus.Ready, 30_000);
|
|
||||||
return connection;
|
|
||||||
} catch (error) {
|
|
||||||
connection.destroy();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new Client({ intents: ['GUILDS', 'GUILD_MESSAGES', 'GUILD_VOICE_STATES'] });
|
|
||||||
|
|
||||||
client.on('ready', async () => {
|
|
||||||
console.log('discord.js client is ready!');
|
|
||||||
attachRecorder();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('messageCreate', async (message) => {
|
|
||||||
if (!message.guild) return;
|
|
||||||
if (message.content === '-join') {
|
|
||||||
const channel = message.member?.voice.channel;
|
|
||||||
if (channel) {
|
|
||||||
try {
|
|
||||||
const connection = await connectToChannel(channel);
|
|
||||||
connection.subscribe(player);
|
|
||||||
await message.reply('Playing now!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await message.reply('Join a voice channel then try again!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
void client.login(config.token);
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "discord-radio-bot",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "A proof-of-concept radio bot for @discordjs/voice",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"start": "node index.js"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"discord",
|
|
||||||
"radio",
|
|
||||||
"bot",
|
|
||||||
"audio",
|
|
||||||
"speakers",
|
|
||||||
"hardware",
|
|
||||||
"dj"
|
|
||||||
],
|
|
||||||
"author": "Amish Shah <amish@shah.gg>",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@discordjs/voice": "file:../../",
|
|
||||||
"discord.js": "^13.7.0",
|
|
||||||
"libsodium-wrappers": "^0.7.9",
|
|
||||||
"module-alias": "^2.2.2",
|
|
||||||
"prism-media": "^1.3.1"
|
|
||||||
},
|
|
||||||
"_moduleAliases": {
|
|
||||||
"@root": ".",
|
|
||||||
"@discordjs/voice": "../../",
|
|
||||||
"libsodium-wrappers": "./node_modules/libsodium-wrappers"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"root": true,
|
|
||||||
"extends": "../../.eslintrc.json",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.eslint.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
packages/voice/examples/recorder/.gitignore
vendored
4
packages/voice/examples/recorder/.gitignore
vendored
@@ -1,4 +0,0 @@
|
|||||||
package-lock.json
|
|
||||||
auth.json
|
|
||||||
tsconfig.tsbuildinfo
|
|
||||||
recordings/*.ogg
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# 👂 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
|
|
||||||
```
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"token": "Your Discord bot token here"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"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 .",
|
|
||||||
"build": "tsc",
|
|
||||||
"build:check": "tsc --noEmit --incremental false"
|
|
||||||
},
|
|
||||||
"author": "Amish Shah <contact@shah.gg>",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@discordjs/opus": "^0.8.0",
|
|
||||||
"discord-api-types": "^0.33.3",
|
|
||||||
"discord.js": "^13.8.0",
|
|
||||||
"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.7.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { getVoiceConnection } from '@discordjs/voice';
|
|
||||||
import { GatewayIntentBits } from 'discord-api-types/v9';
|
|
||||||
import Discord, { Interaction, Constants } from 'discord.js';
|
|
||||||
import { deploy } from './deploy';
|
|
||||||
import { interactionHandlers } from './interactions';
|
|
||||||
|
|
||||||
const { token } = require('../auth.json') as { token: string };
|
|
||||||
|
|
||||||
const client = new Discord.Client({
|
|
||||||
intents: [GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { Events } = Constants;
|
|
||||||
|
|
||||||
client.on(Events.CLIENT_READY, () => console.log('Ready!'));
|
|
||||||
|
|
||||||
client.on(Events.MESSAGE_CREATE, 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(Events.INTERACTION_CREATE, 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(Events.ERROR, console.warn);
|
|
||||||
|
|
||||||
void client.login(token);
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { createWriteStream } from 'node:fs';
|
|
||||||
import { pipeline } from 'node:stream';
|
|
||||||
import { EndBehaviorType, VoiceReceiver } from '@discordjs/voice';
|
|
||||||
import type { User } from 'discord.js';
|
|
||||||
import * as prism from 'prism-media';
|
|
||||||
|
|
||||||
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: 1000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const oggStream = new prism.opus.OggLogicalBitstream({
|
|
||||||
opusHead: new prism.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}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord-api-types/v9';
|
|
||||||
import type { 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: ApplicationCommandOptionType.User,
|
|
||||||
description: 'The user to record',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'leave',
|
|
||||||
description: 'Leave the voice channel',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json"
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"paths": {
|
|
||||||
"@discordjs/voice": ["../../"],
|
|
||||||
"libsodium-wrappers": ["./node_modules/libsodium-wrappers"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src/*.ts"],
|
|
||||||
"exclude": [""]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user