Skip to main content
Your bot can join a voice channel and play sound there. Put a file in public/, then join the channel of the person who asks and play it:
src/commands/intro.ts
That is all. join() resolves once the bot is in the channel, play() once the file has been played, and leave() makes the bot leave. Everything else is done for you: the connection to Discord’s voice servers, the encryption, sending the sound in time, and the end-to-end encryption Discord requires for voice.
Everything a voice connection has is in the reference; join() is on VoiceChannel.

What can be played

play() takes a file of public/ (asset('sounds/intro.ogg')), a file sent on Discord, or a link starting with https://. Discord only plays Opus. A file that already is Opus costs nothing to play; anything else is converted while it plays by ffmpeg, which the bot finds by itself when it is installed. When it is not, play() says so:
Opus is told from the first bytes of the file, whatever its name, and a link or a file sent on Discord is downloaded as it plays. A link must lead to the sound itself (a file, a radio stream): the page of a video, on YouTube for example, is not one.
To play without ffmpeg in production, convert your sounds to Opus once, on your computer: ffmpeg -i intro.mp3 -c:a libopus intro.ogg.

A file sent on Discord

An attachment option lets the person send the song with the command, and play() takes it as is:
src/commands/song.ts

Pause, resume, stop

What is playing can be paused, resumed and stopped from anywhere: another command, a button. guild.voice is the connection of the bot in a server, or null when it is in no voice channel there.
src/commands/pause.ts
  • stop() stops what plays: its play() resolves at once.
  • A new play() replaces what was playing.
  • leave() leaves the channel; a connection that left can’t play any more, join() again.

Moving and being moved

join() on another voice channel of the same server moves the bot there, and gives back the same connection. When someone moves the bot or disconnects it, the connection follows: voice.channel is where it is now, and voice.connected becomes false when it was disconnected.
src/commands/follow.ts

Permissions and full channels

The bot needs Connect to join and Speak to play. Without them, join() or play() fails with what is missing:
A channel that is full (its user limit is reached) is refused too, unless the bot has Move Members. On a stage, the bot becomes a speaker when it has Mute Members; otherwise it asks to speak, and a moderator accepts.

Options of join()

  • deaf (default true): the bot hears nothing, so Discord does not send it what people say. It only plays.
  • mute (default false): the bot shows as muted.

Intents and end-to-end encryption

Joining needs the GUILD_VOICE_STATES intent: ChapterJS asks for it when a file of your project calls join(), and member.voiceChannel is known thanks to it. It is not privileged.
Discord encrypts every voice channel end to end (the DAVE protocol). Your bot takes part in it with libdave, the library of Discord’s own apps, which ChapterJS includes: nothing to install. While the bot is alone in a channel there is no one to encrypt for, so nothing is sent; as soon as someone joins, they hear it.
Recording what people say is not there yet.

Voice events

Who joins, leaves or moves between voice channels.

VoiceConnection

Everything a voice connection has.