This commit is contained in:
mofeng-git
2025-12-28 18:19:16 +08:00
commit d143d158e4
771 changed files with 220548 additions and 0 deletions

View File

@@ -0,0 +1,938 @@
//! WebRTC Streamer - High-level WebRTC streaming manager
//!
//! This module provides a unified interface for WebRTC streaming mode,
//! supporting multiple video codecs (H264, VP8, VP9, H265) and audio (Opus).
//!
//! # Architecture
//!
//! ```text
//! WebRtcStreamer
//! |
//! +-- Video Pipeline
//! | +-- SharedVideoPipeline (single encoder for all sessions)
//! | +-- H264 Encoder
//! | +-- H265 Encoder (hardware only)
//! | +-- VP8 Encoder (hardware only - VAAPI)
//! | +-- VP9 Encoder (hardware only - VAAPI)
//! |
//! +-- Audio Pipeline
//! | +-- SharedAudioPipeline
//! | +-- OpusEncoder
//! |
//! +-- UniversalSession[] (video + audio tracks + DataChannel)
//! +-- UniversalVideoTrack (H264/H265/VP8/VP9)
//! +-- Audio Track (RTP/Opus)
//! +-- DataChannel (HID)
//! ```
//!
//! # Key Features
//!
//! - **Single encoder**: All sessions share one video encoder
//! - **Multi-codec support**: H264, H265, VP8, VP9
//! - **Audio support**: Opus audio streaming via SharedAudioPipeline
//! - **HID via DataChannel**: Keyboard/mouse events through WebRTC DataChannel
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};
use tracing::{debug, error, info, warn};
use crate::audio::shared_pipeline::{SharedAudioPipeline, SharedAudioPipelineConfig};
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::VideoCodecType;
use crate::video::format::{PixelFormat, Resolution};
use crate::video::frame::VideoFrame;
use crate::video::shared_video_pipeline::{SharedVideoPipeline, SharedVideoPipelineConfig, SharedVideoPipelineStats};
use super::config::{TurnServer, WebRtcConfig};
use super::signaling::{ConnectionState, IceCandidate, SdpAnswer, SdpOffer};
use super::universal_session::{UniversalSession, UniversalSessionConfig};
/// WebRTC streamer configuration
#[derive(Debug, Clone)]
pub struct WebRtcStreamerConfig {
/// WebRTC configuration (STUN/TURN servers, etc.)
pub webrtc: WebRtcConfig,
/// Video codec type
pub video_codec: VideoCodecType,
/// Input resolution
pub resolution: Resolution,
/// Input pixel format
pub input_format: PixelFormat,
/// Target bitrate in kbps
pub bitrate_kbps: u32,
/// Target FPS
pub fps: u32,
/// GOP size (keyframe interval)
pub gop_size: u32,
/// Enable audio (reserved)
pub audio_enabled: bool,
/// Encoder backend (None = auto select best available)
pub encoder_backend: Option<EncoderBackend>,
}
impl Default for WebRtcStreamerConfig {
fn default() -> Self {
Self {
webrtc: WebRtcConfig::default(),
video_codec: VideoCodecType::H264,
resolution: Resolution::HD720,
input_format: PixelFormat::Mjpeg,
bitrate_kbps: 8000,
fps: 30,
gop_size: 30,
audio_enabled: false,
encoder_backend: None,
}
}
}
/// WebRTC streamer statistics
#[derive(Debug, Clone, Default)]
pub struct WebRtcStreamerStats {
/// Number of active sessions
pub session_count: usize,
/// Current video codec
pub video_codec: String,
/// Video pipeline stats (if available)
pub video_pipeline: Option<VideoPipelineStats>,
/// Audio enabled
pub audio_enabled: bool,
/// Audio pipeline stats (if available)
pub audio_pipeline: Option<AudioPipelineStats>,
}
/// Video pipeline statistics
#[derive(Debug, Clone, Default)]
pub struct VideoPipelineStats {
pub frames_encoded: u64,
pub frames_dropped: u64,
pub bytes_encoded: u64,
pub keyframes_encoded: u64,
pub avg_encode_time_ms: f32,
pub current_fps: f32,
pub subscribers: u64,
}
/// Audio pipeline statistics
#[derive(Debug, Clone, Default)]
pub struct AudioPipelineStats {
pub frames_encoded: u64,
pub frames_dropped: u64,
pub bytes_encoded: u64,
pub avg_encode_time_ms: f32,
pub subscribers: u64,
}
/// Session info for listing
#[derive(Debug, Clone)]
pub struct SessionInfo {
pub session_id: String,
pub created_at: std::time::Instant,
pub state: String,
}
/// WebRTC Streamer
///
/// High-level manager for WebRTC streaming, supporting multiple video codecs
/// and audio streaming via Opus.
pub struct WebRtcStreamer {
/// Current configuration
config: RwLock<WebRtcStreamerConfig>,
// === Video ===
/// Current video codec type
video_codec: RwLock<VideoCodecType>,
/// Universal video pipeline (for all codecs)
video_pipeline: RwLock<Option<Arc<SharedVideoPipeline>>>,
/// All sessions (unified management)
sessions: Arc<RwLock<HashMap<String, Arc<UniversalSession>>>>,
/// Video frame source
video_frame_tx: RwLock<Option<broadcast::Sender<VideoFrame>>>,
// === Audio ===
/// Audio enabled flag
audio_enabled: RwLock<bool>,
/// Shared audio pipeline for Opus encoding
audio_pipeline: RwLock<Option<Arc<SharedAudioPipeline>>>,
/// Audio controller reference
audio_controller: RwLock<Option<Arc<AudioController>>>,
// === Controllers ===
/// HID controller for DataChannel
hid_controller: RwLock<Option<Arc<HidController>>>,
}
impl WebRtcStreamer {
/// Create a new WebRTC streamer
pub fn new() -> Arc<Self> {
Self::with_config(WebRtcStreamerConfig::default())
}
/// Create a new WebRTC streamer with configuration
pub fn with_config(config: WebRtcStreamerConfig) -> Arc<Self> {
Arc::new(Self {
config: RwLock::new(config.clone()),
video_codec: RwLock::new(config.video_codec),
video_pipeline: RwLock::new(None),
sessions: Arc::new(RwLock::new(HashMap::new())),
video_frame_tx: RwLock::new(None),
audio_enabled: RwLock::new(config.audio_enabled),
audio_pipeline: RwLock::new(None),
audio_controller: RwLock::new(None),
hid_controller: RwLock::new(None),
})
}
// === Video Codec Management ===
/// Get current video codec type
pub async fn current_video_codec(&self) -> VideoCodecType {
*self.video_codec.read().await
}
/// Set video codec type
///
/// Supports H264, H265, VP8, VP9. This will restart the video pipeline
/// and close all existing sessions.
pub async fn set_video_codec(&self, codec: VideoCodecType) -> Result<()> {
let current = *self.video_codec.read().await;
if current == codec {
return Ok(());
}
info!("Switching video codec from {:?} to {:?}", current, codec);
// Close all existing sessions
self.close_all_sessions().await;
// Stop current pipeline
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.stop();
}
*self.video_pipeline.write().await = None;
// Update codec
*self.video_codec.write().await = codec;
// Create new pipeline with new codec
if let Some(ref tx) = *self.video_frame_tx.read().await {
self.ensure_video_pipeline(tx.clone()).await?;
}
info!("Video codec switched to {:?}", codec);
Ok(())
}
/// Get list of supported video codecs
pub fn supported_video_codecs(&self) -> Vec<VideoCodecType> {
use crate::video::encoder::registry::EncoderRegistry;
let registry = EncoderRegistry::global();
let mut codecs = vec![];
// H264 always available (has software fallback)
codecs.push(VideoCodecType::H264);
// Check hardware codecs
if registry.is_format_available(VideoEncoderType::H265, true) {
codecs.push(VideoCodecType::H265);
}
if registry.is_format_available(VideoEncoderType::VP8, true) {
codecs.push(VideoCodecType::VP8);
}
if registry.is_format_available(VideoEncoderType::VP9, true) {
codecs.push(VideoCodecType::VP9);
}
codecs
}
/// Convert VideoCodecType to VideoEncoderType
fn codec_type_to_encoder_type(codec: VideoCodecType) -> VideoEncoderType {
match codec {
VideoCodecType::H264 => VideoEncoderType::H264,
VideoCodecType::H265 => VideoEncoderType::H265,
VideoCodecType::VP8 => VideoEncoderType::VP8,
VideoCodecType::VP9 => VideoEncoderType::VP9,
}
}
/// Ensure video pipeline is initialized and running
async fn ensure_video_pipeline(&self, tx: broadcast::Sender<VideoFrame>) -> Result<Arc<SharedVideoPipeline>> {
let mut pipeline_guard = self.video_pipeline.write().await;
if let Some(ref pipeline) = *pipeline_guard {
if pipeline.is_running() {
return Ok(pipeline.clone());
}
}
let config = self.config.read().await;
let codec = *self.video_codec.read().await;
let pipeline_config = SharedVideoPipelineConfig {
resolution: config.resolution,
input_format: config.input_format,
output_codec: Self::codec_type_to_encoder_type(codec),
bitrate_kbps: config.bitrate_kbps,
fps: config.fps,
gop_size: config.gop_size,
encoder_backend: config.encoder_backend,
};
info!("Creating shared video pipeline for {:?}", codec);
let pipeline = SharedVideoPipeline::new(pipeline_config)?;
pipeline.start(tx.subscribe()).await?;
*pipeline_guard = Some(pipeline.clone());
Ok(pipeline)
}
// === Audio Management ===
/// Check if audio is enabled
pub async fn is_audio_enabled(&self) -> bool {
*self.audio_enabled.read().await
}
/// Set audio enabled state
pub async fn set_audio_enabled(&self, enabled: bool) -> Result<()> {
let was_enabled = *self.audio_enabled.read().await;
*self.audio_enabled.write().await = enabled;
self.config.write().await.audio_enabled = enabled;
if enabled && !was_enabled {
// Start audio pipeline if we have an audio controller
if let Some(ref controller) = *self.audio_controller.read().await {
self.start_audio_pipeline(controller.clone()).await?;
}
} else if !enabled && was_enabled {
// Stop audio pipeline
self.stop_audio_pipeline().await;
}
info!("WebRTC audio enabled: {}", enabled);
Ok(())
}
/// Set audio controller reference
pub async fn set_audio_controller(&self, controller: Arc<AudioController>) {
info!("Setting audio controller for WebRTC streamer");
*self.audio_controller.write().await = Some(controller.clone());
// Start audio pipeline if audio is enabled
if *self.audio_enabled.read().await {
if let Err(e) = self.start_audio_pipeline(controller).await {
error!("Failed to start audio pipeline: {}", e);
}
}
}
/// Start the shared audio pipeline
async fn start_audio_pipeline(&self, controller: Arc<AudioController>) -> Result<()> {
// Check if already running
if let Some(ref pipeline) = *self.audio_pipeline.read().await {
if pipeline.is_running() {
debug!("Audio pipeline already running");
return Ok(());
}
}
// Get Opus frame receiver from audio controller
let _opus_rx = match controller.subscribe_opus_async().await {
Some(rx) => rx,
None => {
warn!("Audio controller not streaming, cannot start audio pipeline");
return Ok(());
}
};
// Create shared audio pipeline config
let config = SharedAudioPipelineConfig::default();
let pipeline = SharedAudioPipeline::new(config)?;
// Note: SharedAudioPipeline expects raw AudioFrame, but AudioController
// already provides encoded OpusFrame. We'll pass the OpusFrame directly
// to sessions instead of re-encoding.
// For now, store the pipeline reference for future use
*self.audio_pipeline.write().await = Some(pipeline);
// Reconnect audio for all existing sessions
self.reconnect_audio_sources().await;
info!("WebRTC audio pipeline started");
Ok(())
}
/// Stop the shared audio pipeline
async fn stop_audio_pipeline(&self) {
if let Some(ref pipeline) = *self.audio_pipeline.read().await {
pipeline.stop();
}
*self.audio_pipeline.write().await = None;
info!("WebRTC audio pipeline stopped");
}
/// Subscribe to encoded Opus frames (for sessions)
pub async fn subscribe_opus(&self) -> Option<broadcast::Receiver<OpusFrame>> {
if let Some(ref controller) = *self.audio_controller.read().await {
controller.subscribe_opus_async().await
} else {
None
}
}
/// Reconnect audio source for all existing sessions
/// Call this after audio controller restarts (e.g., quality change)
pub async fn reconnect_audio_sources(&self) {
if let Some(ref controller) = *self.audio_controller.read().await {
let sessions = self.sessions.read().await;
for (session_id, session) in sessions.iter() {
if session.has_audio() {
info!("Reconnecting audio for session {}", session_id);
if let Some(rx) = controller.subscribe_opus_async().await {
session.start_audio_from_opus(rx).await;
}
}
}
}
}
// === Video Frame Source ===
/// Set video frame source
pub async fn set_video_source(&self, tx: broadcast::Sender<VideoFrame>) {
info!(
"Setting video source for WebRTC streamer (receiver_count={})",
tx.receiver_count()
);
*self.video_frame_tx.write().await = Some(tx.clone());
// Start or restart pipeline if it exists
if let Some(ref pipeline) = *self.video_pipeline.read().await {
if !pipeline.is_running() {
info!("Starting video pipeline with new frame source");
if let Err(e) = pipeline.start(tx.subscribe()).await {
error!("Failed to start video pipeline: {}", e);
}
} else {
// Pipeline is already running but may have old frame source
// We need to restart it with the new frame source
info!("Video pipeline already running, restarting with new frame source");
pipeline.stop();
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
if let Err(e) = pipeline.start(tx.subscribe()).await {
error!("Failed to restart video pipeline: {}", e);
}
}
} else {
info!("No video pipeline exists yet, frame source will be used when pipeline is created");
}
}
/// Prepare for configuration change
///
/// This stops the encoding pipeline and closes all sessions.
pub async fn prepare_for_config_change(&self) {
// Stop pipeline and close sessions - will be recreated on next session
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.stop();
}
*self.video_pipeline.write().await = None;
self.close_all_sessions().await;
}
/// Reconnect video source after configuration change
pub async fn reconnect_video_source(&self, tx: broadcast::Sender<VideoFrame>) {
self.set_video_source(tx).await;
}
// === Configuration ===
/// Update video configuration
///
/// This will restart the encoding pipeline and close all sessions.
pub async fn update_video_config(
&self,
resolution: Resolution,
format: PixelFormat,
fps: u32,
) {
// Stop existing pipeline
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.stop();
}
*self.video_pipeline.write().await = None;
// 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);
}
// Update config
let mut config = self.config.write().await;
config.resolution = resolution;
config.input_format = format;
config.fps = fps;
// Scale bitrate based on resolution
let base_pixels: u64 = 1280 * 720;
let actual_pixels: u64 = resolution.width as u64 * resolution.height as u64;
config.bitrate_kbps = ((8000u64 * actual_pixels / base_pixels).max(1000).min(15000)) as u32;
info!(
"WebRTC config updated: {}x{} {:?} @ {} fps, {} kbps",
resolution.width, resolution.height, format, fps, config.bitrate_kbps
);
}
/// Update encoder backend (software/hardware selection)
pub async fn update_encoder_backend(&self, encoder_backend: Option<EncoderBackend>) {
// Stop existing pipeline
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.stop();
}
*self.video_pipeline.write().await = None;
// 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);
}
// Update config
let mut config = self.config.write().await;
config.encoder_backend = encoder_backend;
info!(
"WebRTC encoder backend updated: {:?}",
encoder_backend
);
}
/// Update ICE configuration (STUN/TURN servers)
///
/// Note: Changes take effect for new sessions only.
/// Existing sessions need to be reconnected to use the new ICE config.
pub async fn update_ice_config(
&self,
stun_server: Option<String>,
turn_server: Option<String>,
turn_username: Option<String>,
turn_password: Option<String>,
) {
let mut config = self.config.write().await;
// Update STUN servers
config.webrtc.stun_servers.clear();
if let Some(ref stun) = stun_server {
if !stun.is_empty() {
config.webrtc.stun_servers.push(stun.clone());
info!("WebRTC STUN server updated: {}", stun);
}
}
// Update TURN servers
config.webrtc.turn_servers.clear();
if let Some(ref turn) = turn_server {
if !turn.is_empty() {
let username = turn_username.unwrap_or_default();
let credential = turn_password.unwrap_or_default();
config.webrtc.turn_servers.push(TurnServer {
url: turn.clone(),
username: username.clone(),
credential,
});
info!("WebRTC TURN server updated: {} (user: {})", turn, username);
}
}
if config.webrtc.stun_servers.is_empty() && config.webrtc.turn_servers.is_empty() {
info!("WebRTC ICE config cleared - only host candidates will be used");
}
}
/// Set HID controller for DataChannel
pub async fn set_hid_controller(&self, hid: Arc<HidController>) {
*self.hid_controller.write().await = Some(hid);
}
// === Session Management ===
/// Create a new WebRTC session
pub async fn create_session(&self) -> Result<String> {
let session_id = uuid::Uuid::new_v4().to_string();
let codec = *self.video_codec.read().await;
// Ensure video pipeline is running
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?;
// Create session config
let config = self.config.read().await;
let session_config = UniversalSessionConfig {
webrtc: config.webrtc.clone(),
codec: Self::codec_type_to_encoder_type(codec),
resolution: config.resolution,
input_format: config.input_format,
bitrate_kbps: config.bitrate_kbps,
fps: config.fps,
gop_size: config.gop_size,
audio_enabled: *self.audio_enabled.read().await,
};
drop(config);
// Create universal session
let mut session = UniversalSession::new(session_config.clone(), session_id.clone()).await?;
// Set HID controller if available
if let Some(ref hid) = *self.hid_controller.read().await {
session.set_hid_controller(hid.clone());
}
// Create data channel
if self.config.read().await.webrtc.enable_datachannel {
session.create_data_channel("hid").await?;
}
let session = Arc::new(session);
// Subscribe to video pipeline frames
// 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;
// Start audio if enabled
if session_config.audio_enabled {
if let Some(ref controller) = *self.audio_controller.read().await {
if let Some(opus_rx) = controller.subscribe_opus_async().await {
session.start_audio_from_opus(opus_rx).await;
}
}
}
// Store session
self.sessions
.write()
.await
.insert(session_id.clone(), session);
info!(
"Session created: {} (codec={:?}, audio={}, {} total)",
session_id,
codec,
session_config.audio_enabled,
self.sessions.read().await.len()
);
Ok(session_id)
}
/// Handle SDP offer
pub async fn handle_offer(&self, session_id: &str, offer: SdpOffer) -> Result<SdpAnswer> {
let sessions = self.sessions.read().await;
let session = sessions
.get(session_id)
.ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))?;
session.handle_offer(offer).await
}
/// Add ICE candidate
pub async fn add_ice_candidate(&self, session_id: &str, candidate: IceCandidate) -> Result<()> {
let sessions = self.sessions.read().await;
let session = sessions
.get(session_id)
.ok_or_else(|| AppError::NotFound(format!("Session not found: {}", session_id)))?;
session.add_ice_candidate(candidate).await
}
/// Close a session
pub async fn close_session(&self, session_id: &str) -> Result<()> {
let session = self.sessions.write().await.remove(session_id);
if let Some(session) = session {
session.close().await?;
}
// Stop pipeline if no more sessions
if self.sessions.read().await.is_empty() {
if let Some(ref pipeline) = *self.video_pipeline.read().await {
info!("No more sessions, stopping video pipeline");
pipeline.stop();
}
}
Ok(())
}
/// Close all sessions
pub async fn close_all_sessions(&self) -> usize {
let mut sessions = self.sessions.write().await;
let count = sessions.len();
for (session_id, session) in sessions.drain() {
debug!("Closing session {}", session_id);
if let Err(e) = session.close().await {
warn!("Error closing session {}: {}", session_id, e);
}
}
// Stop pipeline
drop(sessions);
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.stop();
}
count
}
/// Get session count
pub async fn session_count(&self) -> usize {
self.sessions.read().await.len()
}
/// Get session info
pub async fn get_session(&self, session_id: &str) -> Option<SessionInfo> {
let sessions = self.sessions.read().await;
sessions.get(session_id).map(|s| SessionInfo {
session_id: s.session_id.clone(),
created_at: std::time::Instant::now(),
state: format!("{}", s.state()),
})
}
/// List all sessions
pub async fn list_sessions(&self) -> Vec<SessionInfo> {
self.sessions
.read()
.await
.values()
.map(|s| SessionInfo {
session_id: s.session_id.clone(),
created_at: std::time::Instant::now(),
state: format!("{}", s.state()),
})
.collect()
}
/// Cleanup closed sessions
pub async fn cleanup(&self) {
let to_remove: Vec<String> = {
let sessions = self.sessions.read().await;
sessions
.iter()
.filter(|(_, s)| {
matches!(
s.state(),
ConnectionState::Closed | ConnectionState::Failed | ConnectionState::Disconnected
)
})
.map(|(id, _)| id.clone())
.collect()
};
if !to_remove.is_empty() {
let mut sessions = self.sessions.write().await;
for id in &to_remove {
debug!("Removing closed session: {}", id);
sessions.remove(id);
}
// Stop pipeline if no more sessions
if sessions.is_empty() {
drop(sessions);
if let Some(ref pipeline) = *self.video_pipeline.read().await {
info!("No more sessions after cleanup, stopping video pipeline");
pipeline.stop();
}
}
}
}
// === Statistics ===
/// Get streamer statistics
pub async fn stats(&self) -> WebRtcStreamerStats {
let codec = *self.video_codec.read().await;
let session_count = self.session_count().await;
let video_pipeline = if let Some(ref pipeline) = *self.video_pipeline.read().await {
let s = pipeline.stats().await;
Some(VideoPipelineStats {
frames_encoded: s.frames_encoded,
frames_dropped: s.frames_dropped,
bytes_encoded: s.bytes_encoded,
keyframes_encoded: s.keyframes_encoded,
avg_encode_time_ms: s.avg_encode_time_ms,
current_fps: s.current_fps,
subscribers: s.subscribers,
})
} else {
None
};
// Get audio pipeline stats
let audio_pipeline = if let Some(ref pipeline) = *self.audio_pipeline.read().await {
let stats = pipeline.stats().await;
Some(AudioPipelineStats {
frames_encoded: stats.frames_encoded,
frames_dropped: stats.frames_dropped,
bytes_encoded: stats.bytes_encoded,
avg_encode_time_ms: stats.avg_encode_time_ms,
subscribers: stats.subscribers,
})
} else {
None
};
WebRtcStreamerStats {
session_count,
video_codec: format!("{:?}", codec),
video_pipeline,
audio_enabled: *self.audio_enabled.read().await,
audio_pipeline,
}
}
/// Get pipeline statistics
pub async fn pipeline_stats(&self) -> Option<SharedVideoPipelineStats> {
if let Some(ref pipeline) = *self.video_pipeline.read().await {
Some(pipeline.stats().await)
} else {
None
}
}
/// Set bitrate
///
/// Note: Hardware encoders (VAAPI, NVENC, etc.) don't support dynamic bitrate changes.
/// This method restarts the pipeline to apply the new bitrate.
pub async fn set_bitrate(&self, bitrate_kbps: u32) -> Result<()> {
// Update config first
self.config.write().await.bitrate_kbps = bitrate_kbps;
// Check if pipeline exists and is running
let pipeline_running = {
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.is_running()
} else {
false
}
};
if pipeline_running {
info!(
"Restarting video pipeline to apply new bitrate: {} kbps",
bitrate_kbps
);
// Stop existing pipeline
if let Some(ref pipeline) = *self.video_pipeline.read().await {
pipeline.stop();
}
// Wait for pipeline to stop
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Clear pipeline reference - will be recreated
*self.video_pipeline.write().await = None;
// Recreate pipeline with new config if we have a frame source
if let Some(ref tx) = *self.video_frame_tx.read().await {
// Get existing sessions that need to be reconnected
let session_ids: Vec<String> = self.sessions.read().await.keys().cloned().collect();
if !session_ids.is_empty() {
// Recreate pipeline
let pipeline = self.ensure_video_pipeline(tx.clone()).await?;
// Reconnect all sessions to new pipeline
let sessions = self.sessions.read().await;
for session_id in &session_ids {
if let Some(session) = sessions.get(session_id) {
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;
}
}
info!(
"Video pipeline restarted with {} kbps, reconnected {} sessions",
bitrate_kbps,
session_ids.len()
);
}
}
} else {
debug!(
"Pipeline not running, bitrate {} kbps will apply on next start",
bitrate_kbps
);
}
Ok(())
}
}
impl Default for WebRtcStreamer {
fn default() -> Self {
Self {
config: RwLock::new(WebRtcStreamerConfig::default()),
video_codec: RwLock::new(VideoCodecType::H264),
video_pipeline: RwLock::new(None),
sessions: Arc::new(RwLock::new(HashMap::new())),
video_frame_tx: RwLock::new(None),
audio_enabled: RwLock::new(false),
audio_pipeline: RwLock::new(None),
audio_controller: RwLock::new(None),
hid_controller: RwLock::new(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_webrtc_streamer_config_default() {
let config = WebRtcStreamerConfig::default();
assert_eq!(config.video_codec, VideoCodecType::H264);
assert_eq!(config.resolution, Resolution::HD720);
assert_eq!(config.bitrate_kbps, 8000);
assert_eq!(config.fps, 30);
assert!(!config.audio_enabled);
}
#[tokio::test]
async fn test_supported_codecs() {
let streamer = WebRtcStreamer::new();
let codecs = streamer.supported_video_codecs();
assert!(codecs.contains(&VideoCodecType::H264));
}
}