On github, you can find a large number of music bots that are able to play music content with chat commands in the Discord voice channel. As it is easy to guess, basically, they are all sharpened for playing Youtube links (as far as I understand, now this opportunity has been cut off and Discord on its side stops such broadcasts in order to avoid rights violations).
If you study the discordjs documentation on interaction with voice channels, you can understand that you can play both files and links with Internet radio station streams.
I decided to use Internet radio, firstly, because I already had a radio broadcasting server deployed, and secondly, the Youtube stream periodically crashes for a number of reasons, which means that I would have to insert a new valid link into the script every time.
The most popular protocols for Internet broadcasting are Shoutcast and Icecast. I stopped at Shoutcast because I got it first. But as it turned out later, it is also a little easier to set up than Icecast.
Shoutcast
You need to create a folder where the radio server files will be stored, go to it, download the archive with the current version of Shoutcast and unzip it into the created folder:
mkdir shoutcastcd shoutcastwget https://download.nullsoft.com/shoutcast/tools/sc_serv2_linux_x64-latest.tar.gztar -xzf sc_serv2_linux_x64-latest.tar.gzBy default, Shoutcast uses port 8000 to connect clients, so you need to add a rule to the firewall that will allow such connections:
sudo ufw allow 8000Now you need to create and fill a configuration file with certain contents:
nano sc_serv.confadminpassword=password1
password=password2
requirestreamconfigs=1
streamadminpassword_1=password3
streamid_1=1
streampassword_1=password4
streampath_1=http://YOUR_VPS_IP:8000
logfile=logs/sc_serv.log
w3clog=logs/sc_w3c.log
banfile=control/sc_serv.ban
ripfile=control/sc_serv.ripAll passwords must be different, otherwise the server will give an error at startup.
Also, ‘streampassword_1’ will be needed later in order to run an audio stream from Liquidsoap to the server.
When the configuration file is ready, you can start the server as a background process:
./sc_serv &Now, at: http://YOUR_VPS_IP:8000 The Shoutcast server interface is available. Using the login: ‘admin’ and the password that you specified in the config file, you can enter the advanced settings. But in fact, this is not necessary. The main thing is that the server is now active. If something went wrong, the server background process can be stopped with the command:
killall sc_servLiquidsoap
When Mikulski_Radio was running in audio + looped background mode, there were no problems in getting audio to the Shoutcast server, since Liquidsoap can run multiple streams in parallel. That is, the ending of the Liquid soap script looked like this:
#вывод / output
output.url(fallible=true, url=url, enc, radio)
output.shoutcast(%mp3, host="YOUR_VPS_IP", port=8000, password="streampassword_1 FROM Shoutcast CONFIG FILE", audio)
Where ‘output.url’ is the output to the video stream (which first comes to nginx with an rtmp module, and it, in turn, streams to all the necessary sites), and radio is the source in which audio and video sources are mixed (‘audio’=playlist with tracks, ‘video’=video file with looped animation, plus text that is overlaid on top of the video from other variables). More detailed: https://mikulski.rocks/lofi-stream-24-7guide/
‘output.shoutcast’ – the actual output to the Shoutcast server, where only the audio source is sent.
That’s it! After that, we have two parallel streams with the same metadata and playback order.
But after switching back to video playback, such a code already gave an error. Since now ‘audio’ is a single source of both sound and video.
It is likely that it is possible to resolve these conflicts inside Liquidsoap using the right code, but I decided to go in a simpler way: launch another instance of Liquidsoap, which would accept an rtmp link as an audio source, which appears in nginx (through which you can directly connect to the video stream, for example, through a VLC player. More detailed: https://mikulski.rocks/your-own-restream-server/):
audio = mksafe(input.rtmp(listen=false, "rtmp://IP_VPS/live/streamname"))
output.shoutcast(%mp3, format="audio/mp3", icy_metadata="true", name="Mikulski_Radio", genre="Alternative",
host="IP_VPS", port=8000, password="streampassword_1", audio)
Here it is important to make the incoming source safe (‘mksafe’), so that in case of a stream dump, the script does not break, but begins to broadcast silence. The ‘listen=false’ argument is also important, otherwise without these two add-ons, the script will throw an error after launch.
In ‘output.shoutcast’, you must add the ‘icy_metadata’ argument (true or false, it doesn’t matter as far as I understand), so that the Shoutcast understands exactly how to react to the fact that metadata is not being received. Also, you need to set the name of the Internet radio (name=” “) and optionally, the genre.
The disadvantage of this approach is that now there is nowhere to get metadata (‘artist – song’) (although, of course, you could try to program something by taking metadata from the generated text file), but on the other hand it is not necessary: this radio will not go anywhere else anyway, except in Discord, and in Discord the current track is displayed in the bot status in a different way from the main stream.
Discord
As it was said at the very beginning of this post – there are a lot of music bots for Discord. The problem is that most of them are overloaded with functionality I don’t need (feeding links for playback, connecting on command from a chat, and so on), and also that different versions of the discordjs API and related node modules lead to all sorts of conflicts, leading, as a rule, to complete inactivity.
Initially, I used discobot, made on the logic of discordjs-12th version. It worked properly, with a reservation for random loss of connection with the voice channel or broadcast silence (which was explained by the features of the API). However, all this was treated by adding ‘process.exit(0)’ to the functions that catch errors so that the script stops its work and pm2 automatically restarts it. In general, it is tolerable, and most importantly it works.
However, after a break in the radio broadcast in Discord (after Mikulski_Radio returned to the video format), discobot was still successfully connected to the voice channel, but did not launch an instance of ffmpeg, which should decode audio into an opus format that Discord understands. In general, after completely reinstalling the bot and unsuccessfully trying to edit the code, I decided to look for other options.
Perhaps it’s time to note here that since the 13th version of discordjs, interaction with voice channels has been separated into an independent node module – “@discordjs/voice”.
At first, I spent several evenings trying to write a bot according to the instructions of the latest version of discordjs on my own (after all, in fact, I need the simplest functionality). I managed to run the ffmpeg transcoder, but the bot did not connect to the voice channel in any way (the instruction turned out to be not very noob-user-friendly). I started trying to install other bots from github, whose code I could understand. One of them worked and successfully launched music into the voice channel.. But, after a minute, the sound stopped and was no longer restored (now I assume that this was caused by the fact that the audio bitrate of 128kbs was set in the script, and on Discord servers without Nitro, the maximum allowed in voice channels is 96kbs).
In the end, I found the most simple (what I needed from the very beginning) script discord-js-v13-24-7-radio.
Discordjs differences between versions 13 and 14 are not as striking as with the 12th, so compatibility with the last (at the time of writing) 14th has been preserved. With the exception that instead of npm install sodium, you need to add two other modules.
In general, this is what my package.json looks like:
{
"dependencies": {
"@discordjs/opus": "^0.9.0",
"@discordjs/voice": "^0.16.0",
"discord.js": "^14.9.0",
"ffmpeg-static": "^5.1.0",
"libsodium": "^0.7.11",
"libsodium-wrappers": "^0.7.11",
"play-dl": "^1.9.6"
}
}Installing the necessary modules:
npm install discord.js
npm install @discordjs/voice
npm install @discordjs/opus
npm install play-dl
npm install ffmpeg-static
npm install libsodium
npm install libsodium-wrappersI have slightly modified the script itself by adding config.json:
{
"LINK": "http://IP_VPS:8000/Mikulski_Radio", //link to the Shoutcast-stream
"CHANNEL_ID": " ", //voice channel id
"GUILD_ID": " ", //discord server id
"TOKEN":" " //bot token
}I added log signals for connection/reconnection/disconnection and randomly added error catches to the log (I will not specify this code here, because there has not been a single error in a few days of work, which is surprising):
const Discord = require('discord.js');
const { LINK, CHANNEL_ID, GUILD_ID, TOKEN } = require("./config.json");
const { createAudioPlayer, createAudioResource , StreamType, demuxProbe, joinVoiceChannel, NoSubscriberBehavior, AudioPlayerStatus, VoiceConnectionStatus, getVoiceConnection } = require('@discordjs/voice');
const client = new Discord.Client({ intents: [
Discord.GatewayIntentBits.Guilds,
Discord.GatewayIntentBits.GuildVoiceStates
]});
client.once('ready', () => {
console.log("Status: Radio Connected to discord");
});
client.on('ready', async () => {
function radiostream () {
let guild = client.guilds.cache.get(GUILD_ID);
const voiceChannel = guild.channels.cache.get(CHANNEL_ID);
const connection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: voiceChannel.guild.id,
adapterCreator: voiceChannel.guild.voiceAdapterCreator,
});
connection.on("error", e => {
console.log("Status: Radio Connection error");
console.log(e);
process.exit(0);
});
connection.on(VoiceConnectionStatus.Signalling, () => {
console.log('DiscordRadio requests connection to the Voice Channel');
process.exit(0);
});
connection.on(VoiceConnectionStatus.Disconnected, () => {
console.log('DiscordRadio disconnected from the Voice Channel');
process.exit(0);
});
const resource = createAudioResource(LINK);
let player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
},
});
player.play(resource);
player.on('error', error => {
console.log('Radio Error: ', error.message);
radiostream();
});
player.on(AudioPlayerStatus.Idle, () => {
radiostream();
});
connection.subscribe(player);
connection.on('stateChange', (old_state, new_state) => {
if (old_state.status === VoiceConnectionStatus.Ready && new_state.status === VoiceConnectionStatus.Connecting) {
connection.configureNetworking();
}
});
}
radiostream();
})
client.login(TOKEN);If I manage to catch errors that haven’t happened yet, then I’ll add this code in this post, well, I’ll add ‘process.exit()’ for these cases so that the pm2 manager automatically restarts the bot. And so everything works very stably and has been for a long time!
UPD:
1) Caught a connection error – added a block (“Status: Radio Connection error”).
2) I noticed a curious feature: if no one enters the voice channel for a while, then the ffmpeg instance stops its work. When the user joins the voice channel after that, a fragment is played first, apparently stored in the buffer, after which ffmpeg resumes its process and switches the audio to the current source.
In order for the actual audio to play continuously, I added arguments to “let player = createAudioPlayer” that exclude ffmpeg stopping.
3) It was possible to catch more cases when the bot’s work is disrupted.
Most often, the following happens: the bot goes into the status of a request to connect to the voice channel and can no longer join.
It also rarely happens that the bot just crashes out of the voice channel.
Added the lines process.exit(0) to the script so that pm2 reloads the script.
