mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-15 04:01:58 +08:00
feat: 深入适配 RK628D CSI 采集卡的设备识别、参数读取、自恢复和音频采集
This commit is contained in:
@@ -45,25 +45,10 @@ impl Default for AudioConfig {
|
||||
}
|
||||
|
||||
impl AudioConfig {
|
||||
/// Create config for a specific device
|
||||
/// Create config for a specific device (48 kHz stereo only; must match ALSA hardware).
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -281,23 +266,29 @@ fn run_capture(
|
||||
.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);
|
||||
|
||||
if actual_rate != config.sample_rate {
|
||||
info!(
|
||||
"ALSA sample rate differs from requested ({}Hz vs {}Hz); streamer will resample to 48000Hz for Opus",
|
||||
actual_rate, config.sample_rate
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"Audio capture configured: {}Hz {}ch (requested {}Hz)",
|
||||
actual_rate, config.channels, config.sample_rate
|
||||
);
|
||||
// Fixed 48 kHz stereo: fail if hardware negotiated something else.
|
||||
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
|
||||
)));
|
||||
}
|
||||
info!("Audio capture: 48000 Hz, 2 ch");
|
||||
|
||||
// Prepare for capture
|
||||
pcm.prepare()
|
||||
@@ -357,7 +348,7 @@ fn run_capture(
|
||||
let frame = AudioFrame::new_interleaved(
|
||||
Bytes::copy_from_slice(&buffer[..byte_count]),
|
||||
config.channels,
|
||||
actual_rate,
|
||||
48_000,
|
||||
seq,
|
||||
);
|
||||
|
||||
|
||||
@@ -342,8 +342,7 @@ impl AudioController {
|
||||
}
|
||||
|
||||
/// Subscribe to Opus frames (for WebSocket clients)
|
||||
pub fn subscribe_opus(&self) -> Option<tokio::sync::watch::Receiver<Option<Arc<OpusFrame>>>> {
|
||||
// Use try_read to avoid blocking - this is called from sync context sometimes
|
||||
pub fn subscribe_opus(&self) -> Option<tokio::sync::mpsc::Receiver<Arc<OpusFrame>>> {
|
||||
if let Ok(guard) = self.streamer.try_read() {
|
||||
guard.as_ref().map(|s| s.subscribe_opus())
|
||||
} else {
|
||||
@@ -354,7 +353,7 @@ impl AudioController {
|
||||
/// Subscribe to Opus frames (async version)
|
||||
pub async fn subscribe_opus_async(
|
||||
&self,
|
||||
) -> Option<tokio::sync::watch::Receiver<Option<Arc<OpusFrame>>>> {
|
||||
) -> Option<tokio::sync::mpsc::Receiver<Arc<OpusFrame>>> {
|
||||
self.streamer
|
||||
.read()
|
||||
.await
|
||||
|
||||
@@ -13,7 +13,6 @@ pub mod controller;
|
||||
pub mod device;
|
||||
pub mod encoder;
|
||||
pub mod monitor;
|
||||
pub mod resample;
|
||||
pub mod streamer;
|
||||
|
||||
pub use capture::{AudioCapturer, AudioConfig, AudioFrame};
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
//! Resample capture PCM to 48 kHz stereo for Opus (fixed 20 ms / 960×2 samples).
|
||||
|
||||
const OUT_RATE: f64 = 48000.0;
|
||||
const OPUS_STEREO_SAMPLES: usize = 960 * 2;
|
||||
|
||||
enum PipelineState {
|
||||
/// Native 48 kHz interleaved stereo: only buffer and slice into 20 ms blocks (no float work).
|
||||
Stereo48kPassthrough,
|
||||
/// Other rates / mono: linear interpolation to 48 kHz stereo.
|
||||
Resample {
|
||||
in_rate: u32,
|
||||
in_channels: u32,
|
||||
next_out_frame: u64,
|
||||
buffer_start_frame: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Converts incoming interleaved PCM to 48 kHz stereo, then exposes fixed 960×2-sample chunks.
|
||||
pub struct Opus48kPcmBuffer {
|
||||
state: PipelineState,
|
||||
pending: Vec<i16>,
|
||||
}
|
||||
|
||||
impl Opus48kPcmBuffer {
|
||||
pub fn new(in_rate: u32, in_channels: u32) -> Self {
|
||||
let ch = in_channels.max(1);
|
||||
let rate = in_rate.max(1);
|
||||
let state = if rate == 48000 && ch == 2 {
|
||||
PipelineState::Stereo48kPassthrough
|
||||
} else {
|
||||
PipelineState::Resample {
|
||||
in_rate: rate,
|
||||
in_channels: ch,
|
||||
next_out_frame: 0,
|
||||
buffer_start_frame: 0,
|
||||
}
|
||||
};
|
||||
Self {
|
||||
state,
|
||||
pending: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// True when input is already 48 kHz stereo (no interpolation loop).
|
||||
#[cfg(test)]
|
||||
pub fn is_passthrough(&self) -> bool {
|
||||
matches!(self.state, PipelineState::Stereo48kPassthrough)
|
||||
}
|
||||
|
||||
/// Append one capture block (`sample_rate` must match the rate this buffer was built for).
|
||||
pub fn push_interleaved(&mut self, data: &[i16]) {
|
||||
self.pending.extend_from_slice(data);
|
||||
}
|
||||
|
||||
/// Drain as many 960×2 stereo S16LE samples (20 ms @ 48 kHz) as possible.
|
||||
pub fn pop_opus_frames(&mut self, out: &mut Vec<i16>) {
|
||||
match &mut self.state {
|
||||
PipelineState::Stereo48kPassthrough => {
|
||||
while self.pending.len() >= OPUS_STEREO_SAMPLES {
|
||||
out.extend_from_slice(&self.pending[..OPUS_STEREO_SAMPLES]);
|
||||
self.pending.drain(..OPUS_STEREO_SAMPLES);
|
||||
}
|
||||
}
|
||||
PipelineState::Resample {
|
||||
in_rate,
|
||||
in_channels,
|
||||
next_out_frame,
|
||||
buffer_start_frame,
|
||||
} => {
|
||||
let ch = *in_channels as usize;
|
||||
if ch == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
let batch_start = *next_out_frame;
|
||||
let mut block = Vec::with_capacity(OPUS_STEREO_SAMPLES);
|
||||
let mut complete = true;
|
||||
|
||||
for i in 0u64..960 {
|
||||
let k = batch_start + i;
|
||||
let p_abs = (k as f64) * (*in_rate as f64) / OUT_RATE;
|
||||
let f_abs = p_abs.floor() as u64;
|
||||
let frac = p_abs - f_abs as f64;
|
||||
|
||||
let f_rel = f_abs.saturating_sub(*buffer_start_frame) as usize;
|
||||
if f_rel + 1 >= self.pending.len() / ch {
|
||||
complete = false;
|
||||
break;
|
||||
}
|
||||
|
||||
let base0 = f_rel * ch;
|
||||
let base1 = (f_rel + 1) * ch;
|
||||
|
||||
let (l, r) = if *in_channels >= 2 {
|
||||
let l0 = self.pending[base0] as f64;
|
||||
let l1 = self.pending[base1] as f64;
|
||||
let r0 = self.pending[base0 + 1] as f64;
|
||||
let r1 = self.pending[base1 + 1] as f64;
|
||||
(l0 + frac * (l1 - l0), r0 + frac * (r1 - r0))
|
||||
} else {
|
||||
let m0 = self.pending[base0] as f64;
|
||||
let m1 = self.pending[base1] as f64;
|
||||
let v = m0 + frac * (m1 - m0);
|
||||
(v, v)
|
||||
};
|
||||
|
||||
block.push(clamp_f64_to_i16(l));
|
||||
block.push(clamp_f64_to_i16(r));
|
||||
}
|
||||
|
||||
if !complete || block.len() != OPUS_STEREO_SAMPLES {
|
||||
break;
|
||||
}
|
||||
|
||||
out.extend_from_slice(&block);
|
||||
*next_out_frame = batch_start + 960;
|
||||
trim_resample_prefix(
|
||||
&mut self.pending,
|
||||
*in_rate,
|
||||
*next_out_frame,
|
||||
buffer_start_frame,
|
||||
ch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_resample_prefix(
|
||||
pending: &mut Vec<i16>,
|
||||
in_rate: u32,
|
||||
next_out_frame: u64,
|
||||
buffer_start_frame: &mut u64,
|
||||
ch: usize,
|
||||
) {
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let p_next = (next_out_frame as f64) * (in_rate as f64) / OUT_RATE;
|
||||
let need_abs = p_next.floor() as u64;
|
||||
let keep_from_abs = need_abs.saturating_sub(1);
|
||||
if keep_from_abs <= *buffer_start_frame {
|
||||
return;
|
||||
}
|
||||
|
||||
let drop_frames = (keep_from_abs - *buffer_start_frame) as usize;
|
||||
let drop_samples = drop_frames.saturating_mul(ch).min(pending.len());
|
||||
if drop_samples > 0 {
|
||||
pending.drain(0..drop_samples);
|
||||
*buffer_start_frame += drop_frames as u64;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clamp_f64_to_i16(v: f64) -> i16 {
|
||||
v.round().clamp(i16::MIN as f64, i16::MAX as f64) as i16
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn passthrough_48k_identity_tone_length() {
|
||||
let mut buf = Opus48kPcmBuffer::new(48000, 2);
|
||||
assert!(buf.is_passthrough());
|
||||
let mut chunk = vec![0i16; 960 * 2];
|
||||
for i in 0..960 {
|
||||
let s = (i as f32 * 0.1).sin() * 3000.0;
|
||||
chunk[2 * i] = s as i16;
|
||||
chunk[2 * i + 1] = s as i16;
|
||||
}
|
||||
buf.push_interleaved(&chunk);
|
||||
let mut out = Vec::new();
|
||||
buf.pop_opus_frames(&mut out);
|
||||
assert_eq!(out.len(), 960 * 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upsample_44k_to_48k_chunk() {
|
||||
let mut buf = Opus48kPcmBuffer::new(44100, 2);
|
||||
assert!(!buf.is_passthrough());
|
||||
let mut chunk = vec![0i16; 882 * 2];
|
||||
for i in 0..882 {
|
||||
chunk[2 * i] = (i as i16).wrapping_mul(10);
|
||||
chunk[2 * i + 1] = (i as i16).wrapping_mul(-7);
|
||||
}
|
||||
buf.push_interleaved(&chunk);
|
||||
let mut out = Vec::new();
|
||||
buf.pop_opus_frames(&mut out);
|
||||
assert_eq!(out.len(), 960 * 2, "expected one 20ms Opus block");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mono_48k_not_passthrough() {
|
||||
let buf = Opus48kPcmBuffer::new(48000, 1);
|
||||
assert!(!buf.is_passthrough());
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
//! Audio streaming pipeline
|
||||
//!
|
||||
//! Coordinates audio capture and Opus encoding, distributing encoded
|
||||
//! frames to multiple subscribers via broadcast channel.
|
||||
//! ALSA capture (48 kHz stereo only) → fixed Opus 20 ms frames → `mpsc` fan-out per subscriber.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use tokio::sync::{broadcast, watch, Mutex, RwLock};
|
||||
use tokio::sync::{broadcast, mpsc, watch, Mutex as AsyncMutex, RwLock};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use super::capture::{AudioCapturer, AudioConfig, AudioFrame, CaptureState};
|
||||
use super::encoder::{OpusConfig, OpusEncoder, OpusFrame};
|
||||
use super::resample::Opus48kPcmBuffer;
|
||||
use crate::error::{AppError, Result};
|
||||
use bytemuck;
|
||||
use bytes::Bytes;
|
||||
|
||||
/// Stereo 48 kHz: 20 ms = 960 frames × 2 channels (S16LE).
|
||||
const OPUS_STEREO_SAMPLES: usize = 960 * 2;
|
||||
|
||||
/// Audio stream state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum AudioStreamState {
|
||||
@@ -68,15 +69,16 @@ pub struct AudioStreamStats {
|
||||
|
||||
/// Audio streamer
|
||||
///
|
||||
/// Manages the audio capture -> encode -> broadcast pipeline.
|
||||
/// Manages the audio capture → encode → mpsc fan-out 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: watch::Sender<Option<Arc<OpusFrame>>>,
|
||||
stats: Arc<Mutex<AudioStreamStats>>,
|
||||
encoder: Arc<AsyncMutex<Option<OpusEncoder>>>,
|
||||
/// One `mpsc::Sender` per subscriber (like shared video pipeline).
|
||||
opus_subscribers: Arc<Mutex<Vec<mpsc::Sender<Arc<OpusFrame>>>>>,
|
||||
stats: Arc<AsyncMutex<AudioStreamStats>>,
|
||||
sequence: AtomicU64,
|
||||
stream_start_time: RwLock<Option<Instant>>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
@@ -91,16 +93,15 @@ impl AudioStreamer {
|
||||
/// 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, _opus_rx) = watch::channel(None);
|
||||
|
||||
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())),
|
||||
encoder: Arc::new(AsyncMutex::new(None)),
|
||||
opus_subscribers: Arc::new(Mutex::new(Vec::new())),
|
||||
stats: Arc::new(AsyncMutex::new(AudioStreamStats::default())),
|
||||
sequence: AtomicU64::new(0),
|
||||
stream_start_time: RwLock::new(None),
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
@@ -117,14 +118,21 @@ impl AudioStreamer {
|
||||
self.state_rx.clone()
|
||||
}
|
||||
|
||||
/// Subscribe to Opus frames
|
||||
pub fn subscribe_opus(&self) -> watch::Receiver<Option<Arc<OpusFrame>>> {
|
||||
self.opus_tx.subscribe()
|
||||
/// Subscribe to Opus frames (each packet is one encoded 20 ms frame).
|
||||
pub fn subscribe_opus(&self) -> mpsc::Receiver<Arc<OpusFrame>> {
|
||||
let (tx, rx) = mpsc::channel::<Arc<OpusFrame>>(128);
|
||||
self.opus_subscribers.lock().unwrap().push(tx);
|
||||
rx
|
||||
}
|
||||
|
||||
/// Get number of active subscribers
|
||||
pub fn subscriber_count(&self) -> usize {
|
||||
self.opus_tx.receiver_count()
|
||||
self.opus_subscribers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|s| !s.is_closed())
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Get current statistics
|
||||
@@ -202,12 +210,13 @@ impl AudioStreamer {
|
||||
// Start encoding task
|
||||
let capturer_for_task = capturer.clone();
|
||||
let encoder = self.encoder.clone();
|
||||
let opus_tx = self.opus_tx.clone();
|
||||
let opus_subscribers = self.opus_subscribers.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, state, stop_flag).await;
|
||||
Self::stream_task(capturer_for_task, encoder, opus_subscribers, state, stop_flag)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -229,10 +238,11 @@ impl AudioStreamer {
|
||||
capturer.stop().await?;
|
||||
}
|
||||
|
||||
// Clear resources
|
||||
// Clear resources — drop Opus senders so mpsc receivers see end-of-stream
|
||||
*self.capturer.write().await = None;
|
||||
*self.encoder.lock().await = None;
|
||||
*self.stream_start_time.write().await = None;
|
||||
self.opus_subscribers.lock().unwrap().clear();
|
||||
|
||||
let _ = self.state.send(AudioStreamState::Stopped);
|
||||
info!("Audio stream stopped");
|
||||
@@ -244,51 +254,63 @@ impl AudioStreamer {
|
||||
self.state() == AudioStreamState::Running
|
||||
}
|
||||
|
||||
/// Internal streaming task
|
||||
async fn fanout_opus(
|
||||
subscribers: &Arc<Mutex<Vec<mpsc::Sender<Arc<OpusFrame>>>>>,
|
||||
frame: Arc<OpusFrame>,
|
||||
) {
|
||||
let txs: Vec<_> = {
|
||||
let g = subscribers.lock().unwrap();
|
||||
if g.is_empty() {
|
||||
return;
|
||||
}
|
||||
g.clone()
|
||||
};
|
||||
for tx in &txs {
|
||||
let _ = tx.send(frame.clone()).await;
|
||||
}
|
||||
if txs.iter().any(|tx| tx.is_closed()) {
|
||||
let mut g = subscribers.lock().unwrap();
|
||||
g.retain(|tx| !tx.is_closed());
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_task(
|
||||
capturer: Arc<AudioCapturer>,
|
||||
encoder: Arc<Mutex<Option<OpusEncoder>>>,
|
||||
opus_tx: watch::Sender<Option<Arc<OpusFrame>>>,
|
||||
encoder: Arc<AsyncMutex<Option<OpusEncoder>>>,
|
||||
opus_subscribers: Arc<Mutex<Vec<mpsc::Sender<Arc<OpusFrame>>>>>,
|
||||
state: watch::Sender<AudioStreamState>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
) {
|
||||
let mut pcm_rx = capturer.subscribe();
|
||||
let _ = state.send(AudioStreamState::Running);
|
||||
|
||||
info!("Audio stream task started");
|
||||
info!("Audio stream task started (48 kHz stereo → Opus, mpsc fan-out)");
|
||||
|
||||
let mut to_48k: Option<Opus48kPcmBuffer> = None;
|
||||
let mut queued_48k: Vec<i16> = Vec::new();
|
||||
let mut pending: Vec<i16> = Vec::new();
|
||||
|
||||
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)) => {
|
||||
if to_48k.is_none() {
|
||||
to_48k = Some(Opus48kPcmBuffer::new(
|
||||
audio_frame.sample_rate,
|
||||
audio_frame.channels,
|
||||
));
|
||||
if audio_frame.sample_rate != 48_000 || audio_frame.channels != 2 {
|
||||
warn!(
|
||||
"Skip non–48 kHz/stereo PCM ({} Hz, {} ch)",
|
||||
audio_frame.sample_rate, audio_frame.channels
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let pipeline = match to_48k.as_mut() {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let samples: &[i16] = match bytemuck::try_cast_slice(&audio_frame.data) {
|
||||
Ok(s) => s,
|
||||
@@ -298,16 +320,16 @@ impl AudioStreamer {
|
||||
}
|
||||
};
|
||||
if !samples.is_empty() {
|
||||
pipeline.push_interleaved(samples);
|
||||
pending.extend_from_slice(samples);
|
||||
}
|
||||
pipeline.pop_opus_frames(&mut queued_48k);
|
||||
|
||||
while queued_48k.len() >= 960 * 2 {
|
||||
let pcm_20ms =
|
||||
Bytes::copy_from_slice(bytemuck::cast_slice(&queued_48k[..960 * 2]));
|
||||
queued_48k.drain(..960 * 2);
|
||||
while pending.len() >= OPUS_STEREO_SAMPLES {
|
||||
let pcm_20ms = Bytes::copy_from_slice(bytemuck::cast_slice(
|
||||
&pending[..OPUS_STEREO_SAMPLES],
|
||||
));
|
||||
pending.drain(..OPUS_STEREO_SAMPLES);
|
||||
|
||||
let frame_48k = AudioFrame::new_interleaved(pcm_20ms, 2, 48000, 0);
|
||||
let frame_48k = AudioFrame::new_interleaved(pcm_20ms, 2, 48_000, 0);
|
||||
|
||||
let opus_result = {
|
||||
let mut enc_guard = encoder.lock().await;
|
||||
@@ -318,9 +340,7 @@ impl AudioStreamer {
|
||||
|
||||
match opus_result {
|
||||
Some(Ok(opus_frame)) => {
|
||||
if opus_tx.receiver_count() > 0 {
|
||||
let _ = opus_tx.send(Some(Arc::new(opus_frame)));
|
||||
}
|
||||
Self::fanout_opus(&opus_subscribers, Arc::new(opus_frame)).await;
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
error!("Opus encode error: {}", e);
|
||||
@@ -337,10 +357,9 @@ impl AudioStreamer {
|
||||
break;
|
||||
}
|
||||
Ok(Err(broadcast::error::RecvError::Lagged(n))) => {
|
||||
warn!("Audio receiver lagged by {} frames", n);
|
||||
warn!("PCM receiver lagged by {} frames", n);
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout - check if still capturing
|
||||
if capturer.state() != CaptureState::Running {
|
||||
info!("Audio capture stopped, ending stream task");
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user