From 76c1824bd6f0dcb157544862652b65af8f6539ce Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 01:09:53 +0100 Subject: recording: keep one buffer per guild --- src/commands/record.rs | 7 ++++--- src/recording/mod.rs | 21 ++++++++++----------- src/voice/mod.rs | 47 +++++++++++++++++++++++++++-------------------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/commands/record.rs b/src/commands/record.rs index 48cbc59..dfe736d 100644 --- a/src/commands/record.rs +++ b/src/commands/record.rs @@ -1,3 +1,4 @@ +use poise::serenity_prelude as serenity; use tracing::warn; use crate::error::Result; @@ -52,14 +53,14 @@ pub async fn record(ctx: Context<'_>) -> Result<()> { ctx.say(lang::L_RECORD_NOT_SAME_VC).await?; Ok(()) } - Some(_) => record_impl(ctx).await, + Some(_) => record_impl(ctx, guild_id).await, } } -async fn record_impl(ctx: Context<'_>) -> Result<()> { +async fn record_impl(ctx: Context<'_>, guild_id: serenity::GuildId) -> Result<()> { let data = ctx.data(); - let Some(rec) = data.voice_manager.get_recorder().await else { + let Some(rec) = data.voice_manager.get_recorder(guild_id).await else { ctx.say(lang::L_RECORD_NOT_RECORDING).await?; return Ok(()); }; diff --git a/src/recording/mod.rs b/src/recording/mod.rs index d0a94be..4cd2891 100644 --- a/src/recording/mod.rs +++ b/src/recording/mod.rs @@ -304,22 +304,21 @@ pub async fn on_bot_voice_state( if !voice_manager.recording_enabled() { return; } + let Some(guild_id) = guild_id else { + return; + }; // Already torn down by `VoiceManager::disconnect` in the `/stop` path; this // is the catch-all for leaves `disconnect` didn't initiate (kicks, drops). if new_channel.is_none() { - voice_manager.stop_recording().await; + voice_manager.stop_recording(guild_id).await; return; } - // Joined or moved into a channel. If the recorder is missing (e.g. a move - // broke the prior Call), try to (re)register on the current Call. - if new_channel != old_channel { - let existing = voice_manager.get_recorder().await.is_some(); - if !existing { - if let Some(gid) = guild_id { - if let Some(call) = voice_manager.get_call(gid) { - voice_manager.start_recording(&call).await; - } - } + // Joined or moved into a channel. If this guild's recorder is missing + // (e.g. a move broke the prior Call), try to (re)register on the current + // Call. + if new_channel != old_channel && voice_manager.get_recorder(guild_id).await.is_none() { + if let Some(call) = voice_manager.get_call(guild_id) { + voice_manager.start_recording(guild_id, &call).await; } } } diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 0b6022c..bbb73a0 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::io::Cursor; use std::pin::Pin; use std::sync::Arc; @@ -28,11 +29,13 @@ const RECORDING_SAMPLE_RATE: u32 = 48000; #[derive(Clone)] pub struct VoiceManager { pub songbird: Arc, - /// Live recorder for the bot's current voice connection, if any. + /// Live recorder per guild, if any. /// - /// `None` whenever the bot is not connected (or recording is disabled) — - /// so the ~86 MB ring only exists while actually recording a 15 min window. - pub recorder: Arc>>>, + /// Entries exist only while the bot is connected (and recording) in that + /// guild, so the ~86 MB ring only exists while actually recording. Keying + /// by guild prevents one guild's connection from replacing or stopping + /// another guild's buffer. + pub recorders: Arc>>>, pub recording_enabled: bool, pub recording_window_seconds: u64, } @@ -50,7 +53,7 @@ impl VoiceManager { ))?; Ok(Self { songbird, - recorder: Arc::new(Mutex::new(None)), + recorders: Arc::new(Mutex::new(HashMap::new())), recording_enabled, recording_window_seconds, }) @@ -60,10 +63,10 @@ impl VoiceManager { self.recording_enabled } - /// Construct a `Recorder`, register its `VoiceTick` handler on `call`, and - /// store the strong `Arc` in the slot. Idempotent: if the slot already - /// holds a recorder, the caller is responsible for not double-registering. - pub async fn start_recording(&self, call: &Arc>) { + /// Construct a `Recorder` for `guild_id`, register its `VoiceTick` handler + /// on `call`, and store the strong `Arc`. Replacing an existing entry drops + /// the previous ring; its handler's `Weak` then no-ops. + pub async fn start_recording(&self, guild_id: GuildId, call: &Arc>) { if !self.recording_enabled { return; } @@ -79,25 +82,29 @@ impl VoiceManager { handler, ); } - *self.recorder.lock().await = Some(recorder); + self.recorders + .lock() + .await + .insert(guild_id, recorder); info!( + guild = %guild_id, window_secs = self.recording_window_seconds, "recording: started rolling buffer for this voice connection" ); } - /// Drop the live recorder (frees the ring immediately). The songbird `Call` - /// may still exist briefly; the handler's `Weak` upgrades to `None` thereafter. - pub async fn stop_recording(&self) { - let prev = self.recorder.lock().await.take(); + /// Drop this guild's live recorder (frees the ring immediately). The + /// songbird `Call` may still exist briefly; the handler's `Weak` then no-ops. + pub async fn stop_recording(&self, guild_id: GuildId) { + let prev = self.recorders.lock().await.remove(&guild_id); if prev.is_some() { - info!("recording: stopped, ring buffer freed"); + info!(guild = %guild_id, "recording: stopped, ring buffer freed"); } } - /// Clone of the live recorder `Arc`, if any — used by `/record` to snapshot. - pub async fn get_recorder(&self) -> Option> { - self.recorder.lock().await.clone() + /// Clone of this guild's live recorder, if any — used by `/record`. + pub async fn get_recorder(&self, guild_id: GuildId) -> Option> { + self.recorders.lock().await.get(&guild_id).cloned() } pub async fn connect( @@ -115,7 +122,7 @@ impl VoiceManager { // (The bot's own VoiceStateUpdate also reconciles, but may // race songbird's Call creation — this never misses.) if self.recording_enabled { - self.start_recording(&call).await; + self.start_recording(guild_id, &call).await; } return Ok(call); } @@ -143,7 +150,7 @@ impl VoiceManager { pub async fn disconnect(&self, guild_id: GuildId) -> Result<()> { // Free the ring before tearing down the Call — the handler's `Weak` // will then no-op any in-flight ticks until songbird drops it. - self.stop_recording().await; + self.stop_recording(guild_id).await; self.songbird .remove(guild_id) .await -- cgit v1.2.3