This PR implements a custom scheduler for audio threads, which reduces thread use and (often) memory consumption. To save threads and memory (e.g., packet buffer allocations), Songbird parks Mixer tasks which do not have any live Tracks. These are now all co-located on a single async 'Idle' task. This task is responsible for managing UDP keepalive messages for each task, maintaining event state, and executing any Mixer task messages. Whenever any message arrives which adds a `Track`, the mixer task is moved to a live thread. The Idle task inspects task counts and execution time on each thread, choosing the first live thread with room, and creating a new one if needed. Each live thread is responsible for running as many live mixers as it can in a single tick every 20ms: this currently defaults to 16 mixers per thread, but is user-configurable. A live thread also stores RTP packet blocks to be written into by each sub-task. Each live thread has a conservative limit of 18ms that it will aim to stay under: if all work takes longer than this, it will offload the task with the highest mixing cost once per tick onto another (possibly new) live worker thread.
391 lines
10 KiB
Rust
391 lines
10 KiB
Rust
//! Requires the "client", "standard_framework", and "voice" features be enabled in your
|
|
//! Cargo.toml, like so:
|
|
//!
|
|
//! ```toml
|
|
//! [dependencies.serenity]
|
|
//! git = "https://github.com/serenity-rs/serenity.git"
|
|
//! features = ["client", "standard_framework", "voice"]
|
|
//! ```
|
|
use std::env;
|
|
|
|
// This trait adds the `register_songbird` and `register_songbird_with` methods
|
|
// to the client builder below, making it easy to install this voice client.
|
|
// The voice client can be retrieved in any command using `songbird::get(ctx).await`.
|
|
use songbird::SerenityInit;
|
|
|
|
// Event related imports to detect track creation failures.
|
|
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent};
|
|
|
|
// To turn user URLs into playable audio, we'll use yt-dlp.
|
|
use songbird::input::YoutubeDl;
|
|
|
|
// YtDl requests need an HTTP client to operate -- we'll create and store our own.
|
|
use reqwest::Client as HttpClient;
|
|
|
|
// Import the `Context` to handle commands.
|
|
use serenity::client::Context;
|
|
|
|
use serenity::{
|
|
async_trait,
|
|
client::{Client, EventHandler},
|
|
framework::{
|
|
standard::{
|
|
macros::{command, group},
|
|
Args, CommandResult,
|
|
},
|
|
StandardFramework,
|
|
},
|
|
model::{channel::Message, gateway::Ready},
|
|
prelude::{GatewayIntents, TypeMapKey},
|
|
Result as SerenityResult,
|
|
};
|
|
|
|
struct HttpKey;
|
|
|
|
impl TypeMapKey for HttpKey {
|
|
type Value = HttpClient;
|
|
}
|
|
|
|
struct Handler;
|
|
|
|
#[async_trait]
|
|
impl EventHandler for Handler {
|
|
async fn ready(&self, _: Context, ready: Ready) {
|
|
println!("{} is connected!", ready.user.name);
|
|
}
|
|
}
|
|
|
|
#[group]
|
|
#[commands(deafen, join, leave, mute, play, ping, undeafen, unmute)]
|
|
struct General;
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
// Configure the client with your Discord bot token in the environment.
|
|
let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
|
|
|
|
let framework = StandardFramework::new().group(&GENERAL_GROUP);
|
|
framework.configure(|c| c.prefix("~"));
|
|
|
|
let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT;
|
|
|
|
let mut client = Client::builder(&token, intents)
|
|
.event_handler(Handler)
|
|
.framework(framework)
|
|
.register_songbird()
|
|
// We insert our own HTTP client here to make use of in
|
|
// `~play`. If we wanted, we could supply cookies and auth
|
|
// details ahead of time.
|
|
//
|
|
// Generally, we don't want to make a new Client for every request!
|
|
.type_map_insert::<HttpKey>(HttpClient::new())
|
|
.await
|
|
.expect("Err creating client");
|
|
|
|
tokio::spawn(async move {
|
|
let _ = client
|
|
.start()
|
|
.await
|
|
.map_err(|why| println!("Client ended: {:?}", why));
|
|
});
|
|
|
|
let _signal_err = tokio::signal::ctrl_c().await;
|
|
println!("Received Ctrl-C, shutting down.");
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn deafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let handler_lock = match manager.get(guild_id) {
|
|
Some(handler) => handler,
|
|
None => {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
if handler.is_deaf() {
|
|
check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await);
|
|
} else {
|
|
if let Err(e) = handler.deafen(true).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Deafened").await);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let (guild_id, channel_id) = {
|
|
let guild = msg.guild(&ctx.cache).unwrap();
|
|
let channel_id = guild
|
|
.voice_states
|
|
.get(&msg.author.id)
|
|
.and_then(|voice_state| voice_state.channel_id);
|
|
|
|
(guild.id, channel_id)
|
|
};
|
|
|
|
let connect_to = match channel_id {
|
|
Some(channel) => channel,
|
|
None => {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
|
|
// Attach an event handler to see notifications of all track errors.
|
|
let mut handler = handler_lock.lock().await;
|
|
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
struct TrackErrorNotifier;
|
|
|
|
#[async_trait]
|
|
impl VoiceEventHandler for TrackErrorNotifier {
|
|
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
if let EventContext::Track(track_list) = ctx {
|
|
for (state, handle) in *track_list {
|
|
println!(
|
|
"Track {:?} encountered an error: {:?}",
|
|
handle.uuid(),
|
|
state.playing
|
|
);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
let has_handler = manager.get(guild_id).is_some();
|
|
|
|
if has_handler {
|
|
if let Err(e) = manager.remove(guild_id).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
|
|
} else {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn mute(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
let handler_lock = match manager.get(guild_id) {
|
|
Some(handler) => handler,
|
|
None => {
|
|
check_msg(msg.reply(ctx, "Not in a voice channel").await);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
if handler.is_mute() {
|
|
check_msg(msg.channel_id.say(&ctx.http, "Already muted").await);
|
|
} else {
|
|
if let Err(e) = handler.mute(true).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Now muted").await);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
|
|
check_msg(msg.channel_id.say(&ctx.http, "Pong!").await);
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
let url = match args.single::<String>() {
|
|
Ok(url) => url,
|
|
Err(_) => {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a URL to a video or audio")
|
|
.await,
|
|
);
|
|
|
|
return Ok(());
|
|
},
|
|
};
|
|
|
|
if !url.starts_with("http") {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Must provide a valid URL")
|
|
.await,
|
|
);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let http_client = {
|
|
let data = ctx.data.read().await;
|
|
data.get::<HttpKey>()
|
|
.cloned()
|
|
.expect("Guaranteed to exist in the typemap.")
|
|
};
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
|
|
let src = YoutubeDl::new(http_client, url);
|
|
let _ = handler.play_input(src.into());
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Playing song").await);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to play in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
if let Err(e) = handler.deafen(false).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to undeafen in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[command]
|
|
#[only_in(guilds)]
|
|
async fn unmute(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let guild_id = msg.guild_id.unwrap();
|
|
|
|
let manager = songbird::get(ctx)
|
|
.await
|
|
.expect("Songbird Voice client placed in at initialisation.")
|
|
.clone();
|
|
|
|
if let Some(handler_lock) = manager.get(guild_id) {
|
|
let mut handler = handler_lock.lock().await;
|
|
if let Err(e) = handler.mute(false).await {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, format!("Failed: {:?}", e))
|
|
.await,
|
|
);
|
|
}
|
|
|
|
check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await);
|
|
} else {
|
|
check_msg(
|
|
msg.channel_id
|
|
.say(&ctx.http, "Not in a voice channel to unmute in")
|
|
.await,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks that a message successfully sent; if not, then logs why to stdout.
|
|
fn check_msg(result: SerenityResult<Message>) {
|
|
if let Err(why) = result {
|
|
println!("Error sending message: {:?}", why);
|
|
}
|
|
}
|