Files
One-KVM/src/video/stream_manager.rs
2026-07-06 20:51:58 +08:00

903 lines
32 KiB
Rust

//! Video Stream Manager
//!
//! Unified manager for video streaming that supports single-mode operation.
//! At any given time, only one streaming mode (MJPEG or WebRTC) is active.
//!
//! # Architecture
//!
//! ```text
//! VideoStreamManager (Public API - Single Entry Point)
//! │
//! ├── mode: StreamMode (current active mode)
//! │
//! ├── MJPEG Mode
//! │ └── Streamer ──► MjpegStreamHandler
//! │
//! └── WebRTC Mode
//! └── WebRtcStreamer ──► H264SessionManager
//! (Extensible: H264, VP8, VP9, H265)
//! ```
//!
//! # Design Goals
//!
//! 1. **Single Entry Point**: All video operations go through VideoStreamManager
//! 2. **Mode Isolation**: MJPEG and WebRTC modes are cleanly separated
//! 3. **Extensible Codecs**: WebRTC supports multiple video codecs (H264 now, others reserved)
//! 4. **Simplified API**: Complex configuration flows are encapsulated
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
use crate::config::{ConfigStore, StreamMode};
use crate::error::Result;
use crate::events::{EventBus, SystemEvent, VideoDeviceInfo};
use crate::hid::HidController;
use crate::stream::MjpegStreamHandler;
use crate::video::codec_constraints::StreamCodecConstraints;
use crate::video::format::{PixelFormat, Resolution};
use crate::video::streamer::{Streamer, StreamerState, StreamerStats};
use crate::video::traits::VideoOutput;
/// Video stream manager configuration
#[derive(Debug, Clone)]
pub struct StreamManagerConfig {
/// Initial streaming mode
pub mode: StreamMode,
/// Video device path
pub device: Option<String>,
/// Video format
pub format: PixelFormat,
/// Resolution
pub resolution: Resolution,
/// FPS
pub fps: u32,
}
/// Result of a mode switch request.
#[derive(Debug, Clone)]
pub struct ModeSwitchTransaction {
/// Whether this request started a new switch.
pub accepted: bool,
/// Whether a switch is currently in progress after handling this request.
pub switching: bool,
/// Transition ID if a switch is/was in progress.
pub transition_id: Option<String>,
}
impl Default for StreamManagerConfig {
fn default() -> Self {
Self {
mode: StreamMode::Mjpeg,
device: None,
format: PixelFormat::Mjpeg,
resolution: Resolution::HD1080,
fps: 30,
}
}
}
/// Unified video stream manager
///
/// Manages both MJPEG and WebRTC streaming modes, ensuring only one is active
/// at any given time. This reduces resource usage and simplifies the architecture.
///
/// # Components
///
/// - **Streamer**: Handles video capture and MJPEG distribution (current implementation)
/// - **WebRtcStreamer**: High-level WebRTC manager with multi-codec support (new)
/// - **H264SessionManager**: Legacy WebRTC manager (for backward compatibility)
pub struct VideoStreamManager {
/// Current streaming mode
mode: RwLock<StreamMode>,
/// MJPEG streamer (handles video capture and MJPEG distribution)
streamer: Arc<Streamer>,
/// WebRTC output (unified WebRTC manager with multi-codec support)
webrtc_streamer: Arc<dyn VideoOutput>,
/// Event bus for notifications
events: RwLock<Option<Arc<EventBus>>>,
/// Configuration store
config_store: RwLock<Option<ConfigStore>>,
/// Mode switching lock to prevent concurrent switch requests
switching: AtomicBool,
/// Current mode switch transaction ID (set while switching=true)
transition_id: RwLock<Option<String>>,
}
impl VideoStreamManager {
/// Create a new video stream manager with WebRtcStreamer
pub fn with_webrtc_streamer(
streamer: Arc<Streamer>,
webrtc_streamer: Arc<dyn VideoOutput>,
) -> Arc<Self> {
Arc::new(Self {
mode: RwLock::new(StreamMode::Mjpeg),
streamer,
webrtc_streamer,
events: RwLock::new(None),
config_store: RwLock::new(None),
switching: AtomicBool::new(false),
transition_id: RwLock::new(None),
})
}
/// Check if mode switching is in progress
pub fn is_switching(&self) -> bool {
self.switching.load(Ordering::SeqCst)
}
/// Get current mode switch transition ID, if any
pub async fn current_transition_id(&self) -> Option<String> {
self.transition_id.read().await.clone()
}
/// Set event bus for notifications
pub async fn set_event_bus(&self, events: Arc<EventBus>) {
*self.events.write().await = Some(events.clone());
self.webrtc_streamer.set_event_bus(events).await;
}
/// Set configuration store
pub async fn set_config_store(&self, config: ConfigStore) {
*self.config_store.write().await = Some(config);
}
/// Get current stream codec constraints derived from global configuration.
pub async fn codec_constraints(&self) -> StreamCodecConstraints {
if let Some(ref config_store) = *self.config_store.read().await {
let config = config_store.get();
StreamCodecConstraints::from_config(&config)
} else {
StreamCodecConstraints::unrestricted()
}
}
/// Get current streaming mode
pub async fn current_mode(&self) -> StreamMode {
self.mode.read().await.clone()
}
/// Check if MJPEG mode is active
pub async fn is_mjpeg_enabled(&self) -> bool {
*self.mode.read().await == StreamMode::Mjpeg
}
/// Check if WebRTC mode is active
pub async fn is_webrtc_enabled(&self) -> bool {
*self.mode.read().await == StreamMode::WebRTC
}
/// Get the underlying streamer (for MJPEG mode)
pub fn streamer(&self) -> Arc<Streamer> {
self.streamer.clone()
}
/// Get the MJPEG stream handler
pub fn mjpeg_handler(&self) -> Arc<MjpegStreamHandler> {
self.streamer.mjpeg_handler()
}
/// Initialize with a specific mode
pub async fn init_with_mode(self: &Arc<Self>, mode: StreamMode) -> Result<()> {
info!("Initializing video stream manager with mode: {:?}", mode);
*self.mode.write().await = mode.clone();
// Check if streamer is already initialized (capturer exists)
let needs_init = self.streamer.state().await == StreamerState::Uninitialized;
if needs_init {
match mode {
StreamMode::Mjpeg => {
// Initialize MJPEG streamer
if let Err(e) = self.streamer.init_auto().await {
warn!("Failed to auto-initialize MJPEG streamer: {}", e);
}
}
StreamMode::WebRTC => {
// WebRTC is initialized on-demand when clients connect
// But we still need to initialize the video capture
if let Err(e) = self.streamer.init_auto().await {
warn!("Failed to auto-initialize video capture for WebRTC: {}", e);
}
}
}
}
self.sync_webrtc_capture_source("after init").await;
Ok(())
}
/// Switch streaming mode
///
/// This will:
/// 1. Acquire switching lock (prevent concurrent switches)
/// 2. Notify clients of the mode change
/// 3. Stop the current mode
/// 4. Start the new mode (ensuring video capture runs for WebRTC)
/// 5. Update configuration
pub async fn switch_mode(self: &Arc<Self>, new_mode: StreamMode) -> Result<()> {
let _ = self.switch_mode_transaction(new_mode).await?;
Ok(())
}
/// Switch streaming mode with a transaction ID for correlating events
///
/// If a switch is already in progress, returns `accepted=false` with the
/// current `transition_id` (if known) and does not start a new switch.
pub async fn switch_mode_transaction(
self: &Arc<Self>,
new_mode: StreamMode,
) -> Result<ModeSwitchTransaction> {
let current_mode = self.mode.read().await.clone();
if current_mode == new_mode {
debug!("Already in {:?} mode, no switch needed", new_mode);
// Even if mode is the same, ensure video capture is running for WebRTC
if new_mode == StreamMode::WebRTC {
self.ensure_video_capture_running().await?;
}
return Ok(ModeSwitchTransaction {
accepted: false,
switching: false,
transition_id: None,
});
}
// Acquire switching lock - prevent concurrent switch requests
if self
.switching
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
debug!("Mode switch already in progress, ignoring duplicate request");
return Ok(ModeSwitchTransaction {
accepted: false,
switching: true,
transition_id: self.transition_id.read().await.clone(),
});
}
let transition_id = Uuid::new_v4().to_string();
*self.transition_id.write().await = Some(transition_id.clone());
// Publish transaction start event
let from_mode_str = self.mode_to_string(&current_mode).await;
let to_mode_str = self.mode_to_string(&new_mode).await;
self.publish_event(SystemEvent::StreamModeSwitching {
transition_id: transition_id.clone(),
to_mode: to_mode_str,
from_mode: from_mode_str,
})
.await;
// Perform the switch asynchronously so the HTTP handler can return
// immediately and clients can reliably wait for WebSocket events.
let manager = Arc::clone(self);
let transition_id_for_task = transition_id.clone();
tokio::spawn(async move {
let result = manager
.do_switch_mode(current_mode, new_mode, transition_id_for_task.clone())
.await;
if let Err(e) = result {
error!(
"Mode switch transaction {} failed: {}",
transition_id_for_task, e
);
}
// Publish transaction end marker with best-effort actual mode
let actual_mode = manager.mode.read().await.clone();
let actual_mode_str = manager.mode_to_string(&actual_mode).await;
manager
.publish_event(SystemEvent::StreamModeReady {
transition_id: transition_id_for_task.clone(),
mode: actual_mode_str,
})
.await;
*manager.transition_id.write().await = None;
manager.switching.store(false, Ordering::SeqCst);
});
Ok(ModeSwitchTransaction {
accepted: true,
switching: true,
transition_id: Some(transition_id),
})
}
async fn mode_to_string(&self, mode: &StreamMode) -> String {
match mode {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = self.webrtc_streamer.current_video_codec().await;
codec_to_string(codec)
}
}
}
/// Ensure video capture is running (for WebRTC mode)
async fn ensure_video_capture_running(self: &Arc<Self>) -> Result<()> {
// Initialize streamer if not already initialized (for config discovery)
if self.streamer.state().await == StreamerState::Uninitialized {
info!("Initializing video capture for WebRTC (ensure)");
if let Err(e) = self.streamer.init_auto().await {
error!("Failed to initialize video capture: {}", e);
return Err(e);
}
}
self.sync_webrtc_capture_source("for WebRTC ensure").await;
Ok(())
}
async fn sync_webrtc_capture_source(&self, reason: &str) {
let (device_path, resolution, format, fps, jpeg_quality) =
self.streamer.current_capture_config().await;
info!(
"Syncing WebRTC capture source {}: {}x{} {:?} @ {}fps",
reason, resolution.width, resolution.height, format, fps
);
self.webrtc_streamer
.update_video_config(resolution, format, fps)
.await;
if let Some(device_path) = device_path {
// Resolve the paired subdev so the WebRTC pipeline can run the
// RK628 STREAMON gate + SOURCE_CHANGE polling identically to the
// MJPEG path. See `csi_bridge::discover_subdev_for_video`.
let (subdev_path, bridge_kind, v4l2_driver) = self
.streamer
.current_device()
.await
.map(|d| {
(
d.subdev_path.clone(),
d.bridge_kind.clone(),
Some(d.driver.clone()),
)
})
.unwrap_or((None, None, None));
self.webrtc_streamer
.set_capture_device(
device_path,
jpeg_quality,
subdev_path,
bridge_kind,
v4l2_driver,
)
.await;
} else {
warn!("No capture device configured while syncing WebRTC capture source");
}
}
/// Internal implementation of mode switching (called with lock held)
async fn do_switch_mode(
self: &Arc<Self>,
current_mode: StreamMode,
new_mode: StreamMode,
transition_id: String,
) -> Result<()> {
info!("Switching video mode: {:?} -> {:?}", current_mode, new_mode);
// Get the actual mode strings (with codec info for WebRTC)
let new_mode_str = match &new_mode {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = self.webrtc_streamer.current_video_codec().await;
codec_to_string(codec)
}
};
let previous_mode_str = match &current_mode {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = self.webrtc_streamer.current_video_codec().await;
codec_to_string(codec)
}
};
// 1. Publish mode change event (clients should prepare to reconnect)
self.publish_event(SystemEvent::StreamModeChanged {
transition_id: Some(transition_id.clone()),
mode: new_mode_str,
previous_mode: previous_mode_str,
})
.await;
// 2. Stop current mode
match current_mode {
StreamMode::Mjpeg => {
info!("Stopping MJPEG streaming");
self.streamer.mjpeg_handler().set_offline();
if let Err(e) = self.streamer.stop().await {
warn!("Error stopping MJPEG streamer: {}", e);
}
}
StreamMode::WebRTC => {
info!("Closing all WebRTC sessions and releasing capture device");
let closed = self
.webrtc_streamer
.close_all_sessions_and_release_device()
.await;
if closed > 0 {
info!("Closed {} WebRTC sessions", closed);
}
}
}
// 3. Update mode
*self.mode.write().await = new_mode.clone();
// 4. Start new mode
match new_mode {
StreamMode::Mjpeg => {
info!("Starting MJPEG streaming");
if let Err(e) = self.streamer.start().await {
error!("Failed to start MJPEG streamer: {}", e);
return Err(e);
}
}
StreamMode::WebRTC => {
// WebRTC mode: configure direct capture for encoder pipeline
info!("Activating WebRTC mode");
if self.streamer.state().await == StreamerState::Uninitialized {
info!("Initializing video capture for WebRTC");
if let Err(e) = self.streamer.init_auto().await {
error!("Failed to initialize video capture for WebRTC: {}", e);
return Err(e);
}
}
self.sync_webrtc_capture_source("for WebRTC mode").await;
let codec = self.webrtc_streamer.current_video_codec().await;
let is_hardware = self.webrtc_streamer.is_hardware_encoding().await;
self.publish_event(SystemEvent::WebRTCReady {
transition_id: Some(transition_id.clone()),
codec: codec_to_string(codec),
hardware: is_hardware,
})
.await;
info!("WebRTC mode activated (sessions created on-demand)");
}
}
// 5. Update configuration store if available
if let Some(ref config_store) = *self.config_store.read().await {
let mut config = (*config_store.get()).clone();
config.stream.mode = new_mode.clone();
if let Err(e) = config_store.set(config).await {
warn!("Failed to persist stream mode to config: {}", e);
}
}
info!("Video mode switched to {:?}", new_mode);
Ok(())
}
/// Apply video configuration (device, format, resolution, fps)
///
/// This is called when video settings change. It will restart the
/// appropriate streaming pipeline based on current mode.
pub async fn apply_video_config(
self: &Arc<Self>,
device_path: &str,
format: PixelFormat,
resolution: Resolution,
fps: u32,
) -> Result<()> {
let mode = self.mode.read().await.clone();
info!(
"Applying video config: {} {:?} {}x{} @ {} fps (mode: {:?})",
device_path, format, resolution.width, resolution.height, fps, mode
);
if mode == StreamMode::WebRTC {
// Stop the shared pipeline before replacing the capture source so WebRTC
// sessions do not stay attached to a stale frame source.
self.webrtc_streamer
.update_video_config(resolution, format, fps)
.await;
info!("WebRTC streamer config updated (pipeline stopped, sessions closed)");
}
// Apply to streamer (handles video capture)
self.streamer
.apply_video_config(device_path, format, resolution, fps)
.await?;
if mode != StreamMode::WebRTC {
if let Err(e) = self.start().await {
error!("Failed to start streamer after config change: {}", e);
} else {
info!("Streamer started after config change");
}
}
// Update WebRTC config if in WebRTC mode
if mode == StreamMode::WebRTC {
let (device_path, actual_resolution, actual_format, actual_fps, jpeg_quality) =
self.streamer.current_capture_config().await;
if actual_format != format || actual_resolution != resolution || actual_fps != fps {
info!(
"Actual capture config differs from requested, updating WebRTC: {}x{} {:?} @ {}fps",
actual_resolution.width, actual_resolution.height, actual_format, actual_fps
);
self.webrtc_streamer
.update_video_config(actual_resolution, actual_format, actual_fps)
.await;
}
if let Some(device_path) = device_path {
info!("Configuring direct capture for WebRTC after config change");
let (subdev_path, bridge_kind, v4l2_driver) = self
.streamer
.current_device()
.await
.map(|d| {
(
d.subdev_path.clone(),
d.bridge_kind.clone(),
Some(d.driver.clone()),
)
})
.unwrap_or((None, None, None));
self.webrtc_streamer
.set_capture_device(
device_path,
jpeg_quality,
subdev_path,
bridge_kind,
v4l2_driver,
)
.await;
} else {
warn!("No capture device configured for WebRTC after config change");
}
let codec = self.webrtc_streamer.current_video_codec().await;
let is_hardware = self.webrtc_streamer.is_hardware_encoding().await;
self.publish_event(SystemEvent::WebRTCReady {
transition_id: None,
codec: codec_to_string(codec),
hardware: is_hardware,
})
.await;
}
Ok(())
}
/// Start streaming (based on current mode)
pub async fn start(self: &Arc<Self>) -> Result<()> {
let mode = self.mode.read().await.clone();
match mode {
StreamMode::Mjpeg => {
self.streamer.start().await?;
}
StreamMode::WebRTC => {
// Ensure device is initialized for config discovery
if self.streamer.state().await == StreamerState::Uninitialized {
self.streamer.init_auto().await?;
}
self.sync_webrtc_capture_source("before start").await;
}
}
Ok(())
}
/// Stop streaming
pub async fn stop(&self) -> Result<()> {
let mode = self.mode.read().await.clone();
match mode {
StreamMode::Mjpeg => {
self.streamer.stop().await?;
}
StreamMode::WebRTC => {
self.webrtc_streamer.close_all_sessions().await;
self.streamer.stop().await?;
}
}
Ok(())
}
/// Get video device info for device_info event
pub async fn get_video_info(&self) -> VideoDeviceInfo {
let stats = self.stats().await;
let state = self.streamer.state().await;
let device = self.streamer.current_device().await;
let mode = self.mode.read().await.clone();
// For WebRTC mode, return specific codec type (h264, h265, vp8, vp9)
// instead of generic "webrtc" to prevent frontend from defaulting to h264
let stream_mode = match &mode {
StreamMode::Mjpeg => "mjpeg".to_string(),
StreamMode::WebRTC => {
let codec = self.webrtc_streamer.current_video_codec().await;
codec_to_string(codec)
}
};
VideoDeviceInfo {
available: state != StreamerState::Uninitialized,
device: device.map(|d| d.path.display().to_string()),
format: stats.format,
resolution: stats.resolution,
fps: stats.target_fps,
online: state == StreamerState::Streaming,
stream_mode,
config_changing: self.streamer.is_config_changing(),
error: if state == StreamerState::Error {
Some("Video stream error".to_string())
} else if state.is_no_signal_like() {
Some("No video signal".to_string())
} else {
None
},
}
}
/// Get MJPEG client count
pub fn mjpeg_client_count(&self) -> u64 {
self.streamer.mjpeg_handler().client_count()
}
/// Get WebRTC session count
pub async fn webrtc_session_count(&self) -> usize {
self.webrtc_streamer.session_count().await
}
/// Set HID controller for WebRTC DataChannel
pub async fn set_hid_controller(&self, hid: Arc<HidController>) {
self.webrtc_streamer.set_hid_controller(hid).await;
}
/// Set audio enabled state for WebRTC
pub async fn set_webrtc_audio_enabled(&self, enabled: bool) -> Result<()> {
self.webrtc_streamer.set_audio_enabled(enabled).await
}
/// Check if WebRTC audio is enabled
pub async fn is_webrtc_audio_enabled(&self) -> bool {
self.webrtc_streamer.is_audio_enabled().await
}
/// Reconnect audio sources for all WebRTC sessions
/// Call this after audio controller restarts (e.g., quality change)
pub async fn reconnect_webrtc_audio_sources(&self) {
self.webrtc_streamer.reconnect_audio_sources().await;
}
// =========================================================================
// Delegated methods from Streamer (for backward compatibility)
// =========================================================================
/// List available video devices
pub async fn list_devices(
&self,
) -> crate::error::Result<Vec<crate::video::device::VideoDeviceInfo>> {
self.streamer.list_devices().await
}
/// Get streamer statistics
///
/// In WebRTC mode, resolution/format/target_fps/fps reflect
/// [`WebRtcStreamer`]'s config (updated after DV negotiation / geometry sync),
/// not only the MJPEG [`Streamer`] snapshot — so `/api/stream/status` matches
/// what the shared encoder actually uses.
pub async fn stats(&self) -> StreamerStats {
let mut s = self.streamer.stats().await;
if *self.mode.read().await == StreamMode::WebRTC {
let (res, fmt, tgt_fps) = self.webrtc_streamer.current_video_geometry().await;
s.format = Some(fmt.to_string());
s.resolution = Some((res.width, res.height));
s.target_fps = tgt_fps;
if let Some(ps) = self.webrtc_streamer.pipeline_stats().await {
s.fps = ps.current_fps;
}
s.clients = self.webrtc_streamer.session_count().await as u64;
}
s
}
/// Check if config is being changed
pub fn is_config_changing(&self) -> bool {
self.streamer.is_config_changing()
}
/// Check if streaming is active
pub async fn is_streaming(&self) -> bool {
self.streamer.is_streaming().await
}
/// Subscribe to encoded video frames from the shared video pipeline
///
/// This allows RustDesk (and other consumers) to receive H264/H265/VP8/VP9
/// encoded frames without running a separate encoder. The encoding is shared
/// with WebRTC sessions.
///
/// This method ensures video capture is running before subscribing.
/// Returns None if video capture cannot be started or pipeline creation fails.
pub async fn subscribe_encoded_frames(
&self,
) -> Option<
tokio::sync::mpsc::Receiver<std::sync::Arc<crate::video::pipeline::EncodedVideoFrame>>,
> {
// 1. Ensure video capture is initialized (for config discovery)
if self.streamer.state().await == StreamerState::Uninitialized {
tracing::info!("Initializing video capture for encoded frame subscription");
if let Err(e) = self.streamer.init_auto().await {
tracing::error!(
"Failed to initialize video capture for encoded frames: {}",
e
);
return None;
}
}
// 2. Synchronize WebRTC config with capture config
let (device_path, _, _, _, _) = self.streamer.current_capture_config().await;
self.sync_webrtc_capture_source("for encoded frame subscription")
.await;
if device_path.is_none() {
return None;
}
// 3. Use WebRtcStreamer to ensure the shared video pipeline is running
match self
.webrtc_streamer
.ensure_video_pipeline_for_external()
.await
{
Ok(pipeline) => Some(pipeline.subscribe()),
Err(e) => {
tracing::error!("Failed to start shared video pipeline: {}", e);
None
}
}
}
/// Get the current video encoding configuration from the shared pipeline
pub async fn get_encoding_config(
&self,
) -> Option<crate::video::pipeline::SharedVideoPipelineConfig> {
self.webrtc_streamer.get_pipeline_config().await
}
/// Get current video codec type
pub async fn current_video_codec(&self) -> crate::video::codec::VideoCodecType {
self.webrtc_streamer.current_video_codec().await
}
/// Check if hardware encoding is in use
pub async fn is_hardware_encoding(&self) -> bool {
self.webrtc_streamer.is_hardware_encoding().await
}
/// Set video codec for the shared video pipeline
///
/// This allows external consumers (like RustDesk) to set the video codec
/// before subscribing to encoded frames.
pub async fn set_video_codec(
&self,
codec: crate::video::codec::VideoCodecType,
) -> crate::error::Result<()> {
self.webrtc_streamer.set_video_codec(codec).await
}
/// Set bitrate preset for the shared video pipeline
///
/// This allows external consumers (like RustDesk) to adjust the video quality
/// based on client preferences.
pub async fn set_bitrate_preset(
&self,
preset: crate::video::codec::BitratePreset,
) -> crate::error::Result<()> {
self.webrtc_streamer.set_bitrate_preset(preset).await
}
/// Request a keyframe from the shared video pipeline
pub async fn request_keyframe(&self) -> crate::error::Result<()> {
self.webrtc_streamer.request_keyframe().await
}
/// Notify frontend about a codec-only switch (WebRTC mode unchanged, codec changed).
///
/// `set_video_codec` already rebuilt the pipeline synchronously, so we just
/// emit the events the frontend waits on: `StreamModeChanged`, `WebRTCReady`,
/// and `StreamModeReady`.
///
/// Events are spawned asynchronously so the HTTP response (carrying the
/// `transition_id`) reaches the client before the WebSocket events, giving
/// the frontend time to call `registerTransition()` first.
pub async fn notify_codec_switch(
self: &Arc<Self>,
transition_id: &str,
new_codec_str: &str,
previous_codec_str: &str,
) {
let manager = Arc::clone(self);
let transition_id = transition_id.to_string();
let new_codec = new_codec_str.to_string();
let prev_codec = previous_codec_str.to_string();
tokio::spawn(async move {
// Small yield to ensure the HTTP response is flushed first.
tokio::task::yield_now().await;
manager
.publish_event(SystemEvent::StreamModeChanged {
transition_id: Some(transition_id.clone()),
mode: new_codec.clone(),
previous_mode: prev_codec.clone(),
})
.await;
let is_hardware = manager.webrtc_streamer.is_hardware_encoding().await;
manager
.publish_event(SystemEvent::WebRTCReady {
transition_id: Some(transition_id.clone()),
codec: new_codec.clone(),
hardware: is_hardware,
})
.await;
manager
.publish_event(SystemEvent::StreamModeReady {
transition_id: transition_id.clone(),
mode: new_codec.clone(),
})
.await;
info!(
"Codec switch notified: {} -> {} (transition: {})",
prev_codec, new_codec, transition_id
);
});
}
/// Publish event to event bus
async fn publish_event(&self, event: SystemEvent) {
if let Some(ref events) = *self.events.read().await {
events.publish(event);
}
}
}
/// Convert VideoCodecType to lowercase string for frontend
fn codec_to_string(codec: crate::video::codec::VideoCodecType) -> String {
match codec {
crate::video::codec::VideoCodecType::H264 => "h264".to_string(),
crate::video::codec::VideoCodecType::H265 => "h265".to_string(),
crate::video::codec::VideoCodecType::VP8 => "vp8".to_string(),
crate::video::codec::VideoCodecType::VP9 => "vp9".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::video::codec::VideoCodecType;
#[test]
fn test_codec_to_string() {
assert_eq!(codec_to_string(VideoCodecType::H264), "h264");
assert_eq!(codec_to_string(VideoCodecType::H265), "h265");
assert_eq!(codec_to_string(VideoCodecType::VP8), "vp8");
assert_eq!(codec_to_string(VideoCodecType::VP9), "vp9");
}
}