mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-20 09:56:41 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
Reference in New Issue
Block a user