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

@@ -41,12 +41,14 @@ use crate::audio::shared_pipeline::{SharedAudioPipeline, SharedAudioPipelineConf
use crate::audio::{AudioController, OpusFrame};
use crate::error::{AppError, Result};
use crate::hid::HidController;
use crate::video::encoder::registry::VideoEncoderType;
use crate::video::encoder::registry::EncoderBackend;
use crate::video::encoder::registry::VideoEncoderType;
use crate::video::encoder::VideoCodecType;
use crate::video::format::{PixelFormat, Resolution};
use crate::video::frame::VideoFrame;
use crate::video::shared_video_pipeline::{SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats};
use crate::video::shared_video_pipeline::{
SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats,
};
use super::config::{TurnServer, WebRtcConfig};
use super::signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer};
@@ -489,7 +491,9 @@ impl WebRtcStreamer {
}
}
} else {
info!("No video pipeline exists yet, frame source will be used when pipeline is created");
info!(
"No video pipeline exists yet, frame source will be used when pipeline is created"
);
}
}
@@ -517,24 +521,21 @@ impl WebRtcStreamer {
/// Only restarts the encoding pipeline if configuration actually changed.
/// This allows multiple consumers (WebRTC, RustDesk) to share the same pipeline
/// without interrupting each other when they call this method with the same config.
pub async fn update_video_config(
&self,
resolution: Resolution,
format: PixelFormat,
fps: u32,
) {
pub async fn update_video_config(&self, resolution: Resolution, format: PixelFormat, fps: u32) {
// Check if configuration actually changed
let config = self.config.read().await;
let config_changed = config.resolution != resolution
|| config.input_format != format
|| config.fps != fps;
let config_changed =
config.resolution != resolution || config.input_format != format || config.fps != fps;
drop(config);
if !config_changed {
// Configuration unchanged, no need to restart pipeline
trace!(
"Video config unchanged: {}x{} {:?} @ {} fps",
resolution.width, resolution.height, format, fps
resolution.width,
resolution.height,
format,
fps
);
return;
}
@@ -554,7 +555,10 @@ impl WebRtcStreamer {
// Close all existing sessions - they need to reconnect
let session_count = self.close_all_sessions().await;
if session_count > 0 {
info!("Closed {} existing sessions due to config change", session_count);
info!(
"Closed {} existing sessions due to config change",
session_count
);
}
// Update config (preserve user-configured bitrate)
@@ -581,17 +585,17 @@ impl WebRtcStreamer {
// Close all existing sessions - they need to reconnect with new encoder
let session_count = self.close_all_sessions().await;
if session_count > 0 {
info!("Closed {} existing sessions due to encoder backend change", session_count);
info!(
"Closed {} existing sessions due to encoder backend change",
session_count
);
}
// Update config
let mut config = self.config.write().await;
config.encoder_backend = encoder_backend;
info!(
"WebRTC encoder backend updated: {:?}",
encoder_backend
);
info!("WebRTC encoder backend updated: {:?}", encoder_backend);
}
/// Check if current encoder configuration uses hardware encoding
@@ -694,7 +698,11 @@ impl WebRtcStreamer {
let codec = *self.video_codec.read().await;
// Ensure video pipeline is running
let frame_tx = self.video_frame_tx.read().await.clone()
let frame_tx = self
.video_frame_tx
.read()
.await
.clone()
.ok_or_else(|| AppError::VideoError("No video frame source".to_string()))?;
let pipeline = self.ensure_video_pipeline(frame_tx).await?;
@@ -729,15 +737,20 @@ impl WebRtcStreamer {
// Request keyframe after ICE connection is established (via callback)
let pipeline_for_callback = pipeline.clone();
let session_id_for_callback = session_id.clone();
session.start_from_video_pipeline(pipeline.subscribe(), move || {
// Spawn async task to request keyframe
let pipeline = pipeline_for_callback;
let sid = session_id_for_callback;
tokio::spawn(async move {
info!("Requesting keyframe for session {} after ICE connected", sid);
pipeline.request_keyframe().await;
});
}).await;
session
.start_from_video_pipeline(pipeline.subscribe(), move || {
// Spawn async task to request keyframe
let pipeline = pipeline_for_callback;
let sid = session_id_for_callback;
tokio::spawn(async move {
info!(
"Requesting keyframe for session {} after ICE connected",
sid
);
pipeline.request_keyframe().await;
});
})
.await;
// Start audio if enabled
if session_config.audio_enabled {
@@ -863,7 +876,9 @@ impl WebRtcStreamer {
.filter(|(_, s)| {
matches!(
s.state(),
ConnectionState::Closed | ConnectionState::Failed | ConnectionState::Disconnected
ConnectionState::Closed
| ConnectionState::Failed
| ConnectionState::Disconnected
)
})
.map(|(id, _)| id.clone())
@@ -967,10 +982,7 @@ impl WebRtcStreamer {
};
if pipeline_running {
info!(
"Restarting video pipeline to apply new bitrate: {}",
preset
);
info!("Restarting video pipeline to apply new bitrate: {}", preset);
// Save video_frame_tx BEFORE stopping pipeline (monitor task will clear it)
let saved_frame_tx = self.video_frame_tx.read().await.clone();
@@ -1005,13 +1017,18 @@ impl WebRtcStreamer {
info!("Reconnecting session {} to new pipeline", session_id);
let pipeline_for_callback = pipeline.clone();
let sid = session_id.clone();
session.start_from_video_pipeline(pipeline.subscribe(), move || {
let pipeline = pipeline_for_callback;
tokio::spawn(async move {
info!("Requesting keyframe for session {} after reconnect", sid);
pipeline.request_keyframe().await;
});
}).await;
session
.start_from_video_pipeline(pipeline.subscribe(), move || {
let pipeline = pipeline_for_callback;
tokio::spawn(async move {
info!(
"Requesting keyframe for session {} after reconnect",
sid
);
pipeline.request_keyframe().await;
});
})
.await;
}
}