mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-27 16:00:49 +08:00
158 lines
6.1 KiB
Rust
158 lines
6.1 KiB
Rust
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
|
|
use alsa::pcm::{Access, Format, Frames, HwParams, State, IO};
|
|
use alsa::{Direction, ValueOr, PCM};
|
|
use bytes::Bytes;
|
|
use tokio::sync::{broadcast, watch};
|
|
use tracing::debug;
|
|
|
|
use super::{AudioConfig, AudioFrame, CaptureState};
|
|
use crate::error::{AppError, Result};
|
|
use crate::utils::LogThrottler;
|
|
use crate::warn_throttled;
|
|
|
|
const RETRY_DELAY: Duration = Duration::from_millis(5);
|
|
const MAX_CONSECUTIVE_READ_ERRORS: u32 = 10;
|
|
|
|
pub(super) fn run_capture(
|
|
config: &AudioConfig,
|
|
state: &watch::Sender<CaptureState>,
|
|
frame_tx: &broadcast::Sender<AudioFrame>,
|
|
stop_flag: &AtomicBool,
|
|
log_throttler: &LogThrottler,
|
|
) -> Result<()> {
|
|
// Non-blocking mode guarantees that stop() can always join the worker.
|
|
let pcm = PCM::new(&config.device_name, Direction::Capture, true).map_err(|error| {
|
|
AppError::AudioError(format!(
|
|
"Failed to open audio device {}: {}",
|
|
config.device_name, error
|
|
))
|
|
})?;
|
|
|
|
configure_pcm(&pcm, config)?;
|
|
pcm.prepare()
|
|
.map_err(|error| AppError::AudioError(format!("Failed to prepare PCM: {error}")))?;
|
|
let _ = state.send(CaptureState::Running);
|
|
|
|
let period_frames = pcm
|
|
.hw_params_current()
|
|
.ok()
|
|
.and_then(|params| params.get_period_size().ok())
|
|
.map(|frames| frames as usize)
|
|
.unwrap_or(config.period_frames as usize)
|
|
.max(256);
|
|
let mut buffer = vec![0u8; period_frames * config.channels as usize * 2];
|
|
let io: IO<u8> = pcm.io_bytes();
|
|
let mut consecutive_errors = 0;
|
|
|
|
while !stop_flag.load(Ordering::Acquire) {
|
|
match pcm.state() {
|
|
State::XRun => {
|
|
warn_throttled!(log_throttler, "xrun", "Audio buffer overrun, recovering");
|
|
pcm.prepare().map_err(|error| {
|
|
AppError::AudioError(format!("Failed to recover audio xrun: {error}"))
|
|
})?;
|
|
consecutive_errors = 0;
|
|
continue;
|
|
}
|
|
State::Suspended => {
|
|
warn_throttled!(
|
|
log_throttler,
|
|
"suspended",
|
|
"Audio device suspended, recovering"
|
|
);
|
|
if pcm.resume().is_err() {
|
|
pcm.prepare().map_err(|error| {
|
|
AppError::AudioError(format!("Failed to resume audio capture: {error}"))
|
|
})?;
|
|
}
|
|
consecutive_errors = 0;
|
|
continue;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
match io.readi(&mut buffer) {
|
|
Ok(0) => thread::sleep(RETRY_DELAY),
|
|
Ok(frames_read) => {
|
|
consecutive_errors = 0;
|
|
let byte_count = frames_read * config.channels as usize * 2;
|
|
let frame = AudioFrame::new_interleaved(
|
|
Bytes::copy_from_slice(&buffer[..byte_count]),
|
|
config.channels,
|
|
config.sample_rate,
|
|
);
|
|
if frame_tx.receiver_count() > 0 {
|
|
let _ = frame_tx.send(frame);
|
|
}
|
|
}
|
|
Err(error) if error.errno() == libc::EAGAIN => thread::sleep(RETRY_DELAY),
|
|
Err(error) if is_device_lost_errno(error.errno()) => {
|
|
return Err(AppError::AudioError(format!(
|
|
"Audio device lost while reading {}: {}",
|
|
config.device_name, error
|
|
)));
|
|
}
|
|
Err(error) if error.errno() == libc::EPIPE => {
|
|
warn_throttled!(log_throttler, "buffer_overrun", "Audio buffer overrun");
|
|
pcm.prepare().map_err(|prepare_error| {
|
|
AppError::AudioError(format!(
|
|
"Failed to recover after audio overrun ({error}): {prepare_error}"
|
|
))
|
|
})?;
|
|
consecutive_errors = 0;
|
|
}
|
|
Err(error) => {
|
|
consecutive_errors += 1;
|
|
warn_throttled!(log_throttler, "read_error", "Audio read error: {}", error);
|
|
if consecutive_errors >= MAX_CONSECUTIVE_READ_ERRORS {
|
|
return Err(AppError::AudioError(format!(
|
|
"Audio capture failed {consecutive_errors} times consecutively: {error}"
|
|
)));
|
|
}
|
|
thread::sleep(RETRY_DELAY);
|
|
}
|
|
}
|
|
}
|
|
|
|
debug!("ALSA capture worker stopped");
|
|
Ok(())
|
|
}
|
|
|
|
fn configure_pcm(pcm: &PCM, config: &AudioConfig) -> Result<()> {
|
|
let params = HwParams::any(pcm)
|
|
.map_err(|error| AppError::AudioError(format!("Failed to get HwParams: {error}")))?;
|
|
params
|
|
.set_channels(config.channels)
|
|
.and_then(|_| params.set_rate(config.sample_rate, ValueOr::Nearest))
|
|
.and_then(|_| params.set_format(Format::s16()))
|
|
.and_then(|_| params.set_access(Access::RWInterleaved))
|
|
.and_then(|_| params.set_buffer_size_near(config.buffer_frames as Frames))
|
|
.and_then(|_| params.set_period_size_near(config.period_frames as Frames, ValueOr::Nearest))
|
|
.and_then(|_| pcm.hw_params(¶ms))
|
|
.map_err(|error| AppError::AudioError(format!("Failed to configure audio PCM: {error}")))?;
|
|
|
|
let actual = pcm
|
|
.hw_params_current()
|
|
.map_err(|error| AppError::AudioError(format!("Failed to read PCM parameters: {error}")))?;
|
|
let actual_rate = actual
|
|
.get_rate()
|
|
.map_err(|error| AppError::AudioError(format!("Failed to read sample rate: {error}")))?;
|
|
let actual_channels = actual
|
|
.get_channels()
|
|
.map_err(|error| AppError::AudioError(format!("Failed to read channels: {error}")))?;
|
|
if actual_rate != config.sample_rate || actual_channels != config.channels {
|
|
return Err(AppError::AudioError(format!(
|
|
"Audio device negotiated {actual_rate} Hz/{actual_channels} ch; expected {} Hz/{} ch",
|
|
config.sample_rate, config.channels
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn is_device_lost_errno(errno: i32) -> bool {
|
|
matches!(errno, libc::ENODEV | libc::ENXIO | libc::ESHUTDOWN)
|
|
}
|