diff --git a/src/main.rs b/src/main.rs index d576ee7..19663e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use serenity::prelude::*; // 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; +use songbird::{SerenityInit, Config, driver::DecodeMode}; mod commands; use commands::*; @@ -17,6 +17,7 @@ mod events; use events::*; mod utils; +mod vc; #[tokio::main] async fn main() { @@ -36,10 +37,13 @@ async fn main() { .configure(|c| c.prefix("ALAN! ")) .group(&GENERAL_GROUP); + let songbird_config = Config::default() + .decode_mode(DecodeMode::Decode); + let mut client = Client::builder(&token, intents) .event_handler(Handler) .framework(framework) - .register_songbird() + .register_songbird_from_config(songbird_config) .await .expect("Err creating client"); diff --git a/src/utils/guild_popin.rs b/src/utils/guild_popin.rs index aee39cc..fe35f9c 100644 --- a/src/utils/guild_popin.rs +++ b/src/utils/guild_popin.rs @@ -1,7 +1,7 @@ use rand::prelude::*; use serenity::model::id::GuildId; use serenity::Client; -use songbird::{create_player, ffmpeg, Call}; +use songbird::Call; use std::collections::HashMap; use std::ops::Range; use std::sync::Arc; @@ -9,7 +9,9 @@ use std::thread::sleep; use std::time::Duration; use serenity::model::prelude::{ChannelType, Guild}; -use serenity::prelude::{Context, RwLock, TypeMapKey}; +use serenity::prelude::{Context, RwLock, TypeMapKey, Mutex}; + +use crate::vc; #[derive(Debug)] struct GuildPopIn { @@ -50,30 +52,16 @@ async fn popin(ctx: Context, guild: Guild) { None => {} } } + let mut call = None; match most { Some(chan) => { - let manager = songbird::get(&ctx).await.expect("Songbird: intialization"); - let (call, status) = manager.join(guild.id, chan).await; - match status { - Ok(_) => { - let mut call = call.lock().await; - let ding_src = - std::env::var("DING_SOUND").expect("DING not found in DING_SOUND"); - let ding = ffmpeg(ding_src).await.expect("no ding."); - let (audio, handle) = create_player(ding); - call.play(audio); - tokio::spawn(popout_soon(ctx.clone(), guild, Some(call.clone()))); - } - Err(_err) => { - println!("Error joining channel"); - } - } + call = vc::join(ctx.clone(), guild.clone(), chan).await; } None => { println!("No good channel to join"); } } - // tokio::spawn(popout_soon(ctx.clone(), guild, None)); + tokio::spawn(popout_soon(ctx.clone(), guild, call)); } async fn popin_soon(ctx: Context, guild: Guild) { @@ -95,17 +83,24 @@ async fn popin_soon(ctx: Context, guild: Guild) { tokio::spawn(popin(ctx.clone(), guild)); } -async fn popout(ctx: Context, guild: Guild, call: Option) { +async fn popout(ctx: Context, guild: Guild, call: Option>>) { match call { - Some(mut call) => { - call.leave().await; + Some(call) => { + let mut call = call.lock().await; + let status = call.leave().await; + match status { + Ok(_) => {}, + Err(_err) => { + println!("Failed to leave call"); + } + } }, None => {} } start_dingdong(ctx.clone(), guild); } -async fn popout_soon(ctx: Context, guild: Guild, call: Option) { +async fn popout_soon(ctx: Context, guild: Guild, call: Option>>) { let data = ctx.data.read().await; let pops = data .get::() diff --git a/src/vc/mod.rs b/src/vc/mod.rs new file mode 100644 index 0000000..eceb945 --- /dev/null +++ b/src/vc/mod.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use serenity::{async_trait, + model::prelude::{ChannelId, Guild}, + prelude::{Context, Mutex}}; +use songbird::{EventHandler, Event, EventContext, + model::payload::{Speaking, ClientDisconnect}, ffmpeg, create_player, Call}; + +struct Receiver; + +impl Receiver { + pub fn new() -> Self { + // You can manage state here, such as a buffer of audio packet bytes so + // you can later store them in intervals. + Self { + + } + } +} + +#[async_trait] +impl EventHandler for Receiver { + #[allow(unused_variables)] + async fn act(&self, ctx: &EventContext<'_>) -> Option { + use EventContext as Ctx; + match ctx { + Ctx::SpeakingStateUpdate( + Speaking {speaking, ssrc, user_id, ..} + ) => { + // Discord voice calls use RTP, where every sender uses a randomly allocated + // *Synchronisation Source* (SSRC) to allow receivers to tell which audio + // stream a received packet belongs to. As this number is not derived from + // the sender's user_id, only Discord Voice Gateway messages like this one + // inform us about which random SSRC a user has been allocated. Future voice + // packets will contain *only* the SSRC. + // + // You can implement logic here so that you can differentiate users' + // SSRCs and map the SSRC to the User ID and maintain this state. + // Using this map, you can map the `ssrc` in `voice_packet` + // to the user ID and handle their audio packets separately. + println!( + "Speaking state update: user {:?} has SSRC {:?}, using {:?}", + user_id, + ssrc, + speaking, + ); + }, + Ctx::SpeakingUpdate(data) => { + // You can implement logic here which reacts to a user starting + // or stopping speaking, and to map their SSRC to User ID. + println!( + "Source {} has {} speaking.", + data.ssrc, + if data.speaking {"started"} else {"stopped"}, + ); + }, + Ctx::VoicePacket(data) => { + // An event which fires for every received audio packet, + // containing the decoded data. + if let Some(audio) = data.audio { + println!("Audio packet's first 5 samples: {:?}", audio.get(..5.min(audio.len()))); + println!( + "Audio packet sequence {:05} has {:04} bytes (decompressed from {}), SSRC {}", + data.packet.sequence.0, + audio.len() * std::mem::size_of::(), + data.packet.payload.len(), + data.packet.ssrc, + ); + } else { + println!("RTP packet, but no audio. Driver may not be configured to decode."); + } + }, + Ctx::RtcpPacket(data) => { + // An event which fires for every received rtcp packet, + // containing the call statistics and reporting information. + println!("RTCP packet received: {:?}", data.packet); + }, + Ctx::ClientDisconnect( + ClientDisconnect {user_id, ..} + ) => { + // You can implement your own logic here to handle a user who has left the + // voice channel e.g., finalise processing of statistics etc. + // You will typically need to map the User ID to their SSRC; observed when + // first speaking. + + println!("Client disconnected: user {:?}", user_id); + }, + _ => { + // We won't be registering this struct for any more event classes. + unimplemented!() + } + } + + None + } +} + +pub async fn join(ctx: Context, guild: Guild, cid: ChannelId) -> Option>> { + let manager = songbird::get(&ctx).await.expect("Songbird: intialization"); + let (call, status) = manager.join(guild.id, cid).await; + match status { + Ok(_) => { + { + let mut call = call.lock().await; + let ding_src = + std::env::var("DING_SOUND").expect("DING not found in DING_SOUND"); + let ding = ffmpeg(ding_src).await.expect("no ding."); + let (audio, handle) = create_player(ding); + call.play(audio); + } + return Some(call); + } + Err(_err) => { + println!("Error joining channel"); + } + } + None +}