mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-20 09:56:41 +08:00
init
This commit is contained in:
390
src/audio/capture.rs
Normal file
390
src/audio/capture.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
//! ALSA audio capture implementation
|
||||
|
||||
use alsa::pcm::{Access, Format, Frames, HwParams, State, IO};
|
||||
use alsa::{Direction, ValueOr, PCM};
|
||||
use bytes::Bytes;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::{broadcast, watch, Mutex};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::device::AudioDeviceInfo;
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Audio capture configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioConfig {
|
||||
/// ALSA device name (e.g., "hw:0,0" or "default")
|
||||
pub device_name: String,
|
||||
/// Sample rate in Hz
|
||||
pub sample_rate: u32,
|
||||
/// Number of channels (1 = mono, 2 = stereo)
|
||||
pub channels: u32,
|
||||
/// Samples per frame (for Opus, typically 480 for 10ms at 48kHz)
|
||||
pub frame_size: u32,
|
||||
/// Buffer size in frames
|
||||
pub buffer_frames: u32,
|
||||
/// Period size in frames
|
||||
pub period_frames: u32,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_name: "default".to_string(),
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
frame_size: 960, // 20ms at 48kHz (good for Opus)
|
||||
buffer_frames: 4096,
|
||||
period_frames: 960,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioConfig {
|
||||
/// Create config for a specific device
|
||||
pub fn for_device(device: &AudioDeviceInfo) -> Self {
|
||||
let sample_rate = if device.sample_rates.contains(&48000) {
|
||||
48000
|
||||
} else {
|
||||
*device.sample_rates.first().unwrap_or(&48000)
|
||||
};
|
||||
|
||||
let channels = if device.channels.contains(&2) {
|
||||
2
|
||||
} else {
|
||||
*device.channels.first().unwrap_or(&2)
|
||||
};
|
||||
|
||||
Self {
|
||||
device_name: device.name.clone(),
|
||||
sample_rate,
|
||||
channels,
|
||||
frame_size: sample_rate / 50, // 20ms
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes per sample (16-bit signed)
|
||||
pub fn bytes_per_sample(&self) -> u32 {
|
||||
2 * self.channels
|
||||
}
|
||||
|
||||
/// Bytes per frame
|
||||
pub fn bytes_per_frame(&self) -> usize {
|
||||
(self.frame_size * self.bytes_per_sample()) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio frame data
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioFrame {
|
||||
/// Raw PCM data (S16LE interleaved)
|
||||
pub data: Bytes,
|
||||
/// Sample rate
|
||||
pub sample_rate: u32,
|
||||
/// Number of channels
|
||||
pub channels: u32,
|
||||
/// Number of samples per channel
|
||||
pub samples: u32,
|
||||
/// Frame sequence number
|
||||
pub sequence: u64,
|
||||
/// Capture timestamp
|
||||
pub timestamp: Instant,
|
||||
}
|
||||
|
||||
impl AudioFrame {
|
||||
pub fn new(data: Bytes, config: &AudioConfig, sequence: u64) -> Self {
|
||||
Self {
|
||||
samples: data.len() as u32 / config.bytes_per_sample(),
|
||||
data,
|
||||
sample_rate: config.sample_rate,
|
||||
channels: config.channels,
|
||||
sequence,
|
||||
timestamp: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio capture state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CaptureState {
|
||||
Stopped,
|
||||
Running,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Audio capture statistics
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AudioStats {
|
||||
pub frames_captured: u64,
|
||||
pub frames_dropped: u64,
|
||||
pub buffer_overruns: u64,
|
||||
pub current_latency_ms: f32,
|
||||
}
|
||||
|
||||
/// ALSA audio capturer
|
||||
pub struct AudioCapturer {
|
||||
config: AudioConfig,
|
||||
state: Arc<watch::Sender<CaptureState>>,
|
||||
state_rx: watch::Receiver<CaptureState>,
|
||||
stats: Arc<Mutex<AudioStats>>,
|
||||
frame_tx: broadcast::Sender<AudioFrame>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
sequence: Arc<AtomicU64>,
|
||||
capture_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl AudioCapturer {
|
||||
/// Create a new audio capturer
|
||||
pub fn new(config: AudioConfig) -> Self {
|
||||
let (state_tx, state_rx) = watch::channel(CaptureState::Stopped);
|
||||
let (frame_tx, _) = broadcast::channel(32);
|
||||
|
||||
Self {
|
||||
config,
|
||||
state: Arc::new(state_tx),
|
||||
state_rx,
|
||||
stats: Arc::new(Mutex::new(AudioStats::default())),
|
||||
frame_tx,
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
sequence: Arc::new(AtomicU64::new(0)),
|
||||
capture_handle: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current state
|
||||
pub fn state(&self) -> CaptureState {
|
||||
*self.state_rx.borrow()
|
||||
}
|
||||
|
||||
/// Subscribe to state changes
|
||||
pub fn state_watch(&self) -> watch::Receiver<CaptureState> {
|
||||
self.state_rx.clone()
|
||||
}
|
||||
|
||||
/// Subscribe to audio frames
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame> {
|
||||
self.frame_tx.subscribe()
|
||||
}
|
||||
|
||||
/// Get statistics
|
||||
pub async fn stats(&self) -> AudioStats {
|
||||
self.stats.lock().await.clone()
|
||||
}
|
||||
|
||||
/// Start capturing
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
if self.state() == CaptureState::Running {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"Starting audio capture on {} at {}Hz {}ch",
|
||||
self.config.device_name, self.config.sample_rate, self.config.channels
|
||||
);
|
||||
|
||||
self.stop_flag.store(false, Ordering::SeqCst);
|
||||
|
||||
let config = self.config.clone();
|
||||
let state = self.state.clone();
|
||||
let stats = self.stats.clone();
|
||||
let frame_tx = self.frame_tx.clone();
|
||||
let stop_flag = self.stop_flag.clone();
|
||||
let sequence = self.sequence.clone();
|
||||
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
capture_loop(config, state, stats, frame_tx, stop_flag, sequence);
|
||||
});
|
||||
|
||||
*self.capture_handle.lock().await = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop capturing
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
info!("Stopping audio capture");
|
||||
self.stop_flag.store(true, Ordering::SeqCst);
|
||||
|
||||
if let Some(handle) = self.capture_handle.lock().await.take() {
|
||||
let _ = handle.await;
|
||||
}
|
||||
|
||||
let _ = self.state.send(CaptureState::Stopped);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if running
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.state() == CaptureState::Running
|
||||
}
|
||||
}
|
||||
|
||||
/// Main capture loop
|
||||
fn capture_loop(
|
||||
config: AudioConfig,
|
||||
state: Arc<watch::Sender<CaptureState>>,
|
||||
stats: Arc<Mutex<AudioStats>>,
|
||||
frame_tx: broadcast::Sender<AudioFrame>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
sequence: Arc<AtomicU64>,
|
||||
) {
|
||||
let result = run_capture(&config, &state, &stats, &frame_tx, &stop_flag, &sequence);
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Audio capture error: {}", e);
|
||||
let _ = state.send(CaptureState::Error);
|
||||
} else {
|
||||
let _ = state.send(CaptureState::Stopped);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_capture(
|
||||
config: &AudioConfig,
|
||||
state: &watch::Sender<CaptureState>,
|
||||
stats: &Arc<Mutex<AudioStats>>,
|
||||
frame_tx: &broadcast::Sender<AudioFrame>,
|
||||
stop_flag: &AtomicBool,
|
||||
sequence: &AtomicU64,
|
||||
) -> Result<()> {
|
||||
// Open ALSA device
|
||||
let pcm = PCM::new(&config.device_name, Direction::Capture, false).map_err(|e| {
|
||||
AppError::AudioError(format!(
|
||||
"Failed to open audio device {}: {}",
|
||||
config.device_name, e
|
||||
))
|
||||
})?;
|
||||
|
||||
// Configure hardware parameters
|
||||
{
|
||||
let hwp = HwParams::any(&pcm).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to get HwParams: {}", e))
|
||||
})?;
|
||||
|
||||
hwp.set_channels(config.channels).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to set channels: {}", e))
|
||||
})?;
|
||||
|
||||
hwp.set_rate(config.sample_rate, ValueOr::Nearest).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to set sample rate: {}", e))
|
||||
})?;
|
||||
|
||||
hwp.set_format(Format::s16()).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to set format: {}", e))
|
||||
})?;
|
||||
|
||||
hwp.set_access(Access::RWInterleaved).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to set access: {}", e))
|
||||
})?;
|
||||
|
||||
hwp.set_buffer_size_near(config.buffer_frames as Frames).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to set buffer size: {}", e))
|
||||
})?;
|
||||
|
||||
hwp.set_period_size_near(config.period_frames as Frames, ValueOr::Nearest)
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to set period size: {}", e)))?;
|
||||
|
||||
pcm.hw_params(&hwp).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to apply hw params: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Get actual configuration
|
||||
let actual_rate = pcm.hw_params_current()
|
||||
.map(|h| h.get_rate().unwrap_or(config.sample_rate))
|
||||
.unwrap_or(config.sample_rate);
|
||||
|
||||
info!(
|
||||
"Audio capture configured: {}Hz {}ch (requested {}Hz)",
|
||||
actual_rate, config.channels, config.sample_rate
|
||||
);
|
||||
|
||||
// Prepare for capture
|
||||
pcm.prepare().map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to prepare PCM: {}", e))
|
||||
})?;
|
||||
|
||||
let _ = state.send(CaptureState::Running);
|
||||
|
||||
// Allocate buffer - use u8 directly for zero-copy
|
||||
let frame_bytes = config.bytes_per_frame();
|
||||
let mut buffer = vec![0u8; frame_bytes];
|
||||
|
||||
// Capture loop
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
// Check PCM state
|
||||
match pcm.state() {
|
||||
State::XRun => {
|
||||
warn!("Audio buffer overrun, recovering");
|
||||
if let Ok(mut s) = stats.try_lock() {
|
||||
s.buffer_overruns += 1;
|
||||
}
|
||||
let _ = pcm.prepare();
|
||||
continue;
|
||||
}
|
||||
State::Suspended => {
|
||||
warn!("Audio device suspended, recovering");
|
||||
let _ = pcm.resume();
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Get IO handle and read audio data directly as bytes
|
||||
// Note: Use io() instead of io_checked() because USB audio devices
|
||||
// typically don't support mmap, which io_checked() requires
|
||||
let io: IO<u8> = pcm.io_bytes();
|
||||
|
||||
match io.readi(&mut buffer) {
|
||||
Ok(frames_read) => {
|
||||
if frames_read == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate actual byte count
|
||||
let byte_count = frames_read * config.channels as usize * 2;
|
||||
|
||||
// Directly use the buffer slice (already in correct byte format)
|
||||
let seq = sequence.fetch_add(1, Ordering::Relaxed);
|
||||
let frame = AudioFrame::new(
|
||||
Bytes::copy_from_slice(&buffer[..byte_count]),
|
||||
config,
|
||||
seq,
|
||||
);
|
||||
|
||||
// Send to subscribers
|
||||
if frame_tx.receiver_count() > 0 {
|
||||
if let Err(e) = frame_tx.send(frame) {
|
||||
debug!("No audio receivers: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Update stats
|
||||
if let Ok(mut s) = stats.try_lock() {
|
||||
s.frames_captured += 1;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Check for buffer overrun (EPIPE = 32 on Linux)
|
||||
let desc = e.to_string();
|
||||
if desc.contains("EPIPE") || desc.contains("Broken pipe") {
|
||||
// Buffer overrun
|
||||
warn!("Audio buffer overrun");
|
||||
if let Ok(mut s) = stats.try_lock() {
|
||||
s.buffer_overruns += 1;
|
||||
}
|
||||
let _ = pcm.prepare();
|
||||
} else {
|
||||
error!("Audio read error: {}", e);
|
||||
if let Ok(mut s) = stats.try_lock() {
|
||||
s.frames_dropped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Audio capture stopped");
|
||||
Ok(())
|
||||
}
|
||||
495
src/audio/controller.rs
Normal file
495
src/audio/controller.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
//! Audio controller for high-level audio management
|
||||
//!
|
||||
//! Provides device enumeration, selection, quality control, and streaming management.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
use tracing::info;
|
||||
|
||||
use super::capture::AudioConfig;
|
||||
use super::device::{enumerate_audio_devices_with_current, AudioDeviceInfo};
|
||||
use super::encoder::{OpusConfig, OpusFrame};
|
||||
use super::monitor::{AudioHealthMonitor, AudioHealthStatus};
|
||||
use super::streamer::{AudioStreamer, AudioStreamerConfig};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::{EventBus, SystemEvent};
|
||||
|
||||
/// Audio quality presets
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AudioQuality {
|
||||
/// Low bandwidth voice (32kbps)
|
||||
Voice,
|
||||
/// Balanced quality (64kbps) - default
|
||||
#[default]
|
||||
Balanced,
|
||||
/// High quality audio (128kbps)
|
||||
High,
|
||||
}
|
||||
|
||||
impl AudioQuality {
|
||||
/// Get the bitrate for this quality level
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
match self {
|
||||
AudioQuality::Voice => 32000,
|
||||
AudioQuality::Balanced => 64000,
|
||||
AudioQuality::High => 128000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse from string
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"voice" | "low" => AudioQuality::Voice,
|
||||
"high" | "music" => AudioQuality::High,
|
||||
_ => AudioQuality::Balanced,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to OpusConfig
|
||||
pub fn to_opus_config(&self) -> OpusConfig {
|
||||
match self {
|
||||
AudioQuality::Voice => OpusConfig::voice(),
|
||||
AudioQuality::Balanced => OpusConfig::default(),
|
||||
AudioQuality::High => OpusConfig::music(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AudioQuality {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AudioQuality::Voice => write!(f, "voice"),
|
||||
AudioQuality::Balanced => write!(f, "balanced"),
|
||||
AudioQuality::High => write!(f, "high"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio controller configuration
|
||||
///
|
||||
/// Note: Sample rate is fixed at 48000Hz and channels at 2 (stereo).
|
||||
/// These are optimal for Opus encoding and match WebRTC requirements.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioControllerConfig {
|
||||
/// Whether audio is enabled
|
||||
pub enabled: bool,
|
||||
/// Selected device name
|
||||
pub device: String,
|
||||
/// Audio quality preset
|
||||
pub quality: AudioQuality,
|
||||
}
|
||||
|
||||
impl Default for AudioControllerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
device: "default".to_string(),
|
||||
quality: AudioQuality::Balanced,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Current audio status
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AudioStatus {
|
||||
/// Whether audio feature is enabled
|
||||
pub enabled: bool,
|
||||
/// Whether audio is currently streaming
|
||||
pub streaming: bool,
|
||||
/// Currently selected device
|
||||
pub device: Option<String>,
|
||||
/// Current quality preset
|
||||
pub quality: AudioQuality,
|
||||
/// Number of connected subscribers
|
||||
pub subscriber_count: usize,
|
||||
/// Frames encoded
|
||||
pub frames_encoded: u64,
|
||||
/// Bytes output
|
||||
pub bytes_output: u64,
|
||||
/// Error message if any
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Audio controller
|
||||
///
|
||||
/// High-level interface for audio management, providing:
|
||||
/// - Device enumeration and selection
|
||||
/// - Quality control
|
||||
/// - Stream start/stop
|
||||
/// - Status reporting
|
||||
pub struct AudioController {
|
||||
config: RwLock<AudioControllerConfig>,
|
||||
streamer: RwLock<Option<Arc<AudioStreamer>>>,
|
||||
devices: RwLock<Vec<AudioDeviceInfo>>,
|
||||
event_bus: RwLock<Option<Arc<EventBus>>>,
|
||||
last_error: RwLock<Option<String>>,
|
||||
/// Health monitor for error tracking and recovery
|
||||
monitor: Arc<AudioHealthMonitor>,
|
||||
}
|
||||
|
||||
impl AudioController {
|
||||
/// Create a new audio controller with configuration
|
||||
pub fn new(config: AudioControllerConfig) -> Self {
|
||||
Self {
|
||||
config: RwLock::new(config),
|
||||
streamer: RwLock::new(None),
|
||||
devices: RwLock::new(Vec::new()),
|
||||
event_bus: RwLock::new(None),
|
||||
last_error: RwLock::new(None),
|
||||
monitor: Arc::new(AudioHealthMonitor::with_defaults()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set event bus for publishing audio events
|
||||
pub async fn set_event_bus(&self, event_bus: Arc<EventBus>) {
|
||||
*self.event_bus.write().await = Some(event_bus.clone());
|
||||
// Also set event bus on the monitor for health notifications
|
||||
self.monitor.set_event_bus(event_bus).await;
|
||||
}
|
||||
|
||||
/// Publish an event to the event bus
|
||||
async fn publish_event(&self, event: SystemEvent) {
|
||||
if let Some(ref bus) = *self.event_bus.read().await {
|
||||
bus.publish(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// List available audio capture devices
|
||||
pub async fn list_devices(&self) -> Result<Vec<AudioDeviceInfo>> {
|
||||
// Get current device if streaming (it may be busy and unable to be opened)
|
||||
let current_device = if self.is_streaming().await {
|
||||
Some(self.config.read().await.device.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let devices = enumerate_audio_devices_with_current(current_device.as_deref())?;
|
||||
*self.devices.write().await = devices.clone();
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
/// Refresh device list and cache it
|
||||
pub async fn refresh_devices(&self) -> Result<()> {
|
||||
// Get current device if streaming (it may be busy and unable to be opened)
|
||||
let current_device = if self.is_streaming().await {
|
||||
Some(self.config.read().await.device.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let devices = enumerate_audio_devices_with_current(current_device.as_deref())?;
|
||||
*self.devices.write().await = devices;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get cached device list
|
||||
pub async fn get_cached_devices(&self) -> Vec<AudioDeviceInfo> {
|
||||
self.devices.read().await.clone()
|
||||
}
|
||||
|
||||
/// Select audio device
|
||||
pub async fn select_device(&self, device: &str) -> Result<()> {
|
||||
// Validate device exists
|
||||
let devices = self.list_devices().await?;
|
||||
let found = devices.iter().any(|d| d.name == device || d.description.contains(device));
|
||||
|
||||
if !found && device != "default" {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Audio device not found: {}",
|
||||
device
|
||||
)));
|
||||
}
|
||||
|
||||
// Update config
|
||||
{
|
||||
let mut config = self.config.write().await;
|
||||
config.device = device.to_string();
|
||||
}
|
||||
|
||||
// Publish event
|
||||
self.publish_event(SystemEvent::AudioDeviceSelected {
|
||||
device: device.to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
info!("Audio device selected: {}", device);
|
||||
|
||||
// If streaming, restart with new device
|
||||
if self.is_streaming().await {
|
||||
self.stop_streaming().await?;
|
||||
self.start_streaming().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set audio quality
|
||||
pub async fn set_quality(&self, quality: AudioQuality) -> Result<()> {
|
||||
// Update config
|
||||
{
|
||||
let mut config = self.config.write().await;
|
||||
config.quality = quality;
|
||||
}
|
||||
|
||||
// Update streamer if running
|
||||
if let Some(ref streamer) = *self.streamer.read().await {
|
||||
streamer.set_bitrate(quality.bitrate()).await?;
|
||||
}
|
||||
|
||||
// Publish event
|
||||
self.publish_event(SystemEvent::AudioQualityChanged {
|
||||
quality: quality.to_string(),
|
||||
})
|
||||
.await;
|
||||
|
||||
info!("Audio quality set to: {:?} ({}bps)", quality, quality.bitrate());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start audio streaming
|
||||
pub async fn start_streaming(&self) -> Result<()> {
|
||||
let config = self.config.read().await.clone();
|
||||
|
||||
if !config.enabled {
|
||||
return Err(AppError::AudioError("Audio is disabled".to_string()));
|
||||
}
|
||||
|
||||
// Check if already streaming
|
||||
if self.is_streaming().await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Starting audio streaming with device: {}", config.device);
|
||||
|
||||
// Clear any previous error
|
||||
*self.last_error.write().await = None;
|
||||
|
||||
// Create streamer config (fixed 48kHz stereo)
|
||||
let streamer_config = AudioStreamerConfig {
|
||||
capture: AudioConfig {
|
||||
device_name: config.device.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
opus: config.quality.to_opus_config(),
|
||||
};
|
||||
|
||||
// Create and start streamer
|
||||
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.last_error.write().await = Some(error_msg.clone());
|
||||
|
||||
// Report error to health monitor
|
||||
self.monitor
|
||||
.report_error(Some(&config.device), &error_msg, "start_failed")
|
||||
.await;
|
||||
|
||||
self.publish_event(SystemEvent::AudioStateChanged {
|
||||
streaming: false,
|
||||
device: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
return Err(AppError::AudioError(error_msg));
|
||||
}
|
||||
|
||||
*self.streamer.write().await = Some(streamer);
|
||||
|
||||
// Report recovery if we were in an error state
|
||||
if self.monitor.is_error().await {
|
||||
self.monitor.report_recovered(Some(&config.device)).await;
|
||||
}
|
||||
|
||||
// Publish event
|
||||
self.publish_event(SystemEvent::AudioStateChanged {
|
||||
streaming: true,
|
||||
device: Some(config.device),
|
||||
})
|
||||
.await;
|
||||
|
||||
info!("Audio streaming started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop audio streaming
|
||||
pub async fn stop_streaming(&self) -> Result<()> {
|
||||
if let Some(streamer) = self.streamer.write().await.take() {
|
||||
streamer.stop().await?;
|
||||
}
|
||||
|
||||
// Publish event
|
||||
self.publish_event(SystemEvent::AudioStateChanged {
|
||||
streaming: false,
|
||||
device: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
info!("Audio streaming stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if currently streaming
|
||||
pub async fn is_streaming(&self) -> bool {
|
||||
if let Some(ref streamer) = *self.streamer.read().await {
|
||||
streamer.is_running()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current status
|
||||
pub async fn status(&self) -> AudioStatus {
|
||||
let config = self.config.read().await;
|
||||
let streaming = self.is_streaming().await;
|
||||
let error = self.last_error.read().await.clone();
|
||||
|
||||
let (subscriber_count, frames_encoded, bytes_output) = if let Some(ref streamer) =
|
||||
*self.streamer.read().await
|
||||
{
|
||||
let stats = streamer.stats().await;
|
||||
(stats.subscriber_count, stats.frames_encoded, stats.bytes_output)
|
||||
} else {
|
||||
(0, 0, 0)
|
||||
};
|
||||
|
||||
AudioStatus {
|
||||
enabled: config.enabled,
|
||||
streaming,
|
||||
device: if streaming || config.enabled {
|
||||
Some(config.device.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
quality: config.quality,
|
||||
subscriber_count,
|
||||
frames_encoded,
|
||||
bytes_output,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to Opus frames (for WebSocket clients)
|
||||
pub fn subscribe_opus(&self) -> Option<broadcast::Receiver<OpusFrame>> {
|
||||
// Use try_read to avoid blocking - this is called from sync context sometimes
|
||||
if let Ok(guard) = self.streamer.try_read() {
|
||||
guard.as_ref().map(|s| s.subscribe_opus())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to Opus frames (async version)
|
||||
pub async fn subscribe_opus_async(&self) -> Option<broadcast::Receiver<OpusFrame>> {
|
||||
self.streamer.read().await.as_ref().map(|s| s.subscribe_opus())
|
||||
}
|
||||
|
||||
/// Enable or disable audio
|
||||
pub async fn set_enabled(&self, enabled: bool) -> Result<()> {
|
||||
{
|
||||
let mut config = self.config.write().await;
|
||||
config.enabled = enabled;
|
||||
}
|
||||
|
||||
if !enabled && self.is_streaming().await {
|
||||
self.stop_streaming().await?;
|
||||
}
|
||||
|
||||
info!("Audio enabled: {}", enabled);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update full configuration
|
||||
pub async fn update_config(&self, new_config: AudioControllerConfig) -> Result<()> {
|
||||
let was_streaming = self.is_streaming().await;
|
||||
let old_config = self.config.read().await.clone();
|
||||
|
||||
// Stop streaming if running
|
||||
if was_streaming {
|
||||
self.stop_streaming().await?;
|
||||
}
|
||||
|
||||
// Update config
|
||||
*self.config.write().await = new_config.clone();
|
||||
|
||||
// Restart streaming if it was running and still enabled
|
||||
if was_streaming && new_config.enabled {
|
||||
self.start_streaming().await?;
|
||||
}
|
||||
|
||||
// Publish events for changes
|
||||
if old_config.device != new_config.device {
|
||||
self.publish_event(SystemEvent::AudioDeviceSelected {
|
||||
device: new_config.device.clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
if old_config.quality != new_config.quality {
|
||||
self.publish_event(SystemEvent::AudioQualityChanged {
|
||||
quality: new_config.quality.to_string(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown the controller
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
self.stop_streaming().await
|
||||
}
|
||||
|
||||
/// Get the health monitor reference
|
||||
pub fn monitor(&self) -> &Arc<AudioHealthMonitor> {
|
||||
&self.monitor
|
||||
}
|
||||
|
||||
/// Get current health status
|
||||
pub async fn health_status(&self) -> AudioHealthStatus {
|
||||
self.monitor.status().await
|
||||
}
|
||||
|
||||
/// Check if the audio is healthy
|
||||
pub async fn is_healthy(&self) -> bool {
|
||||
self.monitor.is_healthy().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!(AudioQuality::from_str("voice"), AudioQuality::Voice);
|
||||
assert_eq!(AudioQuality::from_str("low"), AudioQuality::Voice);
|
||||
assert_eq!(AudioQuality::from_str("balanced"), AudioQuality::Balanced);
|
||||
assert_eq!(AudioQuality::from_str("high"), AudioQuality::High);
|
||||
assert_eq!(AudioQuality::from_str("music"), AudioQuality::High);
|
||||
assert_eq!(AudioQuality::from_str("unknown"), AudioQuality::Balanced);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_controller_default() {
|
||||
let controller = AudioController::default();
|
||||
let status = controller.status().await;
|
||||
assert!(!status.enabled);
|
||||
assert!(!status.streaming);
|
||||
}
|
||||
}
|
||||
234
src/audio/device.rs
Normal file
234
src/audio/device.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
//! Audio device enumeration using ALSA
|
||||
|
||||
use alsa::pcm::HwParams;
|
||||
use alsa::{Direction, PCM};
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Audio device information
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AudioDeviceInfo {
|
||||
/// Device name (e.g., "hw:0,0" or "default")
|
||||
pub name: String,
|
||||
/// Human-readable description
|
||||
pub description: String,
|
||||
/// Card index
|
||||
pub card_index: i32,
|
||||
/// Device index
|
||||
pub device_index: i32,
|
||||
/// Supported sample rates
|
||||
pub sample_rates: Vec<u32>,
|
||||
/// Supported channel counts
|
||||
pub channels: Vec<u32>,
|
||||
/// Is this a capture device
|
||||
pub is_capture: bool,
|
||||
/// Is this an HDMI audio device (likely from capture card)
|
||||
pub is_hdmi: bool,
|
||||
}
|
||||
|
||||
impl AudioDeviceInfo {
|
||||
/// Get ALSA device name
|
||||
pub fn alsa_name(&self) -> String {
|
||||
format!("hw:{},{}", self.card_index, self.device_index)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate available audio capture devices
|
||||
pub fn enumerate_audio_devices() -> Result<Vec<AudioDeviceInfo>> {
|
||||
enumerate_audio_devices_with_current(None)
|
||||
}
|
||||
|
||||
/// Enumerate available audio capture devices, with option to include a currently-in-use device
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `current_device` - Optional device name that is currently in use. This device will be
|
||||
/// included in the list even if it cannot be opened (because it's already open by us).
|
||||
pub fn enumerate_audio_devices_with_current(
|
||||
current_device: Option<&str>,
|
||||
) -> Result<Vec<AudioDeviceInfo>> {
|
||||
let mut devices = Vec::new();
|
||||
|
||||
// Try to enumerate cards
|
||||
let cards = match alsa::card::Iter::new() {
|
||||
i => i,
|
||||
};
|
||||
|
||||
for card_result in cards {
|
||||
let card = match card_result {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
debug!("Error iterating card: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let card_index = card.get_index();
|
||||
let card_name = card.get_name().unwrap_or_else(|_| "Unknown".to_string());
|
||||
let card_longname = card.get_longname().unwrap_or_else(|_| card_name.clone());
|
||||
|
||||
debug!("Found audio card {}: {}", card_index, card_longname);
|
||||
|
||||
// Check if this looks like an HDMI capture device
|
||||
let is_hdmi = card_longname.to_lowercase().contains("hdmi")
|
||||
|| card_longname.to_lowercase().contains("capture")
|
||||
|| card_longname.to_lowercase().contains("usb");
|
||||
|
||||
// Try to open each device on this card for capture
|
||||
for device_index in 0..8 {
|
||||
let device_name = format!("hw:{},{}", card_index, device_index);
|
||||
|
||||
// Check if this is the currently-in-use device
|
||||
let is_current_device = current_device == Some(device_name.as_str());
|
||||
|
||||
// Try to open for capture
|
||||
match PCM::new(&device_name, Direction::Capture, false) {
|
||||
Ok(pcm) => {
|
||||
// Query capabilities
|
||||
let (sample_rates, channels) = query_device_caps(&pcm);
|
||||
|
||||
if !sample_rates.is_empty() && !channels.is_empty() {
|
||||
devices.push(AudioDeviceInfo {
|
||||
name: device_name,
|
||||
description: format!("{} - Device {}", card_longname, device_index),
|
||||
card_index,
|
||||
device_index,
|
||||
sample_rates,
|
||||
channels,
|
||||
is_capture: true,
|
||||
is_hdmi,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Device doesn't exist or can't be opened for capture
|
||||
// But if it's the current device, include it anyway (it's busy because we're using it)
|
||||
if is_current_device {
|
||||
debug!(
|
||||
"Device {} is busy (in use by us), adding with default caps",
|
||||
device_name
|
||||
);
|
||||
devices.push(AudioDeviceInfo {
|
||||
name: device_name,
|
||||
description: format!(
|
||||
"{} - Device {} (in use)",
|
||||
card_longname, device_index
|
||||
),
|
||||
card_index,
|
||||
device_index,
|
||||
// Use common default capabilities for HDMI capture devices
|
||||
sample_rates: vec![44100, 48000],
|
||||
channels: vec![2],
|
||||
is_capture: true,
|
||||
is_hdmi,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for "default" device
|
||||
if let Ok(pcm) = PCM::new("default", Direction::Capture, false) {
|
||||
let (sample_rates, channels) = query_device_caps(&pcm);
|
||||
if !sample_rates.is_empty() {
|
||||
devices.insert(
|
||||
0,
|
||||
AudioDeviceInfo {
|
||||
name: "default".to_string(),
|
||||
description: "Default Audio Device".to_string(),
|
||||
card_index: -1,
|
||||
device_index: -1,
|
||||
sample_rates,
|
||||
channels,
|
||||
is_capture: true,
|
||||
is_hdmi: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Found {} audio capture devices", devices.len());
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
/// Query device capabilities
|
||||
fn query_device_caps(pcm: &PCM) -> (Vec<u32>, Vec<u32>) {
|
||||
let hwp = match HwParams::any(pcm) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return (vec![], vec![]),
|
||||
};
|
||||
|
||||
// Common sample rates to check
|
||||
let common_rates = [8000, 16000, 22050, 44100, 48000, 96000];
|
||||
let mut supported_rates = Vec::new();
|
||||
|
||||
for rate in &common_rates {
|
||||
if hwp.test_rate(*rate).is_ok() {
|
||||
supported_rates.push(*rate);
|
||||
}
|
||||
}
|
||||
|
||||
// Check channel counts
|
||||
let mut supported_channels = Vec::new();
|
||||
for ch in 1..=8 {
|
||||
if hwp.test_channels(ch).is_ok() {
|
||||
supported_channels.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
(supported_rates, supported_channels)
|
||||
}
|
||||
|
||||
/// Find the best audio device for capture
|
||||
/// Prefers HDMI/capture devices over built-in microphones
|
||||
pub fn find_best_audio_device() -> Result<AudioDeviceInfo> {
|
||||
let devices = enumerate_audio_devices()?;
|
||||
|
||||
if devices.is_empty() {
|
||||
return Err(AppError::AudioError("No audio capture devices found".to_string()));
|
||||
}
|
||||
|
||||
// First, look for HDMI/capture card devices that support 48kHz stereo
|
||||
for device in &devices {
|
||||
if device.is_hdmi
|
||||
&& device.sample_rates.contains(&48000)
|
||||
&& device.channels.contains(&2)
|
||||
{
|
||||
info!("Selected HDMI audio device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Then look for any device supporting 48kHz stereo
|
||||
for device in &devices {
|
||||
if device.sample_rates.contains(&48000) && device.channels.contains(&2) {
|
||||
info!("Selected audio device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to first device
|
||||
let device = devices.into_iter().next().unwrap();
|
||||
warn!(
|
||||
"Using fallback audio device: {} (may not support optimal settings)",
|
||||
device.description
|
||||
);
|
||||
Ok(device)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_enumerate_devices() {
|
||||
// This test may not find devices in CI environment
|
||||
let result = enumerate_audio_devices();
|
||||
println!("Audio devices: {:?}", result);
|
||||
// Just verify it doesn't panic
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
280
src/audio/encoder.rs
Normal file
280
src/audio/encoder.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
//! Opus audio encoder for WebRTC
|
||||
|
||||
use audiopus::coder::GenericCtl;
|
||||
use audiopus::{coder::Encoder, Application, Bitrate, Channels, SampleRate};
|
||||
use bytes::Bytes;
|
||||
use std::time::Instant;
|
||||
use tracing::{info, trace};
|
||||
|
||||
use super::capture::AudioFrame;
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Opus encoder configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OpusConfig {
|
||||
/// Sample rate (must be 8000, 12000, 16000, 24000, or 48000)
|
||||
pub sample_rate: u32,
|
||||
/// Channels (1 or 2)
|
||||
pub channels: u32,
|
||||
/// Target bitrate in bps
|
||||
pub bitrate: u32,
|
||||
/// Application mode
|
||||
pub application: OpusApplication,
|
||||
/// Enable forward error correction
|
||||
pub fec: bool,
|
||||
}
|
||||
|
||||
impl Default for OpusConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bitrate: 64000, // 64 kbps
|
||||
application: OpusApplication::Audio,
|
||||
fec: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpusConfig {
|
||||
/// Create config for voice (lower latency)
|
||||
pub fn voice() -> Self {
|
||||
Self {
|
||||
application: OpusApplication::Voip,
|
||||
bitrate: 32000,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config for music (higher quality)
|
||||
pub fn music() -> Self {
|
||||
Self {
|
||||
application: OpusApplication::Audio,
|
||||
bitrate: 128000,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn to_audiopus_sample_rate(&self) -> SampleRate {
|
||||
match self.sample_rate {
|
||||
8000 => SampleRate::Hz8000,
|
||||
12000 => SampleRate::Hz12000,
|
||||
16000 => SampleRate::Hz16000,
|
||||
24000 => SampleRate::Hz24000,
|
||||
_ => SampleRate::Hz48000,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_audiopus_channels(&self) -> Channels {
|
||||
if self.channels == 1 {
|
||||
Channels::Mono
|
||||
} else {
|
||||
Channels::Stereo
|
||||
}
|
||||
}
|
||||
|
||||
fn to_audiopus_application(&self) -> Application {
|
||||
match self.application {
|
||||
OpusApplication::Voip => Application::Voip,
|
||||
OpusApplication::Audio => Application::Audio,
|
||||
OpusApplication::LowDelay => Application::LowDelay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opus application mode
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OpusApplication {
|
||||
/// Voice over IP
|
||||
Voip,
|
||||
/// General audio
|
||||
Audio,
|
||||
/// Low delay mode
|
||||
LowDelay,
|
||||
}
|
||||
|
||||
/// Encoded Opus frame
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OpusFrame {
|
||||
/// Encoded Opus data
|
||||
pub data: Bytes,
|
||||
/// Duration in milliseconds
|
||||
pub duration_ms: u32,
|
||||
/// Sequence number
|
||||
pub sequence: u64,
|
||||
/// Timestamp
|
||||
pub timestamp: Instant,
|
||||
/// RTP timestamp (samples)
|
||||
pub rtp_timestamp: u32,
|
||||
}
|
||||
|
||||
impl OpusFrame {
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Opus encoder
|
||||
pub struct OpusEncoder {
|
||||
config: OpusConfig,
|
||||
encoder: Encoder,
|
||||
/// Output buffer
|
||||
output_buffer: Vec<u8>,
|
||||
/// Frame counter for RTP timestamp
|
||||
frame_count: u64,
|
||||
/// Samples per frame
|
||||
samples_per_frame: u32,
|
||||
}
|
||||
|
||||
impl OpusEncoder {
|
||||
/// Create a new Opus encoder
|
||||
pub fn new(config: OpusConfig) -> Result<Self> {
|
||||
let sample_rate = config.to_audiopus_sample_rate();
|
||||
let channels = config.to_audiopus_channels();
|
||||
let application = config.to_audiopus_application();
|
||||
|
||||
let mut encoder = Encoder::new(sample_rate, channels, application).map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to create Opus encoder: {:?}", e))
|
||||
})?;
|
||||
|
||||
// Configure encoder
|
||||
encoder
|
||||
.set_bitrate(Bitrate::BitsPerSecond(config.bitrate as i32))
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to set bitrate: {:?}", e)))?;
|
||||
|
||||
if config.fec {
|
||||
encoder
|
||||
.set_inband_fec(true)
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to enable FEC: {:?}", e)))?;
|
||||
}
|
||||
|
||||
// Calculate samples per frame (20ms at sample_rate)
|
||||
let samples_per_frame = config.sample_rate / 50;
|
||||
|
||||
info!(
|
||||
"Opus encoder created: {}Hz {}ch {}bps",
|
||||
config.sample_rate, config.channels, config.bitrate
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
encoder,
|
||||
output_buffer: vec![0u8; 4000], // Max Opus frame size
|
||||
frame_count: 0,
|
||||
samples_per_frame,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create with default configuration
|
||||
pub fn default_config() -> Result<Self> {
|
||||
Self::new(OpusConfig::default())
|
||||
}
|
||||
|
||||
/// Encode PCM audio data (S16LE interleaved)
|
||||
pub fn encode(&mut self, pcm_data: &[i16]) -> Result<OpusFrame> {
|
||||
let encoded_len = self
|
||||
.encoder
|
||||
.encode(pcm_data, &mut self.output_buffer)
|
||||
.map_err(|e| AppError::AudioError(format!("Opus encode failed: {:?}", e)))?;
|
||||
|
||||
let samples = pcm_data.len() as u32 / self.config.channels;
|
||||
let duration_ms = (samples * 1000) / self.config.sample_rate;
|
||||
let rtp_timestamp = (self.frame_count * self.samples_per_frame as u64) as u32;
|
||||
|
||||
self.frame_count += 1;
|
||||
|
||||
trace!(
|
||||
"Encoded {} samples to {} bytes Opus",
|
||||
pcm_data.len(),
|
||||
encoded_len
|
||||
);
|
||||
|
||||
Ok(OpusFrame {
|
||||
data: Bytes::copy_from_slice(&self.output_buffer[..encoded_len]),
|
||||
duration_ms,
|
||||
sequence: self.frame_count - 1,
|
||||
timestamp: Instant::now(),
|
||||
rtp_timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
/// Encode from AudioFrame
|
||||
///
|
||||
/// Uses zero-copy conversion from bytes to i16 samples via bytemuck.
|
||||
pub fn encode_frame(&mut self, frame: &AudioFrame) -> Result<OpusFrame> {
|
||||
// Zero-copy: directly cast bytes to i16 slice
|
||||
// AudioFrame.data is S16LE format, which matches native little-endian i16
|
||||
let samples: &[i16] = bytemuck::cast_slice(&frame.data);
|
||||
self.encode(samples)
|
||||
}
|
||||
|
||||
/// Get encoder configuration
|
||||
pub fn config(&self) -> &OpusConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Reset encoder state
|
||||
pub fn reset(&mut self) -> Result<()> {
|
||||
self.encoder
|
||||
.reset_state()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to reset encoder: {:?}", e)))?;
|
||||
self.frame_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set bitrate dynamically
|
||||
pub fn set_bitrate(&mut self, bitrate: u32) -> Result<()> {
|
||||
self.encoder
|
||||
.set_bitrate(Bitrate::BitsPerSecond(bitrate as i32))
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to set bitrate: {:?}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio encoder statistics
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EncoderStats {
|
||||
pub frames_encoded: u64,
|
||||
pub bytes_output: u64,
|
||||
pub avg_frame_size: usize,
|
||||
pub current_bitrate: u32,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_opus_config_default() {
|
||||
let config = OpusConfig::default();
|
||||
assert_eq!(config.sample_rate, 48000);
|
||||
assert_eq!(config.channels, 2);
|
||||
assert_eq!(config.bitrate, 64000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_encoder() {
|
||||
let config = OpusConfig::default();
|
||||
let encoder = OpusEncoder::new(config);
|
||||
assert!(encoder.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_silence() {
|
||||
let config = OpusConfig::default();
|
||||
let mut encoder = OpusEncoder::new(config).unwrap();
|
||||
|
||||
// 20ms of stereo silence at 48kHz
|
||||
let silence = vec![0i16; 960 * 2];
|
||||
let result = encoder.encode(&silence);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let frame = result.unwrap();
|
||||
assert!(!frame.is_empty());
|
||||
assert!(frame.len() < silence.len() * 2); // Should be compressed
|
||||
}
|
||||
}
|
||||
26
src/audio/mod.rs
Normal file
26
src/audio/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
//! Audio capture and encoding module
|
||||
//!
|
||||
//! This module provides:
|
||||
//! - ALSA audio capture
|
||||
//! - Opus encoding for WebRTC
|
||||
//! - Audio device enumeration
|
||||
//! - Audio streaming pipeline
|
||||
//! - High-level audio controller
|
||||
//! - Shared audio pipeline for WebRTC multi-session support
|
||||
//! - Device health monitoring
|
||||
|
||||
pub mod capture;
|
||||
pub mod controller;
|
||||
pub mod device;
|
||||
pub mod encoder;
|
||||
pub mod monitor;
|
||||
pub mod shared_pipeline;
|
||||
pub mod streamer;
|
||||
|
||||
pub use capture::{AudioCapturer, AudioConfig, AudioFrame};
|
||||
pub use controller::{AudioController, AudioControllerConfig, AudioQuality, AudioStatus};
|
||||
pub use device::{enumerate_audio_devices, enumerate_audio_devices_with_current, AudioDeviceInfo};
|
||||
pub use encoder::{OpusConfig, OpusEncoder, OpusFrame};
|
||||
pub use monitor::{AudioHealthMonitor, AudioHealthStatus, AudioMonitorConfig};
|
||||
pub use shared_pipeline::{SharedAudioPipeline, SharedAudioPipelineConfig, SharedAudioPipelineStats};
|
||||
pub use streamer::{AudioStreamState, AudioStreamer, AudioStreamerConfig};
|
||||
352
src/audio/monitor.rs
Normal file
352
src/audio/monitor.rs
Normal file
@@ -0,0 +1,352 @@
|
||||
//! Audio device health monitoring
|
||||
//!
|
||||
//! This module provides health monitoring for audio capture devices, including:
|
||||
//! - Device connectivity checks
|
||||
//! - Automatic reconnection on failure
|
||||
//! - Error tracking and notification
|
||||
//! - Log throttling to prevent log flooding
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::events::{EventBus, SystemEvent};
|
||||
use crate::utils::LogThrottler;
|
||||
|
||||
/// Audio health status
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum AudioHealthStatus {
|
||||
/// Device is healthy and operational
|
||||
Healthy,
|
||||
/// Device has an error, attempting recovery
|
||||
Error {
|
||||
/// Human-readable error reason
|
||||
reason: String,
|
||||
/// Error code for programmatic handling
|
||||
error_code: String,
|
||||
/// Number of recovery attempts made
|
||||
retry_count: u32,
|
||||
},
|
||||
/// Device is disconnected or not available
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
impl Default for AudioHealthStatus {
|
||||
fn default() -> Self {
|
||||
Self::Healthy
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio health monitor configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioMonitorConfig {
|
||||
/// Retry interval when device is lost (milliseconds)
|
||||
pub retry_interval_ms: u64,
|
||||
/// Maximum retry attempts before giving up (0 = infinite)
|
||||
pub max_retries: u32,
|
||||
/// Log throttle interval in seconds
|
||||
pub log_throttle_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for AudioMonitorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
retry_interval_ms: 1000,
|
||||
max_retries: 0, // infinite retry
|
||||
log_throttle_secs: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio health monitor
|
||||
///
|
||||
/// Monitors audio device health and manages error recovery.
|
||||
/// Publishes WebSocket events when device status changes.
|
||||
pub struct AudioHealthMonitor {
|
||||
/// Current health status
|
||||
status: RwLock<AudioHealthStatus>,
|
||||
/// Event bus for notifications
|
||||
events: RwLock<Option<Arc<EventBus>>>,
|
||||
/// Log throttler to prevent log flooding
|
||||
throttler: LogThrottler,
|
||||
/// Configuration
|
||||
config: AudioMonitorConfig,
|
||||
/// Whether monitoring is active (reserved for future use)
|
||||
#[allow(dead_code)]
|
||||
running: AtomicBool,
|
||||
/// Current retry count
|
||||
retry_count: AtomicU32,
|
||||
/// Last error code (for change detection)
|
||||
last_error_code: RwLock<Option<String>>,
|
||||
}
|
||||
|
||||
impl AudioHealthMonitor {
|
||||
/// Create a new audio health monitor with the specified configuration
|
||||
pub fn new(config: AudioMonitorConfig) -> Self {
|
||||
let throttle_secs = config.log_throttle_secs;
|
||||
Self {
|
||||
status: RwLock::new(AudioHealthStatus::Healthy),
|
||||
events: RwLock::new(None),
|
||||
throttler: LogThrottler::with_secs(throttle_secs),
|
||||
config,
|
||||
running: AtomicBool::new(false),
|
||||
retry_count: AtomicU32::new(0),
|
||||
last_error_code: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new audio health monitor with default configuration
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(AudioMonitorConfig::default())
|
||||
}
|
||||
|
||||
/// Set the event bus for broadcasting state changes
|
||||
pub async fn set_event_bus(&self, events: Arc<EventBus>) {
|
||||
*self.events.write().await = Some(events);
|
||||
}
|
||||
|
||||
/// Report an error from audio operations
|
||||
///
|
||||
/// This method is called when an audio operation fails. It:
|
||||
/// 1. Updates the health status
|
||||
/// 2. Logs the error (with throttling)
|
||||
/// 3. Publishes a WebSocket event if the error is new or changed
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - The audio device name (if known)
|
||||
/// * `reason` - Human-readable error description
|
||||
/// * `error_code` - Error code for programmatic handling
|
||||
pub async fn report_error(&self, device: Option<&str>, reason: &str, error_code: &str) {
|
||||
let count = self.retry_count.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
|
||||
// Check if error code changed
|
||||
let error_changed = {
|
||||
let last = self.last_error_code.read().await;
|
||||
last.as_ref().map(|s| s.as_str()) != Some(error_code)
|
||||
};
|
||||
|
||||
// Log with throttling (always log if error type changed)
|
||||
let throttle_key = format!("audio_{}", error_code);
|
||||
if error_changed || self.throttler.should_log(&throttle_key) {
|
||||
warn!(
|
||||
"Audio error: {} (code: {}, attempt: {})",
|
||||
reason, error_code, count
|
||||
);
|
||||
}
|
||||
|
||||
// Update last error code
|
||||
*self.last_error_code.write().await = Some(error_code.to_string());
|
||||
|
||||
// Update status
|
||||
*self.status.write().await = AudioHealthStatus::Error {
|
||||
reason: reason.to_string(),
|
||||
error_code: error_code.to_string(),
|
||||
retry_count: count,
|
||||
};
|
||||
|
||||
// Publish event (only if error changed or first occurrence)
|
||||
if error_changed || count == 1 {
|
||||
if let Some(ref events) = *self.events.read().await {
|
||||
events.publish(SystemEvent::AudioDeviceLost {
|
||||
device: device.map(|s| s.to_string()),
|
||||
reason: reason.to_string(),
|
||||
error_code: error_code.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Report that a reconnection attempt is starting
|
||||
///
|
||||
/// Publishes a reconnecting event to notify clients.
|
||||
pub async fn report_reconnecting(&self) {
|
||||
let attempt = self.retry_count.load(Ordering::Relaxed);
|
||||
|
||||
// Only publish every 5 attempts to avoid event spam
|
||||
if attempt == 1 || attempt % 5 == 0 {
|
||||
debug!("Audio reconnecting, attempt {}", attempt);
|
||||
|
||||
if let Some(ref events) = *self.events.read().await {
|
||||
events.publish(SystemEvent::AudioReconnecting { attempt });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Report that the device has recovered
|
||||
///
|
||||
/// This method is called when the audio device successfully reconnects.
|
||||
/// It resets the error state and publishes a recovery event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - The audio device name
|
||||
pub async fn report_recovered(&self, device: Option<&str>) {
|
||||
let prev_status = self.status.read().await.clone();
|
||||
|
||||
// Only report recovery if we were in an error state
|
||||
if prev_status != AudioHealthStatus::Healthy {
|
||||
let retry_count = self.retry_count.load(Ordering::Relaxed);
|
||||
info!("Audio recovered after {} retries", retry_count);
|
||||
|
||||
// Reset state
|
||||
self.retry_count.store(0, Ordering::Relaxed);
|
||||
self.throttler.clear("audio_");
|
||||
*self.last_error_code.write().await = None;
|
||||
*self.status.write().await = AudioHealthStatus::Healthy;
|
||||
|
||||
// Publish recovery event
|
||||
if let Some(ref events) = *self.events.read().await {
|
||||
events.publish(SystemEvent::AudioRecovered {
|
||||
device: device.map(|s| s.to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current health status
|
||||
pub async fn status(&self) -> AudioHealthStatus {
|
||||
self.status.read().await.clone()
|
||||
}
|
||||
|
||||
/// Get the current retry count
|
||||
pub fn retry_count(&self) -> u32 {
|
||||
self.retry_count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Check if the monitor is in an error state
|
||||
pub async fn is_error(&self) -> bool {
|
||||
matches!(*self.status.read().await, AudioHealthStatus::Error { .. })
|
||||
}
|
||||
|
||||
/// Check if the monitor is healthy
|
||||
pub async fn is_healthy(&self) -> bool {
|
||||
matches!(*self.status.read().await, AudioHealthStatus::Healthy)
|
||||
}
|
||||
|
||||
/// Reset the monitor to healthy state without publishing events
|
||||
///
|
||||
/// This is useful during initialization.
|
||||
pub async fn reset(&self) {
|
||||
self.retry_count.store(0, Ordering::Relaxed);
|
||||
*self.last_error_code.write().await = None;
|
||||
*self.status.write().await = AudioHealthStatus::Healthy;
|
||||
self.throttler.clear_all();
|
||||
}
|
||||
|
||||
/// Get the configuration
|
||||
pub fn config(&self) -> &AudioMonitorConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Check if we should continue retrying
|
||||
///
|
||||
/// Returns `false` if max_retries is set and we've exceeded it.
|
||||
pub fn should_retry(&self) -> bool {
|
||||
if self.config.max_retries == 0 {
|
||||
return true; // Infinite retry
|
||||
}
|
||||
self.retry_count.load(Ordering::Relaxed) < self.config.max_retries
|
||||
}
|
||||
|
||||
/// Get the retry interval
|
||||
pub fn retry_interval(&self) -> Duration {
|
||||
Duration::from_millis(self.config.retry_interval_ms)
|
||||
}
|
||||
|
||||
/// Get the current error message if in error state
|
||||
pub async fn error_message(&self) -> Option<String> {
|
||||
match &*self.status.read().await {
|
||||
AudioHealthStatus::Error { reason, .. } => Some(reason.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AudioHealthMonitor {
|
||||
fn default() -> Self {
|
||||
Self::with_defaults()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initial_status() {
|
||||
let monitor = AudioHealthMonitor::with_defaults();
|
||||
assert!(monitor.is_healthy().await);
|
||||
assert!(!monitor.is_error().await);
|
||||
assert_eq!(monitor.retry_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_report_error() {
|
||||
let monitor = AudioHealthMonitor::with_defaults();
|
||||
|
||||
monitor
|
||||
.report_error(Some("hw:0,0"), "Device not found", "device_disconnected")
|
||||
.await;
|
||||
|
||||
assert!(monitor.is_error().await);
|
||||
assert_eq!(monitor.retry_count(), 1);
|
||||
|
||||
if let AudioHealthStatus::Error {
|
||||
reason,
|
||||
error_code,
|
||||
retry_count,
|
||||
} = monitor.status().await
|
||||
{
|
||||
assert_eq!(reason, "Device not found");
|
||||
assert_eq!(error_code, "device_disconnected");
|
||||
assert_eq!(retry_count, 1);
|
||||
} else {
|
||||
panic!("Expected Error status");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_report_recovered() {
|
||||
let monitor = AudioHealthMonitor::with_defaults();
|
||||
|
||||
// First report an error
|
||||
monitor
|
||||
.report_error(Some("default"), "Capture failed", "capture_error")
|
||||
.await;
|
||||
assert!(monitor.is_error().await);
|
||||
|
||||
// Then report recovery
|
||||
monitor.report_recovered(Some("default")).await;
|
||||
assert!(monitor.is_healthy().await);
|
||||
assert_eq!(monitor.retry_count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_retry_count_increments() {
|
||||
let monitor = AudioHealthMonitor::with_defaults();
|
||||
|
||||
for i in 1..=5 {
|
||||
monitor
|
||||
.report_error(None, "Error", "io_error")
|
||||
.await;
|
||||
assert_eq!(monitor.retry_count(), i);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reset() {
|
||||
let monitor = AudioHealthMonitor::with_defaults();
|
||||
|
||||
monitor
|
||||
.report_error(None, "Error", "io_error")
|
||||
.await;
|
||||
assert!(monitor.is_error().await);
|
||||
|
||||
monitor.reset().await;
|
||||
assert!(monitor.is_healthy().await);
|
||||
assert_eq!(monitor.retry_count(), 0);
|
||||
}
|
||||
}
|
||||
453
src/audio/shared_pipeline.rs
Normal file
453
src/audio/shared_pipeline.rs
Normal file
@@ -0,0 +1,453 @@
|
||||
//! Shared Audio Pipeline for WebRTC
|
||||
//!
|
||||
//! This module provides a shared audio encoding pipeline that can serve
|
||||
//! multiple WebRTC sessions with a single encoder instance.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! ```text
|
||||
//! AudioCapturer (ALSA)
|
||||
//! |
|
||||
//! v (broadcast::Receiver<AudioFrame>)
|
||||
//! SharedAudioPipeline (single Opus encoder)
|
||||
//! |
|
||||
//! v (broadcast::Sender<OpusFrame>)
|
||||
//! ┌────┴────┬────────┬────────┐
|
||||
//! v v v v
|
||||
//! Session1 Session2 Session3 ...
|
||||
//! (RTP) (RTP) (RTP) (RTP)
|
||||
//! ```
|
||||
//!
|
||||
//! # Key Features
|
||||
//!
|
||||
//! - **Single encoder**: All sessions share one Opus encoder
|
||||
//! - **Broadcast distribution**: Encoded frames are broadcast to all subscribers
|
||||
//! - **Dynamic bitrate**: Bitrate can be changed at runtime
|
||||
//! - **Statistics**: Tracks encoding performance metrics
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::{broadcast, Mutex, RwLock};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use super::capture::AudioFrame;
|
||||
use super::encoder::{OpusConfig, OpusEncoder, OpusFrame};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Shared audio pipeline configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedAudioPipelineConfig {
|
||||
/// Sample rate (must match audio capture)
|
||||
pub sample_rate: u32,
|
||||
/// Number of channels (1 or 2)
|
||||
pub channels: u32,
|
||||
/// Target bitrate in bps
|
||||
pub bitrate: u32,
|
||||
/// Opus application mode
|
||||
pub application: OpusApplicationMode,
|
||||
/// Enable forward error correction
|
||||
pub fec: bool,
|
||||
/// Broadcast channel capacity
|
||||
pub channel_capacity: usize,
|
||||
}
|
||||
|
||||
impl Default for SharedAudioPipelineConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bitrate: 64000,
|
||||
application: OpusApplicationMode::Audio,
|
||||
fec: true,
|
||||
channel_capacity: 64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedAudioPipelineConfig {
|
||||
/// Create config optimized for voice
|
||||
pub fn voice() -> Self {
|
||||
Self {
|
||||
bitrate: 32000,
|
||||
application: OpusApplicationMode::Voip,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config optimized for music/high quality
|
||||
pub fn high_quality() -> Self {
|
||||
Self {
|
||||
bitrate: 128000,
|
||||
application: OpusApplicationMode::Audio,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to OpusConfig
|
||||
pub fn to_opus_config(&self) -> OpusConfig {
|
||||
OpusConfig {
|
||||
sample_rate: self.sample_rate,
|
||||
channels: self.channels,
|
||||
bitrate: self.bitrate,
|
||||
application: match self.application {
|
||||
OpusApplicationMode::Voip => super::encoder::OpusApplication::Voip,
|
||||
OpusApplicationMode::Audio => super::encoder::OpusApplication::Audio,
|
||||
OpusApplicationMode::LowDelay => super::encoder::OpusApplication::LowDelay,
|
||||
},
|
||||
fec: self.fec,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opus application mode
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OpusApplicationMode {
|
||||
/// Voice over IP - optimized for speech
|
||||
Voip,
|
||||
/// General audio - balanced quality
|
||||
Audio,
|
||||
/// Low delay mode - minimal latency
|
||||
LowDelay,
|
||||
}
|
||||
|
||||
/// Shared audio pipeline statistics
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SharedAudioPipelineStats {
|
||||
/// Frames received from audio capture
|
||||
pub frames_received: u64,
|
||||
/// Frames successfully encoded
|
||||
pub frames_encoded: u64,
|
||||
/// Frames dropped (encode errors)
|
||||
pub frames_dropped: u64,
|
||||
/// Total bytes encoded
|
||||
pub bytes_encoded: u64,
|
||||
/// Number of active subscribers
|
||||
pub subscribers: u64,
|
||||
/// Average encode time in milliseconds
|
||||
pub avg_encode_time_ms: f32,
|
||||
/// Current bitrate in bps
|
||||
pub current_bitrate: u32,
|
||||
/// Pipeline running time in seconds
|
||||
pub running_time_secs: f64,
|
||||
}
|
||||
|
||||
/// Shared Audio Pipeline
|
||||
///
|
||||
/// Provides a single Opus encoder that serves multiple WebRTC sessions.
|
||||
/// All sessions receive the same encoded audio stream via broadcast channel.
|
||||
pub struct SharedAudioPipeline {
|
||||
/// Configuration
|
||||
config: RwLock<SharedAudioPipelineConfig>,
|
||||
/// Opus encoder (protected by mutex for encoding)
|
||||
encoder: Mutex<Option<OpusEncoder>>,
|
||||
/// Broadcast sender for encoded Opus frames
|
||||
opus_tx: broadcast::Sender<OpusFrame>,
|
||||
/// Running state
|
||||
running: AtomicBool,
|
||||
/// Statistics
|
||||
stats: Mutex<SharedAudioPipelineStats>,
|
||||
/// Start time for running time calculation
|
||||
start_time: RwLock<Option<Instant>>,
|
||||
/// Encode time accumulator for averaging
|
||||
encode_time_sum_us: AtomicU64,
|
||||
/// Encode count for averaging
|
||||
encode_count: AtomicU64,
|
||||
/// Stop signal (atomic for lock-free checking)
|
||||
stop_flag: AtomicBool,
|
||||
/// Encoding task handle
|
||||
task_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl SharedAudioPipeline {
|
||||
/// Create a new shared audio pipeline
|
||||
pub fn new(config: SharedAudioPipelineConfig) -> Result<Arc<Self>> {
|
||||
let (opus_tx, _) = broadcast::channel(config.channel_capacity);
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
config: RwLock::new(config),
|
||||
encoder: Mutex::new(None),
|
||||
opus_tx,
|
||||
running: AtomicBool::new(false),
|
||||
stats: Mutex::new(SharedAudioPipelineStats::default()),
|
||||
start_time: RwLock::new(None),
|
||||
encode_time_sum_us: AtomicU64::new(0),
|
||||
encode_count: AtomicU64::new(0),
|
||||
stop_flag: AtomicBool::new(false),
|
||||
task_handle: Mutex::new(None),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create with default configuration
|
||||
pub fn default_config() -> Result<Arc<Self>> {
|
||||
Self::new(SharedAudioPipelineConfig::default())
|
||||
}
|
||||
|
||||
/// Start the audio encoding pipeline
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `audio_rx` - Receiver for raw audio frames from AudioCapturer
|
||||
pub async fn start(self: &Arc<Self>, audio_rx: broadcast::Receiver<AudioFrame>) -> Result<()> {
|
||||
if self.running.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config = self.config.read().await.clone();
|
||||
|
||||
info!(
|
||||
"Starting shared audio pipeline: {}Hz {}ch {}bps",
|
||||
config.sample_rate, config.channels, config.bitrate
|
||||
);
|
||||
|
||||
// Create encoder
|
||||
let opus_config = config.to_opus_config();
|
||||
let encoder = OpusEncoder::new(opus_config)?;
|
||||
*self.encoder.lock().await = Some(encoder);
|
||||
|
||||
// Reset stats
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
*stats = SharedAudioPipelineStats::default();
|
||||
stats.current_bitrate = config.bitrate;
|
||||
}
|
||||
|
||||
// Reset counters
|
||||
self.encode_time_sum_us.store(0, Ordering::SeqCst);
|
||||
self.encode_count.store(0, Ordering::SeqCst);
|
||||
*self.start_time.write().await = Some(Instant::now());
|
||||
self.stop_flag.store(false, Ordering::SeqCst);
|
||||
|
||||
self.running.store(true, Ordering::SeqCst);
|
||||
|
||||
// Start encoding task
|
||||
let pipeline = self.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
pipeline.encoding_task(audio_rx).await;
|
||||
});
|
||||
|
||||
*self.task_handle.lock().await = Some(handle);
|
||||
|
||||
info!("Shared audio pipeline started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the audio encoding pipeline
|
||||
pub fn stop(&self) {
|
||||
if !self.running.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Stopping shared audio pipeline");
|
||||
|
||||
// Signal stop (atomic, no lock needed)
|
||||
self.stop_flag.store(true, Ordering::SeqCst);
|
||||
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Check if pipeline is running
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.running.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Subscribe to encoded Opus frames
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<OpusFrame> {
|
||||
self.opus_tx.subscribe()
|
||||
}
|
||||
|
||||
/// Get number of active subscribers
|
||||
pub fn subscriber_count(&self) -> usize {
|
||||
self.opus_tx.receiver_count()
|
||||
}
|
||||
|
||||
/// Get current statistics
|
||||
pub async fn stats(&self) -> SharedAudioPipelineStats {
|
||||
let mut stats = self.stats.lock().await.clone();
|
||||
stats.subscribers = self.subscriber_count() as u64;
|
||||
|
||||
// Calculate average encode time
|
||||
let count = self.encode_count.load(Ordering::SeqCst);
|
||||
if count > 0 {
|
||||
let sum_us = self.encode_time_sum_us.load(Ordering::SeqCst);
|
||||
stats.avg_encode_time_ms = (sum_us as f64 / count as f64 / 1000.0) as f32;
|
||||
}
|
||||
|
||||
// Calculate running time
|
||||
if let Some(start) = *self.start_time.read().await {
|
||||
stats.running_time_secs = start.elapsed().as_secs_f64();
|
||||
}
|
||||
|
||||
stats
|
||||
}
|
||||
|
||||
/// Set bitrate dynamically
|
||||
pub async fn set_bitrate(&self, bitrate: u32) -> Result<()> {
|
||||
// Update config
|
||||
self.config.write().await.bitrate = bitrate;
|
||||
|
||||
// Update encoder if running
|
||||
if let Some(ref mut encoder) = *self.encoder.lock().await {
|
||||
encoder.set_bitrate(bitrate)?;
|
||||
}
|
||||
|
||||
// Update stats
|
||||
self.stats.lock().await.current_bitrate = bitrate;
|
||||
|
||||
info!("Shared audio pipeline bitrate changed to {}bps", bitrate);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update configuration (requires restart)
|
||||
pub async fn update_config(&self, config: SharedAudioPipelineConfig) -> Result<()> {
|
||||
if self.is_running() {
|
||||
return Err(AppError::AudioError(
|
||||
"Cannot update config while pipeline is running".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
*self.config.write().await = config;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal encoding task
|
||||
async fn encoding_task(self: Arc<Self>, mut audio_rx: broadcast::Receiver<AudioFrame>) {
|
||||
info!("Audio encoding task started");
|
||||
|
||||
loop {
|
||||
// Check stop flag (atomic, no async lock needed)
|
||||
if self.stop_flag.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Receive audio frame with timeout
|
||||
let recv_result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
audio_rx.recv(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match recv_result {
|
||||
Ok(Ok(audio_frame)) => {
|
||||
// Update received count
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.frames_received += 1;
|
||||
}
|
||||
|
||||
// Encode frame
|
||||
let encode_start = Instant::now();
|
||||
let encode_result = {
|
||||
let mut encoder_guard = self.encoder.lock().await;
|
||||
if let Some(ref mut encoder) = *encoder_guard {
|
||||
Some(encoder.encode_frame(&audio_frame))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let encode_time = encode_start.elapsed();
|
||||
|
||||
// Update encode time stats
|
||||
self.encode_time_sum_us
|
||||
.fetch_add(encode_time.as_micros() as u64, Ordering::SeqCst);
|
||||
self.encode_count.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
match encode_result {
|
||||
Some(Ok(opus_frame)) => {
|
||||
// Update stats
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.frames_encoded += 1;
|
||||
stats.bytes_encoded += opus_frame.data.len() as u64;
|
||||
}
|
||||
|
||||
// Broadcast to subscribers
|
||||
if self.opus_tx.receiver_count() > 0 {
|
||||
if let Err(e) = self.opus_tx.send(opus_frame) {
|
||||
trace!("No audio subscribers: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
error!("Opus encode error: {}", e);
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.frames_dropped += 1;
|
||||
}
|
||||
None => {
|
||||
warn!("Encoder not available");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(broadcast::error::RecvError::Closed)) => {
|
||||
info!("Audio source channel closed");
|
||||
break;
|
||||
}
|
||||
Ok(Err(broadcast::error::RecvError::Lagged(n))) => {
|
||||
warn!("Audio pipeline lagged by {} frames", n);
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.frames_dropped += n;
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout - check if still running
|
||||
if !self.running.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
debug!("Audio receive timeout, continuing...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
*self.encoder.lock().await = None;
|
||||
|
||||
let stats = self.stats().await;
|
||||
info!(
|
||||
"Audio encoding task ended: {} frames encoded, {} dropped, {:.1}s runtime",
|
||||
stats.frames_encoded, stats.frames_dropped, stats.running_time_secs
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SharedAudioPipeline {
|
||||
fn drop(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_config_default() {
|
||||
let config = SharedAudioPipelineConfig::default();
|
||||
assert_eq!(config.sample_rate, 48000);
|
||||
assert_eq!(config.channels, 2);
|
||||
assert_eq!(config.bitrate, 64000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_voice() {
|
||||
let config = SharedAudioPipelineConfig::voice();
|
||||
assert_eq!(config.bitrate, 32000);
|
||||
assert_eq!(config.application, OpusApplicationMode::Voip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_high_quality() {
|
||||
let config = SharedAudioPipelineConfig::high_quality();
|
||||
assert_eq!(config.bitrate, 128000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_pipeline_creation() {
|
||||
let config = SharedAudioPipelineConfig::default();
|
||||
let pipeline = SharedAudioPipeline::new(config);
|
||||
assert!(pipeline.is_ok());
|
||||
|
||||
let pipeline = pipeline.unwrap();
|
||||
assert!(!pipeline.is_running());
|
||||
assert_eq!(pipeline.subscriber_count(), 0);
|
||||
}
|
||||
}
|
||||
401
src/audio/streamer.rs
Normal file
401
src/audio/streamer.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
//! Audio streaming pipeline
|
||||
//!
|
||||
//! Coordinates audio capture and Opus encoding, distributing encoded
|
||||
//! frames to multiple subscribers via broadcast channel.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::{broadcast, watch, Mutex, RwLock};
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
use super::capture::{AudioCapturer, AudioConfig, CaptureState};
|
||||
use super::encoder::{OpusConfig, OpusEncoder, OpusFrame};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// Audio stream state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AudioStreamState {
|
||||
/// Stream is stopped
|
||||
Stopped,
|
||||
/// Stream is starting up
|
||||
Starting,
|
||||
/// Stream is running
|
||||
Running,
|
||||
/// Stream encountered an error
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Default for AudioStreamState {
|
||||
fn default() -> Self {
|
||||
Self::Stopped
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio streamer configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioStreamerConfig {
|
||||
/// Audio capture configuration
|
||||
pub capture: AudioConfig,
|
||||
/// Opus encoder configuration
|
||||
pub opus: OpusConfig,
|
||||
}
|
||||
|
||||
impl Default for AudioStreamerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
capture: AudioConfig::default(),
|
||||
opus: OpusConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioStreamerConfig {
|
||||
/// Create config for a specific device with default quality
|
||||
pub fn for_device(device_name: &str) -> Self {
|
||||
Self {
|
||||
capture: AudioConfig {
|
||||
device_name: device_name.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
opus: OpusConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create config with specified bitrate
|
||||
pub fn with_bitrate(mut self, bitrate: u32) -> Self {
|
||||
self.opus.bitrate = bitrate;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio stream statistics
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AudioStreamStats {
|
||||
/// Frames captured from ALSA
|
||||
pub frames_captured: u64,
|
||||
/// Frames encoded to Opus
|
||||
pub frames_encoded: u64,
|
||||
/// Total bytes output (Opus)
|
||||
pub bytes_output: u64,
|
||||
/// Current encoding bitrate
|
||||
pub current_bitrate: u32,
|
||||
/// Number of active subscribers
|
||||
pub subscriber_count: usize,
|
||||
/// Buffer overruns
|
||||
pub buffer_overruns: u64,
|
||||
}
|
||||
|
||||
/// Audio streamer
|
||||
///
|
||||
/// Manages the audio capture -> encode -> broadcast pipeline.
|
||||
pub struct AudioStreamer {
|
||||
config: RwLock<AudioStreamerConfig>,
|
||||
state: watch::Sender<AudioStreamState>,
|
||||
state_rx: watch::Receiver<AudioStreamState>,
|
||||
capturer: RwLock<Option<Arc<AudioCapturer>>>,
|
||||
encoder: Arc<Mutex<Option<OpusEncoder>>>,
|
||||
opus_tx: broadcast::Sender<OpusFrame>,
|
||||
stats: Arc<Mutex<AudioStreamStats>>,
|
||||
sequence: AtomicU64,
|
||||
stream_start_time: RwLock<Option<Instant>>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl AudioStreamer {
|
||||
/// Create a new audio streamer with default configuration
|
||||
pub fn new() -> Self {
|
||||
Self::with_config(AudioStreamerConfig::default())
|
||||
}
|
||||
|
||||
/// Create a new audio streamer with specified configuration
|
||||
pub fn with_config(config: AudioStreamerConfig) -> Self {
|
||||
let (state_tx, state_rx) = watch::channel(AudioStreamState::Stopped);
|
||||
let (opus_tx, _) = broadcast::channel(64);
|
||||
|
||||
Self {
|
||||
config: RwLock::new(config),
|
||||
state: state_tx,
|
||||
state_rx,
|
||||
capturer: RwLock::new(None),
|
||||
encoder: Arc::new(Mutex::new(None)),
|
||||
opus_tx,
|
||||
stats: Arc::new(Mutex::new(AudioStreamStats::default())),
|
||||
sequence: AtomicU64::new(0),
|
||||
stream_start_time: RwLock::new(None),
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current state
|
||||
pub fn state(&self) -> AudioStreamState {
|
||||
*self.state_rx.borrow()
|
||||
}
|
||||
|
||||
/// Subscribe to state changes
|
||||
pub fn state_watch(&self) -> watch::Receiver<AudioStreamState> {
|
||||
self.state_rx.clone()
|
||||
}
|
||||
|
||||
/// Subscribe to Opus frames
|
||||
pub fn subscribe_opus(&self) -> broadcast::Receiver<OpusFrame> {
|
||||
self.opus_tx.subscribe()
|
||||
}
|
||||
|
||||
/// Get number of active subscribers
|
||||
pub fn subscriber_count(&self) -> usize {
|
||||
self.opus_tx.receiver_count()
|
||||
}
|
||||
|
||||
/// Get current statistics
|
||||
pub async fn stats(&self) -> AudioStreamStats {
|
||||
let mut stats = self.stats.lock().await.clone();
|
||||
stats.subscriber_count = self.subscriber_count();
|
||||
stats
|
||||
}
|
||||
|
||||
/// Update configuration (only when stopped)
|
||||
pub async fn set_config(&self, config: AudioStreamerConfig) -> Result<()> {
|
||||
if self.state() != AudioStreamState::Stopped {
|
||||
return Err(AppError::AudioError(
|
||||
"Cannot change config while streaming".to_string(),
|
||||
));
|
||||
}
|
||||
*self.config.write().await = config;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update bitrate dynamically (can be done while streaming)
|
||||
pub async fn set_bitrate(&self, bitrate: u32) -> Result<()> {
|
||||
// Update config
|
||||
self.config.write().await.opus.bitrate = bitrate;
|
||||
|
||||
// Update encoder if running
|
||||
if let Some(ref mut encoder) = *self.encoder.lock().await {
|
||||
encoder.set_bitrate(bitrate)?;
|
||||
}
|
||||
|
||||
// Update stats
|
||||
self.stats.lock().await.current_bitrate = bitrate;
|
||||
|
||||
info!("Audio bitrate changed to {}bps", bitrate);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the audio stream
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
if self.state() == AudioStreamState::Running {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = self.state.send(AudioStreamState::Starting);
|
||||
self.stop_flag.store(false, Ordering::SeqCst);
|
||||
|
||||
let config = self.config.read().await.clone();
|
||||
|
||||
info!(
|
||||
"Starting audio stream: {} @ {}Hz {}ch, {}bps Opus",
|
||||
config.capture.device_name,
|
||||
config.capture.sample_rate,
|
||||
config.capture.channels,
|
||||
config.opus.bitrate
|
||||
);
|
||||
|
||||
// Create capturer
|
||||
let capturer = Arc::new(AudioCapturer::new(config.capture.clone()));
|
||||
*self.capturer.write().await = Some(capturer.clone());
|
||||
|
||||
// Create encoder
|
||||
let encoder = OpusEncoder::new(config.opus.clone())?;
|
||||
*self.encoder.lock().await = Some(encoder);
|
||||
|
||||
// Start capture
|
||||
capturer.start().await?;
|
||||
|
||||
// Reset stats
|
||||
{
|
||||
let mut stats = self.stats.lock().await;
|
||||
*stats = AudioStreamStats::default();
|
||||
stats.current_bitrate = config.opus.bitrate;
|
||||
}
|
||||
|
||||
// Record start time
|
||||
*self.stream_start_time.write().await = Some(Instant::now());
|
||||
self.sequence.store(0, Ordering::SeqCst);
|
||||
|
||||
// Start encoding task
|
||||
let capturer_for_task = capturer.clone();
|
||||
let encoder = self.encoder.clone();
|
||||
let opus_tx = self.opus_tx.clone();
|
||||
let stats = self.stats.clone();
|
||||
let state = self.state.clone();
|
||||
let stop_flag = self.stop_flag.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
Self::stream_task(capturer_for_task, encoder, opus_tx, stats, state, stop_flag).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop the audio stream
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
if self.state() == AudioStreamState::Stopped {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Stopping audio stream");
|
||||
|
||||
// Signal stop
|
||||
self.stop_flag.store(true, Ordering::SeqCst);
|
||||
|
||||
// Stop capturer
|
||||
if let Some(ref capturer) = *self.capturer.read().await {
|
||||
capturer.stop().await?;
|
||||
}
|
||||
|
||||
// Clear resources
|
||||
*self.capturer.write().await = None;
|
||||
*self.encoder.lock().await = None;
|
||||
*self.stream_start_time.write().await = None;
|
||||
|
||||
let _ = self.state.send(AudioStreamState::Stopped);
|
||||
info!("Audio stream stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if streaming
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.state() == AudioStreamState::Running
|
||||
}
|
||||
|
||||
/// Internal streaming task
|
||||
async fn stream_task(
|
||||
capturer: Arc<AudioCapturer>,
|
||||
encoder: Arc<Mutex<Option<OpusEncoder>>>,
|
||||
opus_tx: broadcast::Sender<OpusFrame>,
|
||||
stats: Arc<Mutex<AudioStreamStats>>,
|
||||
state: watch::Sender<AudioStreamState>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
) {
|
||||
let mut pcm_rx = capturer.subscribe();
|
||||
let _ = state.send(AudioStreamState::Running);
|
||||
|
||||
info!("Audio stream task started");
|
||||
|
||||
loop {
|
||||
// Check stop flag (atomic, no async lock needed)
|
||||
if stop_flag.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check capturer state
|
||||
if capturer.state() == CaptureState::Error {
|
||||
error!("Audio capture error, stopping stream");
|
||||
let _ = state.send(AudioStreamState::Error);
|
||||
break;
|
||||
}
|
||||
|
||||
// Receive PCM frame with timeout
|
||||
let recv_result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(2),
|
||||
pcm_rx.recv(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match recv_result {
|
||||
Ok(Ok(audio_frame)) => {
|
||||
// Update capture stats
|
||||
{
|
||||
let mut s = stats.lock().await;
|
||||
s.frames_captured += 1;
|
||||
}
|
||||
|
||||
// Encode to Opus
|
||||
let opus_result = {
|
||||
let mut enc_guard = encoder.lock().await;
|
||||
if let Some(ref mut enc) = *enc_guard {
|
||||
Some(enc.encode_frame(&audio_frame))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
match opus_result {
|
||||
Some(Ok(opus_frame)) => {
|
||||
// Update stats
|
||||
{
|
||||
let mut s = stats.lock().await;
|
||||
s.frames_encoded += 1;
|
||||
s.bytes_output += opus_frame.data.len() as u64;
|
||||
}
|
||||
|
||||
// Broadcast to subscribers
|
||||
if opus_tx.receiver_count() > 0 {
|
||||
if let Err(e) = opus_tx.send(opus_frame) {
|
||||
trace!("No audio subscribers: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
error!("Opus encode error: {}", e);
|
||||
}
|
||||
None => {
|
||||
warn!("Encoder not available");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(broadcast::error::RecvError::Closed)) => {
|
||||
info!("Audio capture channel closed");
|
||||
break;
|
||||
}
|
||||
Ok(Err(broadcast::error::RecvError::Lagged(n))) => {
|
||||
warn!("Audio receiver lagged by {} frames", n);
|
||||
let mut s = stats.lock().await;
|
||||
s.buffer_overruns += n;
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout - check if still capturing
|
||||
if capturer.state() != CaptureState::Running {
|
||||
info!("Audio capture stopped, ending stream task");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = state.send(AudioStreamState::Stopped);
|
||||
info!("Audio stream task ended");
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AudioStreamer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_streamer_config_default() {
|
||||
let config = AudioStreamerConfig::default();
|
||||
assert_eq!(config.capture.sample_rate, 48000);
|
||||
assert_eq!(config.opus.bitrate, 64000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_streamer_config_for_device() {
|
||||
let config = AudioStreamerConfig::for_device("hw:0,0");
|
||||
assert_eq!(config.capture.device_name, "hw:0,0");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_streamer_state() {
|
||||
let streamer = AudioStreamer::new();
|
||||
assert_eq!(streamer.state(), AudioStreamState::Stopped);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user