feat(video): 事务化切换与前端统一编排,增强视频输入格式支持

- 后端:切换事务+transition_id,/stream/mode 返回 switching/transition_id 与实际 codec

- 事件:新增 mode_switching/mode_ready,config/webrtc_ready/mode_changed 关联事务

- 编码/格式:扩展 NV21/NV16/NV24/RGB/BGR 输入与转换链路,RKMPP direct input 优化

- 前端:useVideoSession 统一切换,失败回退真实切回 MJPEG,菜单格式同步修复

- 清理:useVideoStream 降级为 MJPEG-only
This commit is contained in:
mofeng-git
2026-01-11 10:41:57 +08:00
parent 9feb74b72c
commit 206594e292
110 changed files with 3955 additions and 2251 deletions

View File

@@ -201,7 +201,15 @@ impl AudioCapturer {
let log_throttler = self.log_throttler.clone();
let handle = tokio::task::spawn_blocking(move || {
capture_loop(config, state, stats, frame_tx, stop_flag, sequence, log_throttler);
capture_loop(
config,
state,
stats,
frame_tx,
stop_flag,
sequence,
log_throttler,
);
});
*self.capture_handle.lock().await = Some(handle);
@@ -274,40 +282,34 @@ fn run_capture(
// Configure hardware parameters
{
let hwp = HwParams::any(&pcm).map_err(|e| {
AppError::AudioError(format!("Failed to get HwParams: {}", 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_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_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_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_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_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))
})?;
pcm.hw_params(&hwp)
.map_err(|e| AppError::AudioError(format!("Failed to apply hw params: {}", e)))?;
}
// Get actual configuration
let actual_rate = pcm.hw_params_current()
let actual_rate = pcm
.hw_params_current()
.map(|h| h.get_rate().unwrap_or(config.sample_rate))
.unwrap_or(config.sample_rate);
@@ -317,9 +319,8 @@ fn run_capture(
);
// Prepare for capture
pcm.prepare().map_err(|e| {
AppError::AudioError(format!("Failed to prepare PCM: {}", e))
})?;
pcm.prepare()
.map_err(|e| AppError::AudioError(format!("Failed to prepare PCM: {}", e)))?;
let _ = state.send(CaptureState::Running);
@@ -340,7 +341,11 @@ fn run_capture(
continue;
}
State::Suspended => {
warn_throttled!(log_throttler, "suspended", "Audio device suspended, recovering");
warn_throttled!(
log_throttler,
"suspended",
"Audio device suspended, recovering"
);
let _ = pcm.resume();
continue;
}
@@ -363,11 +368,8 @@ fn run_capture(
// Directly use the buffer slice (already in correct byte format)
let seq = sequence.fetch_add(1, Ordering::Relaxed);
let frame = AudioFrame::new(
Bytes::copy_from_slice(&buffer[..byte_count]),
config,
seq,
);
let frame =
AudioFrame::new(Bytes::copy_from_slice(&buffer[..byte_count]), config, seq);
// Send to subscribers
if frame_tx.receiver_count() > 0 {

View File

@@ -193,7 +193,9 @@ impl AudioController {
pub async fn select_device(&self, device: &str) -> Result<()> {
// Validate device exists
let devices = self.list_devices().await?;
let found = devices.iter().any(|d| d.name == device || d.description.contains(device));
let found = devices
.iter()
.any(|d| d.name == device || d.description.contains(device));
if !found && device != "default" {
return Err(AppError::AudioError(format!(
@@ -244,7 +246,11 @@ impl AudioController {
})
.await;
info!("Audio quality set to: {:?} ({}bps)", quality, quality.bitrate());
info!(
"Audio quality set to: {:?} ({}bps)",
quality,
quality.bitrate()
);
Ok(())
}
@@ -346,14 +352,17 @@ impl AudioController {
let streaming = self.is_streaming().await;
let error = self.last_error.read().await.clone();
let (subscriber_count, frames_encoded, bytes_output) = if let Some(ref streamer) =
*self.streamer.read().await
{
let stats = streamer.stats().await;
(stats.subscriber_count, stats.frames_encoded, stats.bytes_output)
} else {
(0, 0, 0)
};
let (subscriber_count, frames_encoded, bytes_output) =
if let Some(ref streamer) = *self.streamer.read().await {
let stats = streamer.stats().await;
(
stats.subscriber_count,
stats.frames_encoded,
stats.bytes_output,
)
} else {
(0, 0, 0)
};
AudioStatus {
enabled: config.enabled,
@@ -383,7 +392,11 @@ impl AudioController {
/// Subscribe to Opus frames (async version)
pub async fn subscribe_opus_async(&self) -> Option<broadcast::Receiver<OpusFrame>> {
self.streamer.read().await.as_ref().map(|s| s.subscribe_opus())
self.streamer
.read()
.await
.as_ref()
.map(|s| s.subscribe_opus())
}
/// Enable or disable audio

View File

@@ -55,7 +55,12 @@ fn get_usb_bus_info(card_index: i32) -> Option<String> {
// Match patterns like "1-1", "1-2", "1-1.2", "2-1.3.1"
if component.contains('-') && !component.contains(':') {
// Verify it looks like a USB port (starts with digit)
if component.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
if component
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
return Some(component.to_string());
}
}
@@ -223,15 +228,14 @@ 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()));
return Err(AppError::AudioError(
"No audio capture devices found".to_string(),
));
}
// First, look for HDMI/capture card devices that support 48kHz stereo
for device in &devices {
if device.is_hdmi
&& device.sample_rates.contains(&48000)
&& device.channels.contains(&2)
{
if device.is_hdmi && device.sample_rates.contains(&48000) && device.channels.contains(&2) {
info!("Selected HDMI audio device: {}", device.description);
return Ok(device.clone());
}

View File

@@ -137,9 +137,8 @@ impl OpusEncoder {
let channels = config.to_audiopus_channels();
let application = config.to_audiopus_application();
let mut encoder = Encoder::new(sample_rate, channels, application).map_err(|e| {
AppError::AudioError(format!("Failed to create Opus encoder: {:?}", e))
})?;
let mut encoder = Encoder::new(sample_rate, channels, application)
.map_err(|e| AppError::AudioError(format!("Failed to create Opus encoder: {:?}", e)))?;
// Configure encoder
encoder

View File

@@ -22,5 +22,7 @@ pub use controller::{AudioController, AudioControllerConfig, AudioQuality, Audio
pub use device::{enumerate_audio_devices, enumerate_audio_devices_with_current, AudioDeviceInfo};
pub use encoder::{OpusConfig, OpusEncoder, OpusFrame};
pub use monitor::{AudioHealthMonitor, AudioHealthStatus, AudioMonitorConfig};
pub use shared_pipeline::{SharedAudioPipeline, SharedAudioPipelineConfig, SharedAudioPipelineStats};
pub use shared_pipeline::{
SharedAudioPipeline, SharedAudioPipelineConfig, SharedAudioPipelineStats,
};
pub use streamer::{AudioStreamState, AudioStreamer, AudioStreamerConfig};

View File

@@ -329,9 +329,7 @@ mod tests {
let monitor = AudioHealthMonitor::with_defaults();
for i in 1..=5 {
monitor
.report_error(None, "Error", "io_error")
.await;
monitor.report_error(None, "Error", "io_error").await;
assert_eq!(monitor.retry_count(), i);
}
}
@@ -340,9 +338,7 @@ mod tests {
async fn test_reset() {
let monitor = AudioHealthMonitor::with_defaults();
monitor
.report_error(None, "Error", "io_error")
.await;
monitor.report_error(None, "Error", "io_error").await;
assert!(monitor.is_error().await);
monitor.reset().await;

View File

@@ -60,7 +60,7 @@ impl Default for SharedAudioPipelineConfig {
bitrate: 64000,
application: OpusApplicationMode::Audio,
fec: true,
channel_capacity: 16, // Reduced from 64 for lower latency
channel_capacity: 16, // Reduced from 64 for lower latency
}
}
}
@@ -320,11 +320,8 @@ impl SharedAudioPipeline {
}
// Receive audio frame with timeout
let recv_result = tokio::time::timeout(
std::time::Duration::from_secs(2),
audio_rx.recv(),
)
.await;
let recv_result =
tokio::time::timeout(std::time::Duration::from_secs(2), audio_rx.recv()).await;
match recv_result {
Ok(Ok(audio_frame)) => {

View File

@@ -297,11 +297,8 @@ impl AudioStreamer {
}
// Receive PCM frame with timeout
let recv_result = tokio::time::timeout(
std::time::Duration::from_secs(2),
pcm_rx.recv(),
)
.await;
let recv_result =
tokio::time::timeout(std::time::Duration::from_secs(2), pcm_rx.recv()).await;
match recv_result {
Ok(Ok(audio_frame)) => {