//! Device selection, quality presets, streaming. use std::sync::Arc; use tokio::sync::{Mutex, RwLock}; use tracing::{debug, info}; use super::capture::AudioConfig; use super::device::{ enumerate_audio_devices_with_current, find_best_audio_device, AudioDeviceInfo, }; use super::encoder::OpusFrame; use super::monitor::AudioHealthMonitor; use super::recovery; use super::streamer::{AudioStreamer, AudioStreamerConfig}; use super::types::{AudioControllerConfig, AudioQuality, AudioStatus}; use crate::error::{AppError, Result}; use crate::events::EventBus; pub(super) type AudioRecoveredCallback = Arc; pub struct AudioController { config: Arc>, streamer: Arc>>>, event_bus: Arc>>>, monitor: Arc, recovery: recovery::AudioRecovery, recovered_callback: Arc>>, operation: Arc>, } impl AudioController { pub fn new(config: AudioControllerConfig) -> Self { let config = Arc::new(RwLock::new(config)); let streamer = Arc::new(RwLock::new(None)); let event_bus = Arc::new(RwLock::new(None)); let monitor = Arc::new(AudioHealthMonitor::new()); let recovered_callback = Arc::new(RwLock::new(None)); let operation = Arc::new(Mutex::new(())); let recovery = recovery::AudioRecovery::new( config.clone(), streamer.clone(), event_bus.clone(), monitor.clone(), recovered_callback.clone(), operation.clone(), ); Self { config, streamer, event_bus, monitor, recovery, recovered_callback, operation, } } pub async fn set_event_bus(&self, event_bus: Arc) { *self.event_bus.write().await = Some(event_bus); } pub async fn set_recovered_callback(&self, callback: Arc) { *self.recovered_callback.write().await = Some(callback); } async fn mark_device_info_dirty(&self) { if let Some(bus) = self.event_bus.read().await.as_ref() { bus.mark_device_info_dirty(); } } pub async fn list_devices(&self) -> Result> { let current_device = if self.is_streaming().await { Some(self.config.read().await.device.clone()) } else { None }; enumerate_audio_devices_with_current(current_device.as_deref()) } pub async fn select_device(&self, device: &str) -> Result<()> { let _operation = self.operation.lock().await; self.recovery.cancel(); let devices = self.list_devices().await?; let found = devices .iter() .any(|d| d.name == device || d.description.contains(device)); if !found { return Err(AppError::NotFound(format!("audio device {device}"))); } { let mut config = self.config.write().await; config.device = device.to_string(); } info!("Audio device selected: {}", device); if self.is_streaming().await { self.stop_streaming_inner().await?; self.start_streaming_inner().await?; } Ok(()) } pub async fn set_quality(&self, quality: AudioQuality) -> Result<()> { let _operation = self.operation.lock().await; { let mut config = self.config.write().await; config.quality = quality; } if let Some(streamer) = self.streamer.read().await.as_ref() { streamer.set_bitrate(quality.bitrate()).await?; } info!( "Audio quality set to: {:?} ({}bps)", quality, quality.bitrate() ); Ok(()) } pub async fn start_streaming(&self) -> Result<()> { let _operation = self.operation.lock().await; self.recovery.cancel(); self.start_streaming_inner().await } async fn start_streaming_inner(&self) -> Result<()> { { let config = self.config.read().await; if !config.enabled { return Err(AppError::AudioError("Audio is disabled".to_string())); } } if self.is_streaming().await { return Ok(()); } let mut select_error = None; let (device_name, quality) = { let mut cfg = self.config.write().await; if cfg.device.trim().is_empty() { match find_best_audio_device() { Ok(best) => cfg.device = best.name, Err(e) => { select_error = Some(format!("Failed to select audio device: {}", e)); } } } (cfg.device.clone(), cfg.quality) }; if let Some(error_msg) = select_error { self.monitor.report_error(&error_msg, "start_failed").await; self.recovery.start("auto".to_string(), error_msg.clone()); self.mark_device_info_dirty().await; return Err(AppError::AudioError(error_msg)); } debug!("Starting audio streaming with device: {}", device_name); self.monitor.prepare_retry_attempt(); let streamer_config = AudioStreamerConfig { capture: AudioConfig { device_name: device_name.clone(), ..Default::default() }, opus: quality.to_opus_config(), }; let streamer = Arc::new(AudioStreamer::with_config(streamer_config)); if let Err(e) = streamer.start().await { let error_msg = format!("Failed to start audio: {}", e); self.monitor.report_error(&error_msg, "start_failed").await; self.recovery.start(device_name.clone(), error_msg.clone()); self.mark_device_info_dirty().await; return Err(AppError::AudioError(error_msg)); } let streamer_for_monitor = streamer.clone(); *self.streamer.write().await = Some(streamer); self.recovery .monitor(streamer_for_monitor, device_name.clone()); if self.monitor.is_error().await { self.monitor.report_recovered().await; } self.mark_device_info_dirty().await; info!("Audio streaming started"); Ok(()) } pub async fn stop_streaming(&self) -> Result<()> { let _operation = self.operation.lock().await; self.stop_streaming_inner().await } async fn stop_streaming_inner(&self) -> Result<()> { self.recovery.cancel(); if let Some(streamer) = self.streamer.write().await.take() { streamer.stop().await?; } self.monitor.reset().await; self.mark_device_info_dirty().await; info!("Audio streaming stopped"); Ok(()) } pub async fn is_streaming(&self) -> bool { self.streamer .read() .await .as_ref() .is_some_and(|streamer| streamer.is_running()) } pub async fn status(&self) -> AudioStatus { let (enabled, device_str, quality) = { let c = self.config.read().await; (c.enabled, c.device.clone(), c.quality) }; let error = self.monitor.error_message().await; let (streaming, subscriber_count) = if let Some(ref streamer) = *self.streamer.read().await { let streaming = streamer.is_running(); let subscriber_count = streamer.subscriber_count(); (streaming, subscriber_count) } else { (false, 0) }; AudioStatus { enabled, streaming, device: if streaming || enabled { Some(device_str) } else { None }, quality, subscriber_count, error, } } pub async fn subscribe_opus(&self) -> Option>> { self.streamer .read() .await .as_ref() .map(|s| s.subscribe_opus()) } pub async fn set_enabled(&self, enabled: bool) -> Result<()> { let _operation = self.operation.lock().await; self.recovery.cancel(); { let mut config = self.config.write().await; config.enabled = enabled; } if !enabled && self.is_streaming().await { self.stop_streaming_inner().await?; } info!("Audio enabled: {}", enabled); Ok(()) } pub async fn update_config(&self, new_config: AudioControllerConfig) -> Result<()> { let _operation = self.operation.lock().await; self.recovery.cancel(); let was_streaming = self.is_streaming().await; if was_streaming { self.stop_streaming_inner().await?; } *self.config.write().await = new_config.clone(); if new_config.enabled { self.start_streaming_inner().await?; } Ok(()) } pub async fn shutdown(&self) -> Result<()> { self.stop_streaming().await } } impl Default for AudioController { fn default() -> Self { Self::new(AudioControllerConfig::default()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_audio_quality_bitrate() { assert_eq!(AudioQuality::Voice.bitrate(), 32000); assert_eq!(AudioQuality::Balanced.bitrate(), 64000); assert_eq!(AudioQuality::High.bitrate(), 128000); } #[test] fn test_audio_quality_from_str() { assert_eq!( "voice".parse::().unwrap(), AudioQuality::Voice ); assert_eq!( "balanced".parse::().unwrap(), AudioQuality::Balanced ); assert_eq!("high".parse::().unwrap(), AudioQuality::High); } #[test] fn test_audio_quality_from_str_rejects_aliases_and_unknown() { assert!("low".parse::().is_err()); assert!("music".parse::().is_err()); assert!("unknown".parse::().is_err()); assert!("".parse::().is_err()); } #[tokio::test] async fn test_controller_default() { let controller = AudioController::default(); let status = controller.status().await; assert!(!status.enabled); assert!(!status.streaming); } }