mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +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:
@@ -38,12 +38,12 @@ const H265_NAL_PPS: u8 = 34;
|
||||
const H265_NAL_AUD: u8 = 35;
|
||||
const H265_NAL_FILLER: u8 = 38;
|
||||
#[allow(dead_code)]
|
||||
const H265_NAL_SEI_PREFIX: u8 = 39; // PREFIX_SEI_NUT
|
||||
const H265_NAL_SEI_PREFIX: u8 = 39; // PREFIX_SEI_NUT
|
||||
#[allow(dead_code)]
|
||||
const H265_NAL_SEI_SUFFIX: u8 = 40; // SUFFIX_SEI_NUT
|
||||
const H265_NAL_SEI_SUFFIX: u8 = 40; // SUFFIX_SEI_NUT
|
||||
#[allow(dead_code)]
|
||||
const H265_NAL_AP: u8 = 48; // Aggregation Packet
|
||||
const H265_NAL_FU: u8 = 49; // Fragmentation Unit
|
||||
const H265_NAL_AP: u8 = 48; // Aggregation Packet
|
||||
const H265_NAL_FU: u8 = 49; // Fragmentation Unit
|
||||
|
||||
/// H.265 NAL header size
|
||||
const H265_NAL_HEADER_SIZE: usize = 2;
|
||||
@@ -228,7 +228,8 @@ impl H265Payloader {
|
||||
let fragment_size = remaining.min(max_fragment_size);
|
||||
|
||||
// Create FU packet
|
||||
let mut packet = BytesMut::with_capacity(H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE + fragment_size);
|
||||
let mut packet =
|
||||
BytesMut::with_capacity(H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE + fragment_size);
|
||||
|
||||
// NAL header for FU (2 bytes)
|
||||
// Preserve F bit (bit 7) and LayerID MSB (bit 0) from original, set Type to 49
|
||||
|
||||
@@ -42,5 +42,7 @@ pub use rtp::{H264VideoTrack, H264VideoTrackConfig, OpusAudioTrack};
|
||||
pub use session::WebRtcSessionManager;
|
||||
pub use signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer, SignalingMessage};
|
||||
pub use universal_session::{UniversalSession, UniversalSessionConfig, UniversalSessionInfo};
|
||||
pub use video_track::{UniversalVideoTrack, UniversalVideoTrackConfig, VideoCodec, VideoTrackStats};
|
||||
pub use video_track::{
|
||||
UniversalVideoTrack, UniversalVideoTrackConfig, VideoCodec, VideoTrackStats,
|
||||
};
|
||||
pub use webrtc_streamer::{SessionInfo, WebRtcStreamer, WebRtcStreamerConfig, WebRtcStreamerStats};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ use tokio::sync::Mutex;
|
||||
use tracing::{debug, error, trace};
|
||||
use webrtc::media::io::h264_reader::H264Reader;
|
||||
use webrtc::media::Sample;
|
||||
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
|
||||
use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample;
|
||||
use webrtc::track::track_local::TrackLocal;
|
||||
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
|
||||
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::video::format::Resolution;
|
||||
@@ -168,7 +168,12 @@ impl H264VideoTrack {
|
||||
/// * `data` - H264 Annex B encoded frame data
|
||||
/// * `duration` - Frame duration (typically 1/fps seconds)
|
||||
/// * `is_keyframe` - Whether this is a keyframe (IDR frame)
|
||||
pub async fn write_frame(&self, data: &[u8], _duration: Duration, is_keyframe: bool) -> Result<()> {
|
||||
pub async fn write_frame(
|
||||
&self,
|
||||
data: &[u8],
|
||||
_duration: Duration,
|
||||
is_keyframe: bool,
|
||||
) -> Result<()> {
|
||||
if data.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -324,9 +329,9 @@ impl H264VideoTrack {
|
||||
let mut payloader = self.payloader.lock().await;
|
||||
let bytes = Bytes::copy_from_slice(data);
|
||||
|
||||
payloader.payload(mtu, &bytes).map_err(|e| {
|
||||
AppError::VideoError(format!("H264 packetization failed: {}", e))
|
||||
})
|
||||
payloader
|
||||
.payload(mtu, &bytes)
|
||||
.map_err(|e| AppError::VideoError(format!("H264 packetization failed: {}", e)))
|
||||
}
|
||||
|
||||
/// Get configuration
|
||||
@@ -423,7 +428,10 @@ impl OpusAudioTrack {
|
||||
let mut stats = self.stats.lock().await;
|
||||
stats.errors += 1;
|
||||
error!("Failed to write Opus sample: {}", e);
|
||||
Err(AppError::WebRtcError(format!("Failed to write audio sample: {}", e)))
|
||||
Err(AppError::WebRtcError(format!(
|
||||
"Failed to write audio sample: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::sync::{broadcast, watch, Mutex};
|
||||
use tracing::{debug, error, info};
|
||||
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
|
||||
use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP;
|
||||
use webrtc::track::track_local::TrackLocalWriter;
|
||||
use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability;
|
||||
|
||||
use crate::video::frame::VideoFrame;
|
||||
|
||||
@@ -56,7 +56,9 @@ impl VideoCodecType {
|
||||
|
||||
pub fn sdp_fmtp(&self) -> &'static str {
|
||||
match self {
|
||||
VideoCodecType::H264 => "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
||||
VideoCodecType::H264 => {
|
||||
"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f"
|
||||
}
|
||||
VideoCodecType::VP8 => "",
|
||||
VideoCodecType::VP9 => "profile-id=0",
|
||||
}
|
||||
@@ -156,10 +158,7 @@ impl VideoTrack {
|
||||
}
|
||||
|
||||
/// Start sending frames from a broadcast receiver
|
||||
pub async fn start_sending(
|
||||
&self,
|
||||
mut frame_rx: broadcast::Receiver<VideoFrame>,
|
||||
) {
|
||||
pub async fn start_sending(&self, mut frame_rx: broadcast::Receiver<VideoFrame>) {
|
||||
let _ = self.running.send(true);
|
||||
let track = self.track.clone();
|
||||
let stats = self.stats.clone();
|
||||
|
||||
@@ -18,7 +18,9 @@ use webrtc::peer_connection::configuration::RTCConfiguration;
|
||||
use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState;
|
||||
use webrtc::peer_connection::sdp::session_description::RTCSessionDescription;
|
||||
use webrtc::peer_connection::RTCPeerConnection;
|
||||
use webrtc::rtp_transceiver::rtp_codec::{RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType};
|
||||
use webrtc::rtp_transceiver::rtp_codec::{
|
||||
RTCRtpCodecCapability, RTCRtpCodecParameters, RTPCodecType,
|
||||
};
|
||||
use webrtc::rtp_transceiver::RTCPFeedback;
|
||||
|
||||
use super::config::WebRtcConfig;
|
||||
@@ -192,7 +194,8 @@ impl UniversalSession {
|
||||
clock_rate: 90000,
|
||||
channels: 0,
|
||||
// Match browser's fmtp format for profile-id=1
|
||||
sdp_fmtp_line: "level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST".to_owned(),
|
||||
sdp_fmtp_line: "level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST"
|
||||
.to_owned(),
|
||||
rtcp_feedback: video_rtcp_feedback.clone(),
|
||||
},
|
||||
payload_type: 49, // Use same payload type as browser
|
||||
@@ -200,7 +203,9 @@ impl UniversalSession {
|
||||
},
|
||||
RTPCodecType::Video,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to register H.265 codec: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!("Failed to register H.265 codec: {}", e))
|
||||
})?;
|
||||
|
||||
// Also register profile-id=2 (Main 10) variant
|
||||
media_engine
|
||||
@@ -210,7 +215,8 @@ impl UniversalSession {
|
||||
mime_type: MIME_TYPE_H265.to_owned(),
|
||||
clock_rate: 90000,
|
||||
channels: 0,
|
||||
sdp_fmtp_line: "level-id=180;profile-id=2;tier-flag=0;tx-mode=SRST".to_owned(),
|
||||
sdp_fmtp_line: "level-id=180;profile-id=2;tier-flag=0;tx-mode=SRST"
|
||||
.to_owned(),
|
||||
rtcp_feedback: video_rtcp_feedback,
|
||||
},
|
||||
payload_type: 51,
|
||||
@@ -218,7 +224,12 @@ impl UniversalSession {
|
||||
},
|
||||
RTPCodecType::Video,
|
||||
)
|
||||
.map_err(|e| AppError::VideoError(format!("Failed to register H.265 codec (profile 2): {}", e)))?;
|
||||
.map_err(|e| {
|
||||
AppError::VideoError(format!(
|
||||
"Failed to register H.265 codec (profile 2): {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
info!("Registered H.265/HEVC codec for session {}", session_id);
|
||||
}
|
||||
@@ -269,10 +280,9 @@ impl UniversalSession {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -291,7 +301,10 @@ impl UniversalSession {
|
||||
pc.add_track(audio.as_track_local())
|
||||
.await
|
||||
.map_err(|e| AppError::AudioError(format!("Failed to add audio track: {}", e)))?;
|
||||
info!("Opus audio track added to peer connection (session {})", session_id);
|
||||
info!(
|
||||
"Opus audio track added to peer connection (session {})",
|
||||
session_id
|
||||
);
|
||||
}
|
||||
|
||||
// Create state channel
|
||||
@@ -479,11 +492,13 @@ impl UniversalSession {
|
||||
&self,
|
||||
mut frame_rx: broadcast::Receiver<EncodedVideoFrame>,
|
||||
on_connected: F,
|
||||
)
|
||||
where
|
||||
) where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
info!("Starting {} session {} with shared encoder", self.codec, self.session_id);
|
||||
info!(
|
||||
"Starting {} session {} with shared encoder",
|
||||
self.codec, self.session_id
|
||||
);
|
||||
|
||||
let video_track = self.video_track.clone();
|
||||
let mut state_rx = self.state_rx.clone();
|
||||
@@ -492,7 +507,10 @@ impl UniversalSession {
|
||||
let expected_codec = self.codec;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
info!("Video receiver waiting for connection for session {}", session_id);
|
||||
info!(
|
||||
"Video receiver waiting for connection for session {}",
|
||||
session_id
|
||||
);
|
||||
|
||||
// Wait for Connected state before sending frames
|
||||
loop {
|
||||
@@ -500,7 +518,10 @@ impl UniversalSession {
|
||||
if current_state == ConnectionState::Connected {
|
||||
break;
|
||||
}
|
||||
if matches!(current_state, ConnectionState::Closed | ConnectionState::Failed) {
|
||||
if matches!(
|
||||
current_state,
|
||||
ConnectionState::Closed | ConnectionState::Failed
|
||||
) {
|
||||
info!("Session {} closed before connecting", session_id);
|
||||
return;
|
||||
}
|
||||
@@ -509,7 +530,10 @@ impl UniversalSession {
|
||||
}
|
||||
}
|
||||
|
||||
info!("Video receiver started for session {} (ICE connected)", session_id);
|
||||
info!(
|
||||
"Video receiver started for session {} (ICE connected)",
|
||||
session_id
|
||||
);
|
||||
|
||||
// Request keyframe now that connection is established
|
||||
on_connected();
|
||||
@@ -592,7 +616,10 @@ impl UniversalSession {
|
||||
}
|
||||
}
|
||||
|
||||
info!("Video receiver stopped for session {} (sent {} frames)", session_id, frames_sent);
|
||||
info!(
|
||||
"Video receiver stopped for session {} (sent {} frames)",
|
||||
session_id, frames_sent
|
||||
);
|
||||
});
|
||||
|
||||
*self.video_receiver_handle.lock().await = Some(handle);
|
||||
@@ -620,7 +647,10 @@ impl UniversalSession {
|
||||
if current_state == ConnectionState::Connected {
|
||||
break;
|
||||
}
|
||||
if matches!(current_state, ConnectionState::Closed | ConnectionState::Failed) {
|
||||
if matches!(
|
||||
current_state,
|
||||
ConnectionState::Closed | ConnectionState::Failed
|
||||
) {
|
||||
info!("Session {} closed before audio could start", session_id);
|
||||
return;
|
||||
}
|
||||
@@ -629,7 +659,10 @@ impl UniversalSession {
|
||||
}
|
||||
}
|
||||
|
||||
info!("Audio receiver started for session {} (ICE connected)", session_id);
|
||||
info!(
|
||||
"Audio receiver started for session {} (ICE connected)",
|
||||
session_id
|
||||
);
|
||||
|
||||
let mut packets_sent: u64 = 0;
|
||||
|
||||
@@ -673,7 +706,10 @@ impl UniversalSession {
|
||||
}
|
||||
}
|
||||
|
||||
info!("Audio receiver stopped for session {} (sent {} packets)", session_id, packets_sent);
|
||||
info!(
|
||||
"Audio receiver stopped for session {} (sent {} packets)",
|
||||
session_id, packets_sent
|
||||
);
|
||||
});
|
||||
|
||||
*self.audio_receiver_handle.lock().await = Some(handle);
|
||||
@@ -697,8 +733,7 @@ impl UniversalSession {
|
||||
|| offer.sdp.to_lowercase().contains("hevc");
|
||||
info!(
|
||||
"[SDP] Session {} offer contains H.265: {}",
|
||||
self.session_id,
|
||||
has_h265
|
||||
self.session_id, has_h265
|
||||
);
|
||||
if !has_h265 {
|
||||
warn!("[SDP] Browser offer does not include H.265 codec! Session may fail.");
|
||||
@@ -708,10 +743,9 @@ impl UniversalSession {
|
||||
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))
|
||||
})?;
|
||||
|
||||
let answer = self
|
||||
.pc
|
||||
@@ -725,8 +759,7 @@ impl UniversalSession {
|
||||
|| answer.sdp.to_lowercase().contains("hevc");
|
||||
info!(
|
||||
"[SDP] Session {} answer contains H.265: {}",
|
||||
self.session_id,
|
||||
has_h265
|
||||
self.session_id, has_h265
|
||||
);
|
||||
if !has_h265 {
|
||||
warn!("[SDP] Answer does not include H.265! Codec negotiation may have failed.");
|
||||
@@ -821,9 +854,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_encoder_type_to_video_codec() {
|
||||
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::H264), VideoCodec::H264);
|
||||
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::H265), VideoCodec::H265);
|
||||
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::VP8), VideoCodec::VP8);
|
||||
assert_eq!(encoder_type_to_video_codec(VideoEncoderType::VP9), VideoCodec::VP9);
|
||||
assert_eq!(
|
||||
encoder_type_to_video_codec(VideoEncoderType::H264),
|
||||
VideoCodec::H264
|
||||
);
|
||||
assert_eq!(
|
||||
encoder_type_to_video_codec(VideoEncoderType::H265),
|
||||
VideoCodec::H265
|
||||
);
|
||||
assert_eq!(
|
||||
encoder_type_to_video_codec(VideoEncoderType::VP8),
|
||||
VideoCodec::VP8
|
||||
);
|
||||
assert_eq!(
|
||||
encoder_type_to_video_codec(VideoEncoderType::VP9),
|
||||
VideoCodec::VP9
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user