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

@@ -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

View File

@@ -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};

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);
}

View File

@@ -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
)))
}
}
}

View File

@@ -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();

View File

@@ -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
);
}
}

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;
}
}