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

@@ -92,10 +92,9 @@ impl PeerConnection {
};
// Create peer connection
let pc = api
.new_peer_connection(rtc_config)
.await
.map_err(|e| AppError::VideoError(format!("Failed to create peer connection: {}", e)))?;
let pc = api.new_peer_connection(rtc_config).await.map_err(|e| {
AppError::VideoError(format!("Failed to create peer connection: {}", e))
})?;
let pc = Arc::new(pc);
@@ -125,68 +124,69 @@ impl PeerConnection {
let session_id = self.session_id.clone();
// Connection state change handler
self.pc.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| {
let state = state.clone();
let session_id = session_id.clone();
self.pc
.on_peer_connection_state_change(Box::new(move |s: RTCPeerConnectionState| {
let state = state.clone();
let session_id = session_id.clone();
Box::pin(async move {
let new_state = match s {
RTCPeerConnectionState::New => ConnectionState::New,
RTCPeerConnectionState::Connecting => ConnectionState::Connecting,
RTCPeerConnectionState::Connected => ConnectionState::Connected,
RTCPeerConnectionState::Disconnected => ConnectionState::Disconnected,
RTCPeerConnectionState::Failed => ConnectionState::Failed,
RTCPeerConnectionState::Closed => ConnectionState::Closed,
_ => return,
};
Box::pin(async move {
let new_state = match s {
RTCPeerConnectionState::New => ConnectionState::New,
RTCPeerConnectionState::Connecting => ConnectionState::Connecting,
RTCPeerConnectionState::Connected => ConnectionState::Connected,
RTCPeerConnectionState::Disconnected => ConnectionState::Disconnected,
RTCPeerConnectionState::Failed => ConnectionState::Failed,
RTCPeerConnectionState::Closed => ConnectionState::Closed,
_ => return,
};
info!("Peer {} connection state: {}", session_id, new_state);
let _ = state.send(new_state);
})
}));
info!("Peer {} connection state: {}", session_id, new_state);
let _ = state.send(new_state);
})
}));
// ICE candidate handler
let ice_candidates = self.ice_candidates.clone();
self.pc.on_ice_candidate(Box::new(move |candidate: Option<RTCIceCandidate>| {
let ice_candidates = ice_candidates.clone();
self.pc
.on_ice_candidate(Box::new(move |candidate: Option<RTCIceCandidate>| {
let ice_candidates = ice_candidates.clone();
Box::pin(async move {
if let Some(c) = candidate {
let candidate_str = c.to_json()
.map(|j| j.candidate)
.unwrap_or_default();
Box::pin(async move {
if let Some(c) = candidate {
let candidate_str = c.to_json().map(|j| j.candidate).unwrap_or_default();
debug!("ICE candidate: {}", candidate_str);
debug!("ICE candidate: {}", candidate_str);
let mut candidates = ice_candidates.lock().await;
candidates.push(IceCandidate {
candidate: candidate_str,
sdp_mid: c.to_json().ok().and_then(|j| j.sdp_mid),
sdp_mline_index: c.to_json().ok().and_then(|j| j.sdp_mline_index),
username_fragment: None,
});
}
})
}));
let mut candidates = ice_candidates.lock().await;
candidates.push(IceCandidate {
candidate: candidate_str,
sdp_mid: c.to_json().ok().and_then(|j| j.sdp_mid),
sdp_mline_index: c.to_json().ok().and_then(|j| j.sdp_mline_index),
username_fragment: None,
});
}
})
}));
// Data channel handler - note: HID processing is done when hid_controller is set
let data_channel = self.data_channel.clone();
self.pc.on_data_channel(Box::new(move |dc: Arc<RTCDataChannel>| {
let data_channel = data_channel.clone();
self.pc
.on_data_channel(Box::new(move |dc: Arc<RTCDataChannel>| {
let data_channel = data_channel.clone();
Box::pin(async move {
info!("Data channel opened: {}", dc.label());
Box::pin(async move {
info!("Data channel opened: {}", dc.label());
// Store data channel
*data_channel.write().await = Some(dc.clone());
// Store data channel
*data_channel.write().await = Some(dc.clone());
// Message handler logs messages; HID processing requires set_hid_controller()
dc.on_message(Box::new(move |msg: DataChannelMessage| {
debug!("DataChannel message: {} bytes", msg.data.len());
Box::pin(async {})
}));
})
}));
// Message handler logs messages; HID processing requires set_hid_controller()
dc.on_message(Box::new(move |msg: DataChannelMessage| {
debug!("DataChannel message: {} bytes", msg.data.len());
Box::pin(async {})
}));
})
}));
}
/// Set HID controller for processing DataChannel messages
@@ -206,7 +206,11 @@ impl PeerConnection {
let is_hid_channel = label == "hid" || label == "hid-unreliable";
if is_hid_channel {
info!("HID DataChannel opened: {} (unreliable: {})", label, label == "hid-unreliable");
info!(
"HID DataChannel opened: {} (unreliable: {})",
label,
label == "hid-unreliable"
);
// Store the reliable data channel for sending responses
if label == "hid" {
@@ -291,10 +295,9 @@ impl PeerConnection {
let sdp = RTCSessionDescription::offer(offer.sdp)
.map_err(|e| AppError::VideoError(format!("Invalid SDP offer: {}", e)))?;
self.pc
.set_remote_description(sdp)
.await
.map_err(|e| AppError::VideoError(format!("Failed to set remote description: {}", e)))?;
self.pc.set_remote_description(sdp).await.map_err(|e| {
AppError::VideoError(format!("Failed to set remote description: {}", e))
})?;
// Create answer
let answer = self
@@ -373,7 +376,11 @@ impl PeerConnection {
// Reset HID state to release any held keys/buttons
if let Some(ref hid) = self.hid_controller {
if let Err(e) = hid.reset().await {
tracing::warn!("Failed to reset HID on peer {} close: {}", self.session_id, e);
tracing::warn!(
"Failed to reset HID on peer {} close: {}",
self.session_id,
e
);
} else {
tracing::debug!("HID reset on peer {} close", self.session_id);
}