mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-14 03:32:00 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
@@ -1,334 +1,9 @@
|
||||
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, info};
|
||||
#[cfg(unix)]
|
||||
#[path = "capture_linux.rs"]
|
||||
mod imp;
|
||||
|
||||
use super::device::AudioDeviceInfo;
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::utils::LogThrottler;
|
||||
use crate::{error_throttled, warn_throttled};
|
||||
#[cfg(windows)]
|
||||
#[path = "capture_windows.rs"]
|
||||
mod imp;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioConfig {
|
||||
pub device_name: String,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u32,
|
||||
pub frame_size: u32,
|
||||
pub buffer_frames: u32,
|
||||
pub period_frames: u32,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_name: String::new(),
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
frame_size: 960,
|
||||
buffer_frames: 4096,
|
||||
period_frames: 960,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioConfig {
|
||||
pub fn for_device(device: &AudioDeviceInfo) -> Self {
|
||||
Self {
|
||||
device_name: device.name.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_per_sample(&self) -> u32 {
|
||||
2 * self.channels
|
||||
}
|
||||
|
||||
pub fn bytes_per_frame(&self) -> usize {
|
||||
(self.frame_size * self.bytes_per_sample()) as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioFrame {
|
||||
pub data: Bytes,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u32,
|
||||
pub samples: u32,
|
||||
pub sequence: u64,
|
||||
pub timestamp: Instant,
|
||||
}
|
||||
|
||||
impl AudioFrame {
|
||||
pub fn new_interleaved(data: Bytes, channels: u32, sample_rate: u32, sequence: u64) -> Self {
|
||||
let bps = 2 * channels;
|
||||
Self {
|
||||
samples: data.len() as u32 / bps,
|
||||
data,
|
||||
sample_rate,
|
||||
channels,
|
||||
sequence,
|
||||
timestamp: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CaptureState {
|
||||
Stopped,
|
||||
Running,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct AudioCapturer {
|
||||
config: AudioConfig,
|
||||
state: Arc<watch::Sender<CaptureState>>,
|
||||
state_rx: watch::Receiver<CaptureState>,
|
||||
frame_tx: broadcast::Sender<AudioFrame>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
sequence: Arc<AtomicU64>,
|
||||
capture_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
log_throttler: LogThrottler,
|
||||
}
|
||||
|
||||
impl AudioCapturer {
|
||||
pub fn new(config: AudioConfig) -> Self {
|
||||
let (state_tx, state_rx) = watch::channel(CaptureState::Stopped);
|
||||
let (frame_tx, _) = broadcast::channel(16);
|
||||
|
||||
Self {
|
||||
config,
|
||||
state: Arc::new(state_tx),
|
||||
state_rx,
|
||||
frame_tx,
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
sequence: Arc::new(AtomicU64::new(0)),
|
||||
capture_handle: Mutex::new(None),
|
||||
log_throttler: LogThrottler::with_secs(5),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> CaptureState {
|
||||
*self.state_rx.borrow()
|
||||
}
|
||||
|
||||
pub fn state_watch(&self) -> watch::Receiver<CaptureState> {
|
||||
self.state_rx.clone()
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame> {
|
||||
self.frame_tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
if self.state() == CaptureState::Running {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!(
|
||||
"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 frame_tx = self.frame_tx.clone();
|
||||
let stop_flag = self.stop_flag.clone();
|
||||
let sequence = self.sequence.clone();
|
||||
let log_throttler = self.log_throttler.clone();
|
||||
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
let result = run_capture(
|
||||
&config,
|
||||
&state,
|
||||
&frame_tx,
|
||||
&stop_flag,
|
||||
&sequence,
|
||||
&log_throttler,
|
||||
);
|
||||
|
||||
if let Err(e) = result {
|
||||
error_throttled!(log_throttler, "capture_error", "Audio capture error: {}", e);
|
||||
let _ = state.send(CaptureState::Error);
|
||||
} else {
|
||||
let _ = state.send(CaptureState::Stopped);
|
||||
}
|
||||
});
|
||||
|
||||
*self.capture_handle.lock().await = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.state() == CaptureState::Running
|
||||
}
|
||||
}
|
||||
|
||||
fn run_capture(
|
||||
config: &AudioConfig,
|
||||
state: &watch::Sender<CaptureState>,
|
||||
frame_tx: &broadcast::Sender<AudioFrame>,
|
||||
stop_flag: &AtomicBool,
|
||||
sequence: &AtomicU64,
|
||||
log_throttler: &LogThrottler,
|
||||
) -> Result<()> {
|
||||
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
|
||||
))
|
||||
})?;
|
||||
|
||||
{
|
||||
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)))?;
|
||||
}
|
||||
|
||||
let hw_now = pcm.hw_params_current().map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to read hw_params after apply: {}", e))
|
||||
})?;
|
||||
let actual_rate = hw_now
|
||||
.get_rate()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to read sample rate: {}", e)))?;
|
||||
let actual_ch = hw_now
|
||||
.get_channels()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to read channels: {}", e)))?;
|
||||
if actual_rate != 48_000 {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Audio capture requires 48000 Hz; device is {} Hz",
|
||||
actual_rate
|
||||
)));
|
||||
}
|
||||
if actual_ch != 2 {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Audio capture requires 2 channels (stereo); device has {}",
|
||||
actual_ch
|
||||
)));
|
||||
}
|
||||
debug!("Audio capture: 48000 Hz, 2 ch");
|
||||
|
||||
pcm.prepare()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to prepare PCM: {}", e)))?;
|
||||
|
||||
let _ = state.send(CaptureState::Running);
|
||||
|
||||
let period_frames = pcm
|
||||
.hw_params_current()
|
||||
.ok()
|
||||
.and_then(|h| h.get_period_size().ok())
|
||||
.map(|f| f as usize)
|
||||
.unwrap_or(1024)
|
||||
.max(256);
|
||||
let buf_frames = period_frames.saturating_mul(4).max(2048);
|
||||
let bytes_per_frame = (config.channels as usize) * 2;
|
||||
let mut buffer = vec![0u8; buf_frames * bytes_per_frame];
|
||||
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
match pcm.state() {
|
||||
State::XRun => {
|
||||
warn_throttled!(log_throttler, "xrun", "Audio buffer overrun, recovering");
|
||||
let _ = pcm.prepare();
|
||||
continue;
|
||||
}
|
||||
State::Suspended => {
|
||||
warn_throttled!(
|
||||
log_throttler,
|
||||
"suspended",
|
||||
"Audio device suspended, recovering"
|
||||
);
|
||||
let _ = pcm.resume();
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// io_bytes: USB capture often lacks mmap (io_checked requires it).
|
||||
let io: IO<u8> = pcm.io_bytes();
|
||||
|
||||
match io.readi(&mut buffer) {
|
||||
Ok(frames_read) => {
|
||||
if frames_read == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let byte_count = frames_read * config.channels as usize * 2;
|
||||
|
||||
let seq = sequence.fetch_add(1, Ordering::Relaxed);
|
||||
let frame = AudioFrame::new_interleaved(
|
||||
Bytes::copy_from_slice(&buffer[..byte_count]),
|
||||
config.channels,
|
||||
48_000,
|
||||
seq,
|
||||
);
|
||||
|
||||
if frame_tx.receiver_count() > 0 {
|
||||
if let Err(e) = frame_tx.send(frame) {
|
||||
debug!("No audio receivers: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let desc = e.to_string();
|
||||
if is_device_lost_error(&desc) {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Audio device lost while reading {}: {}",
|
||||
config.device_name, e
|
||||
)));
|
||||
} else if desc.contains("EPIPE") || desc.contains("Broken pipe") {
|
||||
warn_throttled!(log_throttler, "buffer_overrun", "Audio buffer overrun");
|
||||
let _ = pcm.prepare();
|
||||
} else {
|
||||
error_throttled!(log_throttler, "read_error", "Audio read error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Audio capture stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_device_lost_error(desc: &str) -> bool {
|
||||
desc.contains("No such device")
|
||||
|| desc.contains("ENODEV")
|
||||
|| desc.contains("ENXIO")
|
||||
|| desc.contains("ESHUTDOWN")
|
||||
}
|
||||
pub use imp::*;
|
||||
|
||||
334
src/audio/capture_linux.rs
Normal file
334
src/audio/capture_linux.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
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, info};
|
||||
|
||||
use super::device::AudioDeviceInfo;
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::utils::LogThrottler;
|
||||
use crate::{error_throttled, warn_throttled};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioConfig {
|
||||
pub device_name: String,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u32,
|
||||
pub frame_size: u32,
|
||||
pub buffer_frames: u32,
|
||||
pub period_frames: u32,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_name: String::new(),
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
frame_size: 960,
|
||||
buffer_frames: 4096,
|
||||
period_frames: 960,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioConfig {
|
||||
pub fn for_device(device: &AudioDeviceInfo) -> Self {
|
||||
Self {
|
||||
device_name: device.name.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_per_sample(&self) -> u32 {
|
||||
2 * self.channels
|
||||
}
|
||||
|
||||
pub fn bytes_per_frame(&self) -> usize {
|
||||
(self.frame_size * self.bytes_per_sample()) as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioFrame {
|
||||
pub data: Bytes,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u32,
|
||||
pub samples: u32,
|
||||
pub sequence: u64,
|
||||
pub timestamp: Instant,
|
||||
}
|
||||
|
||||
impl AudioFrame {
|
||||
pub fn new_interleaved(data: Bytes, channels: u32, sample_rate: u32, sequence: u64) -> Self {
|
||||
let bps = 2 * channels;
|
||||
Self {
|
||||
samples: data.len() as u32 / bps,
|
||||
data,
|
||||
sample_rate,
|
||||
channels,
|
||||
sequence,
|
||||
timestamp: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CaptureState {
|
||||
Stopped,
|
||||
Running,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct AudioCapturer {
|
||||
config: AudioConfig,
|
||||
state: Arc<watch::Sender<CaptureState>>,
|
||||
state_rx: watch::Receiver<CaptureState>,
|
||||
frame_tx: broadcast::Sender<AudioFrame>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
sequence: Arc<AtomicU64>,
|
||||
capture_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
log_throttler: LogThrottler,
|
||||
}
|
||||
|
||||
impl AudioCapturer {
|
||||
pub fn new(config: AudioConfig) -> Self {
|
||||
let (state_tx, state_rx) = watch::channel(CaptureState::Stopped);
|
||||
let (frame_tx, _) = broadcast::channel(16);
|
||||
|
||||
Self {
|
||||
config,
|
||||
state: Arc::new(state_tx),
|
||||
state_rx,
|
||||
frame_tx,
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
sequence: Arc::new(AtomicU64::new(0)),
|
||||
capture_handle: Mutex::new(None),
|
||||
log_throttler: LogThrottler::with_secs(5),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> CaptureState {
|
||||
*self.state_rx.borrow()
|
||||
}
|
||||
|
||||
pub fn state_watch(&self) -> watch::Receiver<CaptureState> {
|
||||
self.state_rx.clone()
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame> {
|
||||
self.frame_tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
if self.state() == CaptureState::Running {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!(
|
||||
"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 frame_tx = self.frame_tx.clone();
|
||||
let stop_flag = self.stop_flag.clone();
|
||||
let sequence = self.sequence.clone();
|
||||
let log_throttler = self.log_throttler.clone();
|
||||
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
let result = run_capture(
|
||||
&config,
|
||||
&state,
|
||||
&frame_tx,
|
||||
&stop_flag,
|
||||
&sequence,
|
||||
&log_throttler,
|
||||
);
|
||||
|
||||
if let Err(e) = result {
|
||||
error_throttled!(log_throttler, "capture_error", "Audio capture error: {}", e);
|
||||
let _ = state.send(CaptureState::Error);
|
||||
} else {
|
||||
let _ = state.send(CaptureState::Stopped);
|
||||
}
|
||||
});
|
||||
|
||||
*self.capture_handle.lock().await = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.state() == CaptureState::Running
|
||||
}
|
||||
}
|
||||
|
||||
fn run_capture(
|
||||
config: &AudioConfig,
|
||||
state: &watch::Sender<CaptureState>,
|
||||
frame_tx: &broadcast::Sender<AudioFrame>,
|
||||
stop_flag: &AtomicBool,
|
||||
sequence: &AtomicU64,
|
||||
log_throttler: &LogThrottler,
|
||||
) -> Result<()> {
|
||||
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
|
||||
))
|
||||
})?;
|
||||
|
||||
{
|
||||
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)))?;
|
||||
}
|
||||
|
||||
let hw_now = pcm.hw_params_current().map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to read hw_params after apply: {}", e))
|
||||
})?;
|
||||
let actual_rate = hw_now
|
||||
.get_rate()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to read sample rate: {}", e)))?;
|
||||
let actual_ch = hw_now
|
||||
.get_channels()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to read channels: {}", e)))?;
|
||||
if actual_rate != 48_000 {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Audio capture requires 48000 Hz; device is {} Hz",
|
||||
actual_rate
|
||||
)));
|
||||
}
|
||||
if actual_ch != 2 {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Audio capture requires 2 channels (stereo); device has {}",
|
||||
actual_ch
|
||||
)));
|
||||
}
|
||||
debug!("Audio capture: 48000 Hz, 2 ch");
|
||||
|
||||
pcm.prepare()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to prepare PCM: {}", e)))?;
|
||||
|
||||
let _ = state.send(CaptureState::Running);
|
||||
|
||||
let period_frames = pcm
|
||||
.hw_params_current()
|
||||
.ok()
|
||||
.and_then(|h| h.get_period_size().ok())
|
||||
.map(|f| f as usize)
|
||||
.unwrap_or(1024)
|
||||
.max(256);
|
||||
let buf_frames = period_frames.saturating_mul(4).max(2048);
|
||||
let bytes_per_frame = (config.channels as usize) * 2;
|
||||
let mut buffer = vec![0u8; buf_frames * bytes_per_frame];
|
||||
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
match pcm.state() {
|
||||
State::XRun => {
|
||||
warn_throttled!(log_throttler, "xrun", "Audio buffer overrun, recovering");
|
||||
let _ = pcm.prepare();
|
||||
continue;
|
||||
}
|
||||
State::Suspended => {
|
||||
warn_throttled!(
|
||||
log_throttler,
|
||||
"suspended",
|
||||
"Audio device suspended, recovering"
|
||||
);
|
||||
let _ = pcm.resume();
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// io_bytes: USB capture often lacks mmap (io_checked requires it).
|
||||
let io: IO<u8> = pcm.io_bytes();
|
||||
|
||||
match io.readi(&mut buffer) {
|
||||
Ok(frames_read) => {
|
||||
if frames_read == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let byte_count = frames_read * config.channels as usize * 2;
|
||||
|
||||
let seq = sequence.fetch_add(1, Ordering::Relaxed);
|
||||
let frame = AudioFrame::new_interleaved(
|
||||
Bytes::copy_from_slice(&buffer[..byte_count]),
|
||||
config.channels,
|
||||
48_000,
|
||||
seq,
|
||||
);
|
||||
|
||||
if frame_tx.receiver_count() > 0 {
|
||||
if let Err(e) = frame_tx.send(frame) {
|
||||
debug!("No audio receivers: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let desc = e.to_string();
|
||||
if is_device_lost_error(&desc) {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Audio device lost while reading {}: {}",
|
||||
config.device_name, e
|
||||
)));
|
||||
} else if desc.contains("EPIPE") || desc.contains("Broken pipe") {
|
||||
warn_throttled!(log_throttler, "buffer_overrun", "Audio buffer overrun");
|
||||
let _ = pcm.prepare();
|
||||
} else {
|
||||
error_throttled!(log_throttler, "read_error", "Audio read error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Audio capture stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_device_lost_error(desc: &str) -> bool {
|
||||
desc.contains("No such device")
|
||||
|| desc.contains("ENODEV")
|
||||
|| desc.contains("ENXIO")
|
||||
|| desc.contains("ESHUTDOWN")
|
||||
}
|
||||
516
src/audio/capture_windows.rs
Normal file
516
src/audio/capture_windows.rs
Normal file
@@ -0,0 +1,516 @@
|
||||
use bytes::Bytes;
|
||||
use cpal::traits::{DeviceTrait, StreamTrait};
|
||||
use cpal::{BufferSize, SampleFormat, StreamConfig};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::{broadcast, watch, Mutex};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::audio::device::{find_wasapi_device, AudioDeviceInfo};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::error_throttled;
|
||||
use crate::utils::LogThrottler;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioConfig {
|
||||
pub device_name: String,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u32,
|
||||
pub frame_size: u32,
|
||||
pub buffer_frames: u32,
|
||||
pub period_frames: u32,
|
||||
}
|
||||
|
||||
impl Default for AudioConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_name: String::new(),
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
frame_size: 960,
|
||||
buffer_frames: 4096,
|
||||
period_frames: 960,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioConfig {
|
||||
pub fn for_device(device: &AudioDeviceInfo) -> Self {
|
||||
Self {
|
||||
device_name: device.name.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bytes_per_sample(&self) -> u32 {
|
||||
2 * self.channels
|
||||
}
|
||||
|
||||
pub fn bytes_per_frame(&self) -> usize {
|
||||
(self.frame_size * self.bytes_per_sample()) as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioFrame {
|
||||
pub data: Bytes,
|
||||
pub sample_rate: u32,
|
||||
pub channels: u32,
|
||||
pub samples: u32,
|
||||
pub sequence: u64,
|
||||
pub timestamp: Instant,
|
||||
}
|
||||
|
||||
impl AudioFrame {
|
||||
pub fn new_interleaved(data: Bytes, channels: u32, sample_rate: u32, sequence: u64) -> Self {
|
||||
let bps = 2 * channels;
|
||||
Self {
|
||||
samples: data.len() as u32 / bps,
|
||||
data,
|
||||
sample_rate,
|
||||
channels,
|
||||
sequence,
|
||||
timestamp: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CaptureState {
|
||||
Stopped,
|
||||
Running,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct AudioCapturer {
|
||||
config: AudioConfig,
|
||||
state: Arc<watch::Sender<CaptureState>>,
|
||||
state_rx: watch::Receiver<CaptureState>,
|
||||
frame_tx: broadcast::Sender<AudioFrame>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
sequence: Arc<AtomicU64>,
|
||||
capture_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
log_throttler: LogThrottler,
|
||||
}
|
||||
|
||||
impl AudioCapturer {
|
||||
pub fn new(config: AudioConfig) -> Self {
|
||||
let (state_tx, state_rx) = watch::channel(CaptureState::Stopped);
|
||||
let (frame_tx, _) = broadcast::channel(16);
|
||||
|
||||
Self {
|
||||
config,
|
||||
state: Arc::new(state_tx),
|
||||
state_rx,
|
||||
frame_tx,
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
sequence: Arc::new(AtomicU64::new(0)),
|
||||
capture_handle: Mutex::new(None),
|
||||
log_throttler: LogThrottler::with_secs(5),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> CaptureState {
|
||||
*self.state_rx.borrow()
|
||||
}
|
||||
|
||||
pub fn state_watch(&self) -> watch::Receiver<CaptureState> {
|
||||
self.state_rx.clone()
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame> {
|
||||
self.frame_tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
if self.state() == CaptureState::Running {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Starting WASAPI 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 frame_tx = self.frame_tx.clone();
|
||||
let stop_flag = self.stop_flag.clone();
|
||||
let sequence = self.sequence.clone();
|
||||
let log_throttler = self.log_throttler.clone();
|
||||
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
let result = run_capture(
|
||||
&config,
|
||||
&state,
|
||||
&frame_tx,
|
||||
&stop_flag,
|
||||
&sequence,
|
||||
&log_throttler,
|
||||
);
|
||||
|
||||
if let Err(e) = result {
|
||||
error_throttled!(
|
||||
log_throttler,
|
||||
"capture_error",
|
||||
"WASAPI audio capture error: {}",
|
||||
e
|
||||
);
|
||||
let _ = state.send(CaptureState::Error);
|
||||
} else {
|
||||
let _ = state.send(CaptureState::Stopped);
|
||||
}
|
||||
});
|
||||
|
||||
*self.capture_handle.lock().await = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> Result<()> {
|
||||
info!("Stopping WASAPI 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(())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.state() == CaptureState::Running
|
||||
}
|
||||
}
|
||||
|
||||
fn run_capture(
|
||||
config: &AudioConfig,
|
||||
state: &watch::Sender<CaptureState>,
|
||||
frame_tx: &broadcast::Sender<AudioFrame>,
|
||||
stop_flag: &AtomicBool,
|
||||
sequence: &AtomicU64,
|
||||
log_throttler: &LogThrottler,
|
||||
) -> Result<()> {
|
||||
let device = find_wasapi_device(&config.device_name)?;
|
||||
let device_label = device_label(&device);
|
||||
|
||||
let supported = select_input_config(&device, config)?;
|
||||
let sample_format = supported.sample_format();
|
||||
let input_channels = supported.channels() as u32;
|
||||
let input_rate = supported.sample_rate();
|
||||
let stream_config = StreamConfig {
|
||||
channels: supported.channels(),
|
||||
sample_rate: supported.sample_rate(),
|
||||
buffer_size: BufferSize::Fixed(config.period_frames.max(128)),
|
||||
};
|
||||
|
||||
debug!(
|
||||
"WASAPI capture selected: {} @ {}Hz {}ch {:?}",
|
||||
device_label, input_rate, input_channels, sample_format
|
||||
);
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<Vec<i16>>(8);
|
||||
let (err_tx, err_rx) = mpsc::sync_channel::<String>(1);
|
||||
let callback_stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let stream = match sample_format {
|
||||
SampleFormat::F32 => build_stream::<f32>(
|
||||
&device,
|
||||
&stream_config,
|
||||
input_channels,
|
||||
input_rate,
|
||||
tx.clone(),
|
||||
err_tx.clone(),
|
||||
callback_stop.clone(),
|
||||
),
|
||||
SampleFormat::I16 => build_stream::<i16>(
|
||||
&device,
|
||||
&stream_config,
|
||||
input_channels,
|
||||
input_rate,
|
||||
tx.clone(),
|
||||
err_tx.clone(),
|
||||
callback_stop.clone(),
|
||||
),
|
||||
SampleFormat::U16 => build_stream::<u16>(
|
||||
&device,
|
||||
&stream_config,
|
||||
input_channels,
|
||||
input_rate,
|
||||
tx.clone(),
|
||||
err_tx.clone(),
|
||||
callback_stop.clone(),
|
||||
),
|
||||
other => {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"Unsupported WASAPI sample format: {:?}",
|
||||
other
|
||||
)));
|
||||
}
|
||||
}?;
|
||||
|
||||
stream
|
||||
.play()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to start WASAPI stream: {}", e)))?;
|
||||
|
||||
let _ = state.send(CaptureState::Running);
|
||||
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
if let Ok(err) = err_rx.try_recv() {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"WASAPI stream error for {}: {}",
|
||||
device_label, err
|
||||
)));
|
||||
}
|
||||
|
||||
match rx.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(samples) => {
|
||||
if samples.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let seq = sequence.fetch_add(1, Ordering::Relaxed);
|
||||
let frame = AudioFrame::new_interleaved(
|
||||
Bytes::copy_from_slice(bytemuck::cast_slice(&samples)),
|
||||
2,
|
||||
48_000,
|
||||
seq,
|
||||
);
|
||||
if frame_tx.receiver_count() > 0 {
|
||||
if let Err(e) = frame_tx.send(frame) {
|
||||
debug!("No audio receivers: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
return Err(AppError::AudioError(format!(
|
||||
"WASAPI capture callback stopped for {}",
|
||||
device_label
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback_stop.store(true, Ordering::SeqCst);
|
||||
drop(stream);
|
||||
|
||||
info!("WASAPI audio capture stopped");
|
||||
let _ = log_throttler;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn select_input_config(
|
||||
device: &cpal::Device,
|
||||
config: &AudioConfig,
|
||||
) -> Result<cpal::SupportedStreamConfig> {
|
||||
let requested_rate = config.sample_rate;
|
||||
let mut fallback = None;
|
||||
|
||||
let configs = device.supported_input_configs().map_err(|e| {
|
||||
AppError::AudioError(format!("Failed to query WASAPI input configs: {}", e))
|
||||
})?;
|
||||
|
||||
for range in configs {
|
||||
let sample_format = range.sample_format();
|
||||
if !matches!(
|
||||
sample_format,
|
||||
SampleFormat::F32 | SampleFormat::I16 | SampleFormat::U16
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fallback
|
||||
.as_ref()
|
||||
.is_none_or(|best: &cpal::SupportedStreamConfigRange| {
|
||||
range.cmp_default_heuristics(best).is_gt()
|
||||
})
|
||||
{
|
||||
fallback = Some(range);
|
||||
}
|
||||
|
||||
if range.channels() >= 2
|
||||
&& range.min_sample_rate() <= requested_rate
|
||||
&& requested_rate <= range.max_sample_rate()
|
||||
{
|
||||
return Ok(range.with_sample_rate(requested_rate));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(range) = fallback {
|
||||
let rate = if range.min_sample_rate() <= requested_rate
|
||||
&& requested_rate <= range.max_sample_rate()
|
||||
{
|
||||
requested_rate
|
||||
} else {
|
||||
range.with_max_sample_rate().sample_rate()
|
||||
};
|
||||
return Ok(range.with_sample_rate(rate));
|
||||
}
|
||||
|
||||
device.default_input_config().map_err(|e| {
|
||||
AppError::AudioError(format!(
|
||||
"No supported WASAPI input format found, and default config failed: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn build_stream<T>(
|
||||
device: &cpal::Device,
|
||||
config: &StreamConfig,
|
||||
input_channels: u32,
|
||||
input_rate: u32,
|
||||
tx: mpsc::SyncSender<Vec<i16>>,
|
||||
err_tx: mpsc::SyncSender<String>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
) -> Result<cpal::Stream>
|
||||
where
|
||||
T: cpal::SizedSample + SampleToI16,
|
||||
{
|
||||
let mut converter = PcmConverter::new(input_channels, input_rate, 2, 48_000);
|
||||
let data_tx = tx.clone();
|
||||
let stream = device
|
||||
.build_input_stream(
|
||||
config,
|
||||
move |data: &[T], _| {
|
||||
if stop_flag.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let pcm = converter.convert(data);
|
||||
if !pcm.is_empty() {
|
||||
let _ = data_tx.try_send(pcm);
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
let _ = err_tx.try_send(err.to_string());
|
||||
},
|
||||
Some(Duration::from_secs(2)),
|
||||
)
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to build WASAPI input stream: {}", e)))?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
trait SampleToI16: Copy + Send + 'static {
|
||||
fn to_i16_sample(self) -> i16;
|
||||
}
|
||||
|
||||
impl SampleToI16 for i16 {
|
||||
fn to_i16_sample(self) -> i16 {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleToI16 for u16 {
|
||||
fn to_i16_sample(self) -> i16 {
|
||||
(self as i32 - 32768).clamp(i16::MIN as i32, i16::MAX as i32) as i16
|
||||
}
|
||||
}
|
||||
|
||||
impl SampleToI16 for f32 {
|
||||
fn to_i16_sample(self) -> i16 {
|
||||
(self.clamp(-1.0, 1.0) * i16::MAX as f32).round() as i16
|
||||
}
|
||||
}
|
||||
|
||||
struct PcmConverter {
|
||||
input_channels: usize,
|
||||
input_rate: u32,
|
||||
output_channels: usize,
|
||||
output_rate: u32,
|
||||
input_position: u64,
|
||||
next_output_position: u64,
|
||||
}
|
||||
|
||||
impl PcmConverter {
|
||||
fn new(input_channels: u32, input_rate: u32, output_channels: u32, output_rate: u32) -> Self {
|
||||
Self {
|
||||
input_channels: input_channels.max(1) as usize,
|
||||
input_rate: input_rate.max(1),
|
||||
output_channels: output_channels.max(1) as usize,
|
||||
output_rate: output_rate.max(1),
|
||||
input_position: 0,
|
||||
next_output_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert<T: SampleToI16>(&mut self, input: &[T]) -> Vec<i16> {
|
||||
let frames = input.len() / self.input_channels;
|
||||
if frames == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if self.input_rate == self.output_rate {
|
||||
self.input_position = self.input_position.saturating_add(frames as u64);
|
||||
return self.convert_channels(input, frames);
|
||||
}
|
||||
|
||||
let start = self.input_position;
|
||||
let end = start.saturating_add(frames as u64);
|
||||
let mut out = Vec::with_capacity(
|
||||
((frames as u64 * self.output_rate as u64 / self.input_rate as u64 + 2) as usize)
|
||||
* self.output_channels,
|
||||
);
|
||||
|
||||
while self.source_position_for_output(self.next_output_position) < end {
|
||||
let src = self.source_position_for_output(self.next_output_position);
|
||||
if src >= start {
|
||||
let local = (src - start) as usize;
|
||||
self.push_frame(input, local.min(frames - 1), &mut out);
|
||||
}
|
||||
self.next_output_position = self.next_output_position.saturating_add(1);
|
||||
}
|
||||
|
||||
self.input_position = end;
|
||||
out
|
||||
}
|
||||
|
||||
fn source_position_for_output(&self, output_position: u64) -> u64 {
|
||||
output_position.saturating_mul(self.input_rate as u64) / self.output_rate as u64
|
||||
}
|
||||
|
||||
fn convert_channels<T: SampleToI16>(&self, input: &[T], frames: usize) -> Vec<i16> {
|
||||
let mut out = Vec::with_capacity(frames * self.output_channels);
|
||||
for frame in 0..frames {
|
||||
self.push_frame(input, frame, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn push_frame<T: SampleToI16>(&self, input: &[T], frame: usize, out: &mut Vec<i16>) {
|
||||
let base = frame * self.input_channels;
|
||||
let left = input
|
||||
.get(base)
|
||||
.copied()
|
||||
.map(SampleToI16::to_i16_sample)
|
||||
.unwrap_or(0);
|
||||
let right = if self.input_channels > 1 {
|
||||
input
|
||||
.get(base + 1)
|
||||
.copied()
|
||||
.map(SampleToI16::to_i16_sample)
|
||||
.unwrap_or(left)
|
||||
} else {
|
||||
left
|
||||
};
|
||||
|
||||
out.push(left);
|
||||
if self.output_channels > 1 {
|
||||
out.push(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn device_label(device: &cpal::Device) -> String {
|
||||
device
|
||||
.description()
|
||||
.map(|desc| desc.to_string())
|
||||
.or_else(|_| {
|
||||
#[allow(deprecated)]
|
||||
device.name()
|
||||
})
|
||||
.unwrap_or_else(|_| "Unknown WASAPI capture device".to_string())
|
||||
}
|
||||
@@ -1,107 +1,21 @@
|
||||
//! Device selection, quality presets, streaming.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use super::capture::AudioConfig;
|
||||
use super::device::{
|
||||
enumerate_audio_devices, enumerate_audio_devices_with_current, find_best_audio_device,
|
||||
AudioDeviceInfo,
|
||||
};
|
||||
use super::encoder::{OpusConfig, OpusFrame};
|
||||
use super::device::{enumerate_audio_devices_with_current, find_best_audio_device, AudioDeviceInfo};
|
||||
use super::encoder::OpusFrame;
|
||||
use super::monitor::AudioHealthMonitor;
|
||||
use super::streamer::{AudioStreamState, AudioStreamer, AudioStreamerConfig};
|
||||
use super::streamer::{AudioStreamer, AudioStreamerConfig};
|
||||
use super::recovery;
|
||||
use super::types::{AudioControllerConfig, AudioQuality, AudioStatus};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::events::{EventBus, StreamDeviceLostKind, SystemEvent};
|
||||
use crate::events::EventBus;
|
||||
|
||||
const AUDIO_RECOVERY_RETRY_DELAY: Duration = Duration::from_secs(1);
|
||||
|
||||
type AudioRecoveredCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AudioQuality {
|
||||
Voice,
|
||||
#[default]
|
||||
Balanced,
|
||||
High,
|
||||
}
|
||||
|
||||
impl AudioQuality {
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
match self {
|
||||
AudioQuality::Voice => 32000,
|
||||
AudioQuality::Balanced => 64000,
|
||||
AudioQuality::High => 128000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_opus_config(&self) -> OpusConfig {
|
||||
match self {
|
||||
AudioQuality::Voice => OpusConfig::voice(),
|
||||
AudioQuality::Balanced => OpusConfig::default(),
|
||||
AudioQuality::High => OpusConfig::music(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AudioQuality {
|
||||
type Err = AppError;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"voice" => Ok(Self::Voice),
|
||||
"balanced" => Ok(Self::Balanced),
|
||||
"high" => Ok(Self::High),
|
||||
_ => Err(AppError::BadRequest(format!(
|
||||
"invalid audio quality {:?} (expected voice, balanced, or high)",
|
||||
s.trim()
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioControllerConfig {
|
||||
pub enabled: bool,
|
||||
pub device: String,
|
||||
pub quality: AudioQuality,
|
||||
}
|
||||
|
||||
impl Default for AudioControllerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
device: String::new(),
|
||||
quality: AudioQuality::Balanced,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AudioStatus {
|
||||
pub enabled: bool,
|
||||
pub streaming: bool,
|
||||
pub device: Option<String>,
|
||||
pub quality: AudioQuality,
|
||||
pub subscriber_count: usize,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
pub(super) type AudioRecoveredCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
|
||||
pub struct AudioController {
|
||||
config: Arc<RwLock<AudioControllerConfig>>,
|
||||
@@ -135,274 +49,12 @@ impl AudioController {
|
||||
}
|
||||
|
||||
async fn mark_device_info_dirty(&self) {
|
||||
if let Some(ref bus) = *self.event_bus.read().await {
|
||||
if let Some(bus) = self.event_bus.read().await.as_ref() {
|
||||
bus.mark_device_info_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_state(
|
||||
event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
state: &str,
|
||||
device: Option<String>,
|
||||
reason: Option<&str>,
|
||||
next_retry_ms: Option<u64>,
|
||||
) {
|
||||
if let Some(ref bus) = *event_bus.read().await {
|
||||
bus.publish(SystemEvent::StreamStateChanged {
|
||||
state: state.to_string(),
|
||||
device,
|
||||
reason: reason.map(str::to_string),
|
||||
next_retry_ms,
|
||||
});
|
||||
bus.mark_device_info_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_device_lost(
|
||||
event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
device: &str,
|
||||
reason: &str,
|
||||
) {
|
||||
if let Some(ref bus) = *event_bus.read().await {
|
||||
bus.publish(SystemEvent::StreamDeviceLost {
|
||||
kind: StreamDeviceLostKind::Audio,
|
||||
device: device.to_string(),
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_reconnecting(
|
||||
event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
device: &str,
|
||||
attempt: u32,
|
||||
) {
|
||||
if let Some(ref bus) = *event_bus.read().await {
|
||||
bus.publish(SystemEvent::StreamReconnecting {
|
||||
device: device.to_string(),
|
||||
attempt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_recovered(event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>, device: &str) {
|
||||
if let Some(ref bus) = *event_bus.read().await {
|
||||
bus.publish(SystemEvent::StreamRecovered {
|
||||
device: device.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn select_recovery_device(
|
||||
devices: &[AudioDeviceInfo],
|
||||
preferred: &str,
|
||||
) -> Option<AudioDeviceInfo> {
|
||||
if !preferred.trim().is_empty() {
|
||||
if let Some(device) = devices.iter().find(|d| d.name == preferred) {
|
||||
return Some(device.clone());
|
||||
}
|
||||
}
|
||||
|
||||
devices
|
||||
.iter()
|
||||
.find(|d| d.is_hdmi && d.sample_rates.contains(&48_000) && d.channels.contains(&2))
|
||||
.or_else(|| {
|
||||
devices
|
||||
.iter()
|
||||
.find(|d| d.sample_rates.contains(&48_000) && d.channels.contains(&2))
|
||||
})
|
||||
.or_else(|| devices.first())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn spawn_stream_monitor_from_parts(
|
||||
config: Arc<RwLock<AudioControllerConfig>>,
|
||||
streamer_slot: Arc<RwLock<Option<Arc<AudioStreamer>>>>,
|
||||
event_bus: Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
monitor: Arc<AudioHealthMonitor>,
|
||||
recovery_in_progress: Arc<AtomicBool>,
|
||||
recovered_callback: Arc<RwLock<Option<AudioRecoveredCallback>>>,
|
||||
streamer: Arc<AudioStreamer>,
|
||||
device: String,
|
||||
) {
|
||||
let mut state_rx = streamer.state_watch();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if state_rx.changed().await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
if *state_rx.borrow() != AudioStreamState::Error {
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let current = streamer_slot.read().await;
|
||||
if !current
|
||||
.as_ref()
|
||||
.is_some_and(|current| Arc::ptr_eq(current, &streamer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let reason = format!("Audio device lost: {}", device);
|
||||
monitor.report_error(&reason, "device_lost").await;
|
||||
Self::spawn_recovery_task_from_parts(
|
||||
config,
|
||||
streamer_slot,
|
||||
event_bus,
|
||||
monitor,
|
||||
recovery_in_progress,
|
||||
recovered_callback,
|
||||
device,
|
||||
reason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_recovery_task_from_parts(
|
||||
config: Arc<RwLock<AudioControllerConfig>>,
|
||||
streamer_slot: Arc<RwLock<Option<Arc<AudioStreamer>>>>,
|
||||
event_bus: Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
monitor: Arc<AudioHealthMonitor>,
|
||||
recovery_in_progress: Arc<AtomicBool>,
|
||||
recovered_callback: Arc<RwLock<Option<AudioRecoveredCallback>>>,
|
||||
lost_device: String,
|
||||
reason: String,
|
||||
) {
|
||||
if recovery_in_progress.swap(true, Ordering::SeqCst) {
|
||||
debug!("Audio recovery already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
warn!("Audio recovery started for {}: {}", lost_device, reason);
|
||||
Self::publish_device_lost(&event_bus, &lost_device, &reason).await;
|
||||
Self::publish_state(
|
||||
&event_bus,
|
||||
"device_lost",
|
||||
Some(lost_device.clone()),
|
||||
Some("audio_device_lost"),
|
||||
Some(AUDIO_RECOVERY_RETRY_DELAY.as_millis() as u64),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut attempt = 0u32;
|
||||
|
||||
loop {
|
||||
if !recovery_in_progress.load(Ordering::SeqCst) {
|
||||
debug!("Audio recovery canceled");
|
||||
return;
|
||||
}
|
||||
|
||||
if streamer_slot
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.is_running())
|
||||
{
|
||||
recovery_in_progress.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
|
||||
let cfg = config.read().await.clone();
|
||||
if !cfg.enabled {
|
||||
recovery_in_progress.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
|
||||
attempt = attempt.saturating_add(1);
|
||||
Self::publish_reconnecting(&event_bus, &lost_device, attempt).await;
|
||||
Self::publish_state(
|
||||
&event_bus,
|
||||
"device_lost",
|
||||
Some(lost_device.clone()),
|
||||
Some("audio_reconnecting"),
|
||||
Some(AUDIO_RECOVERY_RETRY_DELAY.as_millis() as u64),
|
||||
)
|
||||
.await;
|
||||
|
||||
tokio::time::sleep(AUDIO_RECOVERY_RETRY_DELAY).await;
|
||||
|
||||
let devices = match enumerate_audio_devices() {
|
||||
Ok(devices) => devices,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Audio recovery enumerate failed (attempt {}): {}",
|
||||
attempt, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(device) = Self::select_recovery_device(&devices, &cfg.device) else {
|
||||
debug!("No audio devices found during recovery attempt {}", attempt);
|
||||
continue;
|
||||
};
|
||||
|
||||
let streamer_config = AudioStreamerConfig {
|
||||
capture: AudioConfig {
|
||||
device_name: device.name.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
opus: cfg.quality.to_opus_config(),
|
||||
};
|
||||
let new_streamer = Arc::new(AudioStreamer::with_config(streamer_config));
|
||||
|
||||
match new_streamer.start().await {
|
||||
Ok(()) => {
|
||||
{
|
||||
let mut cfg = config.write().await;
|
||||
cfg.device = device.name.clone();
|
||||
}
|
||||
*streamer_slot.write().await = Some(new_streamer.clone());
|
||||
monitor.report_recovered().await;
|
||||
Self::publish_recovered(&event_bus, &device.name).await;
|
||||
if let Some(callback) = recovered_callback.read().await.clone() {
|
||||
callback();
|
||||
}
|
||||
Self::publish_state(
|
||||
&event_bus,
|
||||
"streaming",
|
||||
Some(device.name.clone()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
recovery_in_progress.store(false, Ordering::SeqCst);
|
||||
info!(
|
||||
"Audio device recovered with {} after {} attempts",
|
||||
device.name, attempt
|
||||
);
|
||||
Self::spawn_stream_monitor_from_parts(
|
||||
config,
|
||||
streamer_slot,
|
||||
event_bus,
|
||||
monitor,
|
||||
recovery_in_progress,
|
||||
recovered_callback,
|
||||
new_streamer,
|
||||
device.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Audio recovery start failed with {} (attempt {}): {}",
|
||||
device.name, attempt, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_recovery_task(&self, lost_device: String, reason: String) {
|
||||
Self::spawn_recovery_task_from_parts(
|
||||
recovery::spawn_recovery_task(
|
||||
self.config.clone(),
|
||||
self.streamer.clone(),
|
||||
self.event_bus.clone(),
|
||||
@@ -415,7 +67,7 @@ impl AudioController {
|
||||
}
|
||||
|
||||
fn spawn_stream_monitor(&self, streamer: Arc<AudioStreamer>, device: String) {
|
||||
Self::spawn_stream_monitor_from_parts(
|
||||
recovery::spawn_stream_monitor(
|
||||
self.config.clone(),
|
||||
self.streamer.clone(),
|
||||
self.event_bus.clone(),
|
||||
@@ -477,7 +129,7 @@ impl AudioController {
|
||||
config.quality = quality;
|
||||
}
|
||||
|
||||
if let Some(ref streamer) = *self.streamer.read().await {
|
||||
if let Some(streamer) = self.streamer.read().await.as_ref() {
|
||||
streamer.set_bitrate(quality.bitrate()).await?;
|
||||
}
|
||||
|
||||
@@ -578,11 +230,11 @@ impl AudioController {
|
||||
}
|
||||
|
||||
pub async fn is_streaming(&self) -> bool {
|
||||
if let Some(ref streamer) = *self.streamer.read().await {
|
||||
streamer.is_running()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
self.streamer
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.is_some_and(|streamer| streamer.is_running())
|
||||
}
|
||||
|
||||
pub async fn status(&self) -> AudioStatus {
|
||||
|
||||
@@ -1,201 +1,9 @@
|
||||
use alsa::pcm::HwParams;
|
||||
use alsa::{Direction, PCM};
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, info, warn};
|
||||
#[cfg(unix)]
|
||||
#[path = "device_linux.rs"]
|
||||
mod imp;
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
#[cfg(windows)]
|
||||
#[path = "device_windows.rs"]
|
||||
mod imp;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AudioDeviceInfo {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub card_index: i32,
|
||||
pub device_index: i32,
|
||||
pub sample_rates: Vec<u32>,
|
||||
pub channels: Vec<u32>,
|
||||
pub is_capture: bool,
|
||||
pub is_hdmi: bool,
|
||||
pub usb_bus: Option<String>,
|
||||
}
|
||||
|
||||
fn get_usb_bus_info(card_index: i32) -> Option<String> {
|
||||
if card_index < 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let device_path = format!("/sys/class/sound/card{}/device", card_index);
|
||||
let link_target = std::fs::read_link(&device_path).ok()?;
|
||||
let link_str = link_target.to_string_lossy();
|
||||
|
||||
for component in link_str.split('/') {
|
||||
if component.contains('-') && !component.contains(':') {
|
||||
if component
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Some(component.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn enumerate_audio_devices() -> Result<Vec<AudioDeviceInfo>> {
|
||||
enumerate_audio_devices_with_current(None)
|
||||
}
|
||||
|
||||
pub fn enumerate_audio_devices_with_current(
|
||||
current_device: Option<&str>,
|
||||
) -> Result<Vec<AudioDeviceInfo>> {
|
||||
let mut devices = Vec::new();
|
||||
|
||||
let cards = alsa::card::Iter::new();
|
||||
|
||||
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);
|
||||
|
||||
let long_lower = card_longname.to_lowercase();
|
||||
let is_hdmi = long_lower.contains("hdmi")
|
||||
|| long_lower.contains("capture")
|
||||
|| long_lower.contains("usb");
|
||||
|
||||
let usb_bus = get_usb_bus_info(card_index);
|
||||
|
||||
for device_index in 0..8 {
|
||||
let device_name = format!("hw:{},{}", card_index, device_index);
|
||||
let is_current_device = current_device == Some(device_name.as_str());
|
||||
|
||||
let mut push_info =
|
||||
|sample_rates: Vec<u32>, channels: Vec<u32>, description: String| {
|
||||
devices.push(AudioDeviceInfo {
|
||||
name: device_name.clone(),
|
||||
description,
|
||||
card_index,
|
||||
device_index,
|
||||
sample_rates,
|
||||
channels,
|
||||
is_capture: true,
|
||||
is_hdmi,
|
||||
usb_bus: usb_bus.clone(),
|
||||
});
|
||||
};
|
||||
|
||||
match PCM::new(&device_name, Direction::Capture, false) {
|
||||
Ok(pcm) => {
|
||||
let (sample_rates, channels) = query_device_caps(&pcm);
|
||||
|
||||
if !sample_rates.is_empty() && !channels.is_empty() {
|
||||
push_info(
|
||||
sample_rates,
|
||||
channels,
|
||||
format!("{} - Device {}", card_longname, device_index),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if is_current_device {
|
||||
debug!(
|
||||
"Device {} is busy (in use by us), adding with default caps",
|
||||
device_name
|
||||
);
|
||||
push_info(
|
||||
vec![44100, 48000],
|
||||
vec![2],
|
||||
format!("{} - Device {} (in use)", card_longname, device_index),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Found {} audio capture devices", devices.len());
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn query_device_caps(pcm: &PCM) -> (Vec<u32>, Vec<u32>) {
|
||||
let hwp = match HwParams::any(pcm) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return (vec![], vec![]),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut first_48k_stereo: Option<&AudioDeviceInfo> = None;
|
||||
for device in &devices {
|
||||
if !device.sample_rates.contains(&48000) || !device.channels.contains(&2) {
|
||||
continue;
|
||||
}
|
||||
if device.is_hdmi {
|
||||
info!("Selected HDMI audio device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
if first_48k_stereo.is_none() {
|
||||
first_48k_stereo = Some(device);
|
||||
}
|
||||
}
|
||||
if let Some(device) = first_48k_stereo {
|
||||
info!("Selected audio device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
|
||||
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() {
|
||||
let result = enumerate_audio_devices();
|
||||
println!("Audio devices: {:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
pub use imp::*;
|
||||
|
||||
201
src/audio/device_linux.rs
Normal file
201
src/audio/device_linux.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use alsa::pcm::HwParams;
|
||||
use alsa::{Direction, PCM};
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AudioDeviceInfo {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub card_index: i32,
|
||||
pub device_index: i32,
|
||||
pub sample_rates: Vec<u32>,
|
||||
pub channels: Vec<u32>,
|
||||
pub is_capture: bool,
|
||||
pub is_hdmi: bool,
|
||||
pub usb_bus: Option<String>,
|
||||
}
|
||||
|
||||
fn get_usb_bus_info(card_index: i32) -> Option<String> {
|
||||
if card_index < 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let device_path = format!("/sys/class/sound/card{}/device", card_index);
|
||||
let link_target = std::fs::read_link(&device_path).ok()?;
|
||||
let link_str = link_target.to_string_lossy();
|
||||
|
||||
for component in link_str.split('/') {
|
||||
if component.contains('-') && !component.contains(':') {
|
||||
if component
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Some(component.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn enumerate_audio_devices() -> Result<Vec<AudioDeviceInfo>> {
|
||||
enumerate_audio_devices_with_current(None)
|
||||
}
|
||||
|
||||
pub fn enumerate_audio_devices_with_current(
|
||||
current_device: Option<&str>,
|
||||
) -> Result<Vec<AudioDeviceInfo>> {
|
||||
let mut devices = Vec::new();
|
||||
|
||||
let cards = alsa::card::Iter::new();
|
||||
|
||||
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);
|
||||
|
||||
let long_lower = card_longname.to_lowercase();
|
||||
let is_hdmi = long_lower.contains("hdmi")
|
||||
|| long_lower.contains("capture")
|
||||
|| long_lower.contains("usb");
|
||||
|
||||
let usb_bus = get_usb_bus_info(card_index);
|
||||
|
||||
for device_index in 0..8 {
|
||||
let device_name = format!("hw:{},{}", card_index, device_index);
|
||||
let is_current_device = current_device == Some(device_name.as_str());
|
||||
|
||||
let mut push_info =
|
||||
|sample_rates: Vec<u32>, channels: Vec<u32>, description: String| {
|
||||
devices.push(AudioDeviceInfo {
|
||||
name: device_name.clone(),
|
||||
description,
|
||||
card_index,
|
||||
device_index,
|
||||
sample_rates,
|
||||
channels,
|
||||
is_capture: true,
|
||||
is_hdmi,
|
||||
usb_bus: usb_bus.clone(),
|
||||
});
|
||||
};
|
||||
|
||||
match PCM::new(&device_name, Direction::Capture, false) {
|
||||
Ok(pcm) => {
|
||||
let (sample_rates, channels) = query_device_caps(&pcm);
|
||||
|
||||
if !sample_rates.is_empty() && !channels.is_empty() {
|
||||
push_info(
|
||||
sample_rates,
|
||||
channels,
|
||||
format!("{} - Device {}", card_longname, device_index),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
if is_current_device {
|
||||
debug!(
|
||||
"Device {} is busy (in use by us), adding with default caps",
|
||||
device_name
|
||||
);
|
||||
push_info(
|
||||
vec![44100, 48000],
|
||||
vec![2],
|
||||
format!("{} - Device {} (in use)", card_longname, device_index),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Found {} audio capture devices", devices.len());
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn query_device_caps(pcm: &PCM) -> (Vec<u32>, Vec<u32>) {
|
||||
let hwp = match HwParams::any(pcm) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return (vec![], vec![]),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut first_48k_stereo: Option<&AudioDeviceInfo> = None;
|
||||
for device in &devices {
|
||||
if !device.sample_rates.contains(&48000) || !device.channels.contains(&2) {
|
||||
continue;
|
||||
}
|
||||
if device.is_hdmi {
|
||||
info!("Selected HDMI audio device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
if first_48k_stereo.is_none() {
|
||||
first_48k_stereo = Some(device);
|
||||
}
|
||||
}
|
||||
if let Some(device) = first_48k_stereo {
|
||||
info!("Selected audio device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
|
||||
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() {
|
||||
let result = enumerate_audio_devices();
|
||||
println!("Audio devices: {:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
232
src/audio/device_windows.rs
Normal file
232
src/audio/device_windows.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use cpal::DeviceId;
|
||||
use serde::Serialize;
|
||||
use std::str::FromStr;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AudioDeviceInfo {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub card_index: i32,
|
||||
pub device_index: i32,
|
||||
pub sample_rates: Vec<u32>,
|
||||
pub channels: Vec<u32>,
|
||||
pub is_capture: bool,
|
||||
pub is_hdmi: bool,
|
||||
pub usb_bus: Option<String>,
|
||||
}
|
||||
|
||||
pub fn enumerate_audio_devices() -> Result<Vec<AudioDeviceInfo>> {
|
||||
enumerate_audio_devices_with_current(None)
|
||||
}
|
||||
|
||||
pub fn enumerate_audio_devices_with_current(
|
||||
current_device: Option<&str>,
|
||||
) -> Result<Vec<AudioDeviceInfo>> {
|
||||
let host = cpal::default_host();
|
||||
let devices = host
|
||||
.input_devices()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to enumerate WASAPI devices: {}", e)))?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for (index, device) in devices.enumerate() {
|
||||
let labels = device_labels(&device);
|
||||
let id = device
|
||||
.id()
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_else(|_| format!("wasapi-index:{}", index));
|
||||
|
||||
let (sample_rates, channels) = query_device_caps(&device);
|
||||
if sample_rates.is_empty() || channels.is_empty() {
|
||||
debug!(
|
||||
"Skipping WASAPI endpoint without usable input caps: {}",
|
||||
labels.search_text
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_current =
|
||||
current_device == Some(id.as_str()) || current_device == Some(labels.display.as_str());
|
||||
let description = if is_current {
|
||||
format!("{} (in use)", labels.display)
|
||||
} else {
|
||||
labels.display.clone()
|
||||
};
|
||||
|
||||
let lower = labels.search_text.to_lowercase();
|
||||
let is_hdmi = lower.contains("hdmi")
|
||||
|| lower.contains("capture")
|
||||
|| lower.contains("usb")
|
||||
|| lower.contains("digital");
|
||||
|
||||
result.push(AudioDeviceInfo {
|
||||
name: id,
|
||||
description,
|
||||
card_index: index as i32,
|
||||
device_index: 0,
|
||||
sample_rates,
|
||||
channels,
|
||||
is_capture: true,
|
||||
is_hdmi,
|
||||
usb_bus: None,
|
||||
});
|
||||
}
|
||||
|
||||
info!("Found {} WASAPI audio capture devices", result.len());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn query_device_caps(device: &cpal::Device) -> (Vec<u32>, Vec<u32>) {
|
||||
let mut sample_rates = Vec::new();
|
||||
let mut channels = Vec::new();
|
||||
|
||||
if let Ok(configs) = device.supported_input_configs() {
|
||||
for cfg in configs {
|
||||
for rate in [8000, 16000, 22050, 44100, 48000, 96000] {
|
||||
if cfg.min_sample_rate() <= rate
|
||||
&& rate <= cfg.max_sample_rate()
|
||||
&& !sample_rates.contains(&rate)
|
||||
{
|
||||
sample_rates.push(rate);
|
||||
}
|
||||
}
|
||||
|
||||
let ch = cfg.channels() as u32;
|
||||
if !channels.contains(&ch) {
|
||||
channels.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sample_rates.is_empty() || channels.is_empty()) && device.default_input_config().is_ok() {
|
||||
if let Ok(default_cfg) = device.default_input_config() {
|
||||
if !sample_rates.contains(&default_cfg.sample_rate()) {
|
||||
sample_rates.push(default_cfg.sample_rate());
|
||||
}
|
||||
let ch = default_cfg.channels() as u32;
|
||||
if !channels.contains(&ch) {
|
||||
channels.push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sample_rates.sort_unstable();
|
||||
channels.sort_unstable();
|
||||
(sample_rates, channels)
|
||||
}
|
||||
|
||||
struct DeviceLabels {
|
||||
display: String,
|
||||
search_text: String,
|
||||
}
|
||||
|
||||
fn device_labels(device: &cpal::Device) -> DeviceLabels {
|
||||
match device.description() {
|
||||
Ok(desc) => {
|
||||
let formatted = desc.to_string();
|
||||
let display = desc
|
||||
.extended()
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| formatted.clone());
|
||||
let mut parts = vec![formatted, desc.name().to_string(), display.clone()];
|
||||
parts.extend(desc.extended().iter().cloned());
|
||||
|
||||
DeviceLabels {
|
||||
display,
|
||||
search_text: parts.join(" "),
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
#[allow(deprecated)]
|
||||
let display = device
|
||||
.name()
|
||||
.unwrap_or_else(|_| "Unknown WASAPI capture device".to_string());
|
||||
DeviceLabels {
|
||||
display: display.clone(),
|
||||
search_text: display,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_wasapi_device(requested_device: &str) -> Result<cpal::Device> {
|
||||
let host = cpal::default_host();
|
||||
let trimmed = requested_device.trim();
|
||||
|
||||
if trimmed.is_empty()
|
||||
|| trimmed.eq_ignore_ascii_case("auto")
|
||||
|| trimmed.eq_ignore_ascii_case("default")
|
||||
{
|
||||
return host.default_input_device().ok_or_else(|| {
|
||||
AppError::AudioError("No default WASAPI input device found".to_string())
|
||||
});
|
||||
}
|
||||
|
||||
if let Ok(id) = DeviceId::from_str(trimmed) {
|
||||
if let Some(device) = host.device_by_id(&id) {
|
||||
return Ok(device);
|
||||
}
|
||||
}
|
||||
|
||||
let needle = trimmed.to_lowercase();
|
||||
let devices = host
|
||||
.input_devices()
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to enumerate WASAPI devices: {}", e)))?;
|
||||
|
||||
for device in devices {
|
||||
let id_match = device
|
||||
.id()
|
||||
.map(|id| id.to_string() == trimmed)
|
||||
.unwrap_or(false);
|
||||
let labels = device_labels(&device);
|
||||
if id_match || labels.search_text.to_lowercase().contains(&needle) {
|
||||
return Ok(device);
|
||||
}
|
||||
}
|
||||
|
||||
Err(AppError::AudioError(format!(
|
||||
"WASAPI audio device not found: {}",
|
||||
requested_device
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn find_best_audio_device() -> Result<AudioDeviceInfo> {
|
||||
let devices = enumerate_audio_devices()?;
|
||||
|
||||
if devices.is_empty() {
|
||||
return Err(AppError::AudioError(
|
||||
"No WASAPI audio capture devices found".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut first_48k_stereo: Option<&AudioDeviceInfo> = None;
|
||||
for device in &devices {
|
||||
if !device.sample_rates.contains(&48000) || !device.channels.contains(&2) {
|
||||
continue;
|
||||
}
|
||||
if device.is_hdmi {
|
||||
info!("Selected WASAPI capture device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
if first_48k_stereo.is_none() {
|
||||
first_48k_stereo = Some(device);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(device) = first_48k_stereo {
|
||||
info!("Selected WASAPI capture device: {}", device.description);
|
||||
return Ok(device.clone());
|
||||
}
|
||||
|
||||
let device = devices.into_iter().next().unwrap();
|
||||
warn!(
|
||||
"Using fallback WASAPI audio device: {} (will resample if needed)",
|
||||
device.description
|
||||
);
|
||||
Ok(device)
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
//! ALSA capture, Opus encode, device enumeration, streaming, controller, health monitor.
|
||||
//! Platform audio capture, Opus encode, device enumeration, streaming, controller, health monitor.
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
pub mod capture;
|
||||
pub mod controller;
|
||||
#[cfg(any(unix, windows))]
|
||||
pub mod device;
|
||||
#[cfg(any(unix, windows))]
|
||||
pub mod encoder;
|
||||
pub mod monitor;
|
||||
pub mod recovery;
|
||||
pub mod streamer;
|
||||
pub mod types;
|
||||
|
||||
pub use capture::{AudioCapturer, AudioConfig, AudioFrame};
|
||||
pub use controller::{AudioController, AudioControllerConfig, AudioQuality, AudioStatus};
|
||||
pub use controller::AudioController;
|
||||
pub use device::{enumerate_audio_devices, enumerate_audio_devices_with_current, AudioDeviceInfo};
|
||||
pub use encoder::{OpusConfig, OpusEncoder, OpusFrame};
|
||||
pub use monitor::{AudioHealthMonitor, AudioHealthStatus};
|
||||
pub use streamer::{AudioStreamState, AudioStreamer, AudioStreamerConfig};
|
||||
pub use types::{AudioControllerConfig, AudioQuality, AudioStatus};
|
||||
|
||||
320
src/audio/recovery.rs
Normal file
320
src/audio/recovery.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::capture::AudioConfig;
|
||||
use super::device::{enumerate_audio_devices, AudioDeviceInfo};
|
||||
use super::monitor::AudioHealthMonitor;
|
||||
use super::streamer::{AudioStreamState, AudioStreamer, AudioStreamerConfig};
|
||||
use super::types::AudioControllerConfig;
|
||||
use super::controller::AudioRecoveredCallback;
|
||||
use crate::events::{EventBus, StreamDeviceLostKind, SystemEvent};
|
||||
|
||||
const AUDIO_RECOVERY_RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
|
||||
pub(super) fn select_recovery_device(
|
||||
devices: &[AudioDeviceInfo],
|
||||
preferred: &str,
|
||||
) -> Option<AudioDeviceInfo> {
|
||||
if let Some(device) = devices
|
||||
.iter()
|
||||
.find(|d| !preferred.trim().is_empty() && d.name == preferred)
|
||||
{
|
||||
return Some(device.clone());
|
||||
}
|
||||
|
||||
devices
|
||||
.iter()
|
||||
.find(|d| d.is_hdmi && d.sample_rates.contains(&48_000) && d.channels.contains(&2))
|
||||
.or_else(|| {
|
||||
devices
|
||||
.iter()
|
||||
.find(|d| d.sample_rates.contains(&48_000) && d.channels.contains(&2))
|
||||
})
|
||||
.or_else(|| devices.first())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
async fn publish_state(
|
||||
event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
state: &str,
|
||||
device: Option<String>,
|
||||
reason: Option<&str>,
|
||||
next_retry_ms: Option<u64>,
|
||||
) {
|
||||
if let Some(bus) = event_bus.read().await.as_ref() {
|
||||
bus.publish(SystemEvent::StreamStateChanged {
|
||||
state: state.to_string(),
|
||||
device,
|
||||
reason: reason.map(str::to_string),
|
||||
next_retry_ms,
|
||||
});
|
||||
bus.mark_device_info_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_device_lost(
|
||||
event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
device: &str,
|
||||
reason: &str,
|
||||
) {
|
||||
if let Some(bus) = event_bus.read().await.as_ref() {
|
||||
bus.publish(SystemEvent::StreamDeviceLost {
|
||||
kind: StreamDeviceLostKind::Audio,
|
||||
device: device.to_string(),
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_reconnecting(
|
||||
event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
device: &str,
|
||||
attempt: u32,
|
||||
) {
|
||||
if let Some(bus) = event_bus.read().await.as_ref() {
|
||||
bus.publish(SystemEvent::StreamReconnecting {
|
||||
device: device.to_string(),
|
||||
attempt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn publish_recovered(event_bus: &Arc<RwLock<Option<Arc<EventBus>>>>, device: &str) {
|
||||
if let Some(bus) = event_bus.read().await.as_ref() {
|
||||
bus.publish(SystemEvent::StreamRecovered {
|
||||
device: device.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_stream_monitor_from_parts(
|
||||
config: Arc<RwLock<AudioControllerConfig>>,
|
||||
streamer_slot: Arc<RwLock<Option<Arc<AudioStreamer>>>>,
|
||||
event_bus: Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
monitor: Arc<AudioHealthMonitor>,
|
||||
recovery_in_progress: Arc<AtomicBool>,
|
||||
recovered_callback: Arc<RwLock<Option<AudioRecoveredCallback>>>,
|
||||
streamer: Arc<AudioStreamer>,
|
||||
device: String,
|
||||
) {
|
||||
let mut state_rx = streamer.state_watch();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if state_rx.changed().await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
if *state_rx.borrow() != AudioStreamState::Error {
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let current = streamer_slot.read().await;
|
||||
if !current
|
||||
.as_ref()
|
||||
.is_some_and(|current| Arc::ptr_eq(current, &streamer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let reason = format!("Audio device lost: {}", device);
|
||||
monitor.report_error(&reason, "device_lost").await;
|
||||
spawn_recovery_task_from_parts(
|
||||
config,
|
||||
streamer_slot,
|
||||
event_bus,
|
||||
monitor,
|
||||
recovery_in_progress,
|
||||
recovered_callback,
|
||||
device,
|
||||
reason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_recovery_task_from_parts(
|
||||
config: Arc<RwLock<AudioControllerConfig>>,
|
||||
streamer_slot: Arc<RwLock<Option<Arc<AudioStreamer>>>>,
|
||||
event_bus: Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
monitor: Arc<AudioHealthMonitor>,
|
||||
recovery_in_progress: Arc<AtomicBool>,
|
||||
recovered_callback: Arc<RwLock<Option<AudioRecoveredCallback>>>,
|
||||
lost_device: String,
|
||||
reason: String,
|
||||
) {
|
||||
if recovery_in_progress.swap(true, Ordering::SeqCst) {
|
||||
debug!("Audio recovery already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
warn!("Audio recovery started for {}: {}", lost_device, reason);
|
||||
publish_device_lost(&event_bus, &lost_device, &reason).await;
|
||||
publish_state(
|
||||
&event_bus,
|
||||
"device_lost",
|
||||
Some(lost_device.clone()),
|
||||
Some("audio_device_lost"),
|
||||
Some(AUDIO_RECOVERY_RETRY_DELAY.as_millis() as u64),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut attempt = 0u32;
|
||||
|
||||
loop {
|
||||
if !recovery_in_progress.load(Ordering::SeqCst) {
|
||||
debug!("Audio recovery canceled");
|
||||
return;
|
||||
}
|
||||
|
||||
if streamer_slot
|
||||
.read()
|
||||
.await
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.is_running())
|
||||
{
|
||||
recovery_in_progress.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
|
||||
let cfg: AudioControllerConfig = config.read().await.clone();
|
||||
if !cfg.enabled {
|
||||
recovery_in_progress.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
|
||||
attempt = attempt.saturating_add(1);
|
||||
publish_reconnecting(&event_bus, &lost_device, attempt).await;
|
||||
publish_state(
|
||||
&event_bus,
|
||||
"device_lost",
|
||||
Some(lost_device.clone()),
|
||||
Some("audio_reconnecting"),
|
||||
Some(AUDIO_RECOVERY_RETRY_DELAY.as_millis() as u64),
|
||||
)
|
||||
.await;
|
||||
|
||||
tokio::time::sleep(AUDIO_RECOVERY_RETRY_DELAY).await;
|
||||
|
||||
let devices = match enumerate_audio_devices() {
|
||||
Ok(devices) => devices,
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Audio recovery enumerate failed (attempt {}): {}",
|
||||
attempt, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(device) = select_recovery_device(&devices, &cfg.device) else {
|
||||
debug!("No audio devices found during recovery attempt {}", attempt);
|
||||
continue;
|
||||
};
|
||||
|
||||
let streamer_config = AudioStreamerConfig {
|
||||
capture: AudioConfig {
|
||||
device_name: device.name.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
opus: cfg.quality.to_opus_config(),
|
||||
};
|
||||
let new_streamer = Arc::new(AudioStreamer::with_config(streamer_config));
|
||||
|
||||
match new_streamer.start().await {
|
||||
Ok(()) => {
|
||||
{
|
||||
let mut cfg = config.write().await;
|
||||
cfg.device = device.name.clone();
|
||||
}
|
||||
*streamer_slot.write().await = Some(new_streamer.clone());
|
||||
monitor.report_recovered().await;
|
||||
publish_recovered(&event_bus, &device.name).await;
|
||||
if let Some(callback) = recovered_callback.read().await.clone() {
|
||||
callback();
|
||||
}
|
||||
publish_state(
|
||||
&event_bus,
|
||||
"streaming",
|
||||
Some(device.name.clone()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
recovery_in_progress.store(false, Ordering::SeqCst);
|
||||
info!(
|
||||
"Audio device recovered with {} after {} attempts",
|
||||
device.name, attempt
|
||||
);
|
||||
spawn_stream_monitor_from_parts(
|
||||
config,
|
||||
streamer_slot,
|
||||
event_bus,
|
||||
monitor,
|
||||
recovery_in_progress,
|
||||
recovered_callback,
|
||||
new_streamer,
|
||||
device.name,
|
||||
);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Audio recovery start failed with {} (attempt {}): {}",
|
||||
device.name, attempt, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn spawn_stream_monitor(
|
||||
config: Arc<RwLock<AudioControllerConfig>>,
|
||||
streamer_slot: Arc<RwLock<Option<Arc<AudioStreamer>>>>,
|
||||
event_bus: Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
monitor: Arc<AudioHealthMonitor>,
|
||||
recovery_in_progress: Arc<AtomicBool>,
|
||||
recovered_callback: Arc<RwLock<Option<AudioRecoveredCallback>>>,
|
||||
streamer: Arc<AudioStreamer>,
|
||||
device: String,
|
||||
) {
|
||||
spawn_stream_monitor_from_parts(
|
||||
config,
|
||||
streamer_slot,
|
||||
event_bus,
|
||||
monitor,
|
||||
recovery_in_progress,
|
||||
recovered_callback,
|
||||
streamer,
|
||||
device,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn spawn_recovery_task(
|
||||
config: Arc<RwLock<AudioControllerConfig>>,
|
||||
streamer_slot: Arc<RwLock<Option<Arc<AudioStreamer>>>>,
|
||||
event_bus: Arc<RwLock<Option<Arc<EventBus>>>>,
|
||||
monitor: Arc<AudioHealthMonitor>,
|
||||
recovery_in_progress: Arc<AtomicBool>,
|
||||
recovered_callback: Arc<RwLock<Option<AudioRecoveredCallback>>>,
|
||||
lost_device: String,
|
||||
reason: String,
|
||||
) {
|
||||
spawn_recovery_task_from_parts(
|
||||
config,
|
||||
streamer_slot,
|
||||
event_bus,
|
||||
monitor,
|
||||
recovery_in_progress,
|
||||
recovered_callback,
|
||||
lost_device,
|
||||
reason,
|
||||
);
|
||||
}
|
||||
85
src/audio/types.rs
Normal file
85
src/audio/types.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::encoder::OpusConfig;
|
||||
use crate::error::AppError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AudioQuality {
|
||||
Voice,
|
||||
#[default]
|
||||
Balanced,
|
||||
High,
|
||||
}
|
||||
|
||||
impl AudioQuality {
|
||||
pub fn bitrate(&self) -> u32 {
|
||||
match self {
|
||||
AudioQuality::Voice => 32000,
|
||||
AudioQuality::Balanced => 64000,
|
||||
AudioQuality::High => 128000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_opus_config(&self) -> OpusConfig {
|
||||
match self {
|
||||
AudioQuality::Voice => OpusConfig::voice(),
|
||||
AudioQuality::Balanced => OpusConfig::default(),
|
||||
AudioQuality::High => OpusConfig::music(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AudioQuality {
|
||||
type Err = AppError;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"voice" => Ok(Self::Voice),
|
||||
"balanced" => Ok(Self::Balanced),
|
||||
"high" => Ok(Self::High),
|
||||
_ => Err(AppError::BadRequest(format!(
|
||||
"invalid audio quality {:?} (expected voice, balanced, or high)",
|
||||
s.trim()
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioControllerConfig {
|
||||
pub enabled: bool,
|
||||
pub device: String,
|
||||
pub quality: AudioQuality,
|
||||
}
|
||||
|
||||
impl Default for AudioControllerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
device: String::new(),
|
||||
quality: AudioQuality::Balanced,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AudioStatus {
|
||||
pub enabled: bool,
|
||||
pub streaming: bool,
|
||||
pub device: Option<String>,
|
||||
pub quality: AudioQuality,
|
||||
pub subscriber_count: usize,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user