mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-07-29 15:31:46 +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:
@@ -12,8 +12,8 @@ use std::time::{Duration, Instant};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::video::encoder::JpegEncoder;
|
||||
use crate::video::encoder::traits::{Encoder, EncoderConfig};
|
||||
use crate::video::encoder::JpegEncoder;
|
||||
use crate::video::format::PixelFormat;
|
||||
use crate::video::VideoFrame;
|
||||
|
||||
@@ -256,7 +256,10 @@ impl MjpegStreamHandler {
|
||||
let config = EncoderConfig::jpeg(resolution, 85);
|
||||
match JpegEncoder::new(config) {
|
||||
Ok(enc) => {
|
||||
debug!("Created JPEG encoder for MJPEG stream: {}x{}", resolution.width, resolution.height);
|
||||
debug!(
|
||||
"Created JPEG encoder for MJPEG stream: {}x{}",
|
||||
resolution.width, resolution.height
|
||||
);
|
||||
enc
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -270,37 +273,40 @@ impl MjpegStreamHandler {
|
||||
|
||||
// Check if resolution changed
|
||||
if encoder.config().resolution != resolution {
|
||||
debug!("Resolution changed, recreating JPEG encoder: {}x{}", resolution.width, resolution.height);
|
||||
debug!(
|
||||
"Resolution changed, recreating JPEG encoder: {}x{}",
|
||||
resolution.width, resolution.height
|
||||
);
|
||||
let config = EncoderConfig::jpeg(resolution, 85);
|
||||
*encoder = JpegEncoder::new(config).map_err(|e| format!("Failed to create encoder: {}", e))?;
|
||||
*encoder =
|
||||
JpegEncoder::new(config).map_err(|e| format!("Failed to create encoder: {}", e))?;
|
||||
}
|
||||
|
||||
// Encode based on input format
|
||||
let encoded = match frame.format {
|
||||
PixelFormat::Yuyv => {
|
||||
encoder.encode_yuyv(frame.data(), sequence)
|
||||
.map_err(|e| format!("YUYV encode failed: {}", e))?
|
||||
}
|
||||
PixelFormat::Nv12 => {
|
||||
encoder.encode_nv12(frame.data(), sequence)
|
||||
.map_err(|e| format!("NV12 encode failed: {}", e))?
|
||||
}
|
||||
PixelFormat::Rgb24 => {
|
||||
encoder.encode_rgb(frame.data(), sequence)
|
||||
.map_err(|e| format!("RGB encode failed: {}", e))?
|
||||
}
|
||||
PixelFormat::Bgr24 => {
|
||||
encoder.encode_bgr(frame.data(), sequence)
|
||||
.map_err(|e| format!("BGR encode failed: {}", e))?
|
||||
}
|
||||
PixelFormat::Yuyv => encoder
|
||||
.encode_yuyv(frame.data(), sequence)
|
||||
.map_err(|e| format!("YUYV encode failed: {}", e))?,
|
||||
PixelFormat::Nv12 => encoder
|
||||
.encode_nv12(frame.data(), sequence)
|
||||
.map_err(|e| format!("NV12 encode failed: {}", e))?,
|
||||
PixelFormat::Rgb24 => encoder
|
||||
.encode_rgb(frame.data(), sequence)
|
||||
.map_err(|e| format!("RGB encode failed: {}", e))?,
|
||||
PixelFormat::Bgr24 => encoder
|
||||
.encode_bgr(frame.data(), sequence)
|
||||
.map_err(|e| format!("BGR encode failed: {}", e))?,
|
||||
_ => {
|
||||
return Err(format!("Unsupported format for JPEG encoding: {}", frame.format));
|
||||
return Err(format!(
|
||||
"Unsupported format for JPEG encoding: {}",
|
||||
frame.format
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
// Create new VideoFrame with JPEG data
|
||||
Ok(VideoFrame::from_vec(
|
||||
encoded.data.to_vec(),
|
||||
// Create new VideoFrame with JPEG data (zero-copy: Bytes -> Arc<Bytes>)
|
||||
Ok(VideoFrame::new(
|
||||
encoded.data,
|
||||
resolution,
|
||||
PixelFormat::Mjpeg,
|
||||
0, // stride not relevant for JPEG
|
||||
@@ -333,7 +339,11 @@ impl MjpegStreamHandler {
|
||||
pub fn register_client(&self, client_id: ClientId) {
|
||||
let session = ClientSession::new(client_id.clone());
|
||||
self.clients.write().insert(client_id.clone(), session);
|
||||
info!("Client {} connected (total: {})", client_id, self.client_count());
|
||||
info!(
|
||||
"Client {} connected (total: {})",
|
||||
client_id,
|
||||
self.client_count()
|
||||
);
|
||||
}
|
||||
|
||||
/// Unregister a client
|
||||
@@ -391,7 +401,9 @@ impl MjpegStreamHandler {
|
||||
*self.auto_pause_config.write() = config;
|
||||
info!(
|
||||
"Auto-pause config updated: enabled={}, delay={}s, timeout={}s",
|
||||
config_clone.enabled, config_clone.shutdown_delay_secs, config_clone.client_timeout_secs
|
||||
config_clone.enabled,
|
||||
config_clone.shutdown_delay_secs,
|
||||
config_clone.client_timeout_secs
|
||||
);
|
||||
}
|
||||
|
||||
@@ -440,10 +452,7 @@ impl ClientGuard {
|
||||
/// Create a new client guard
|
||||
pub fn new(client_id: ClientId, handler: Arc<MjpegStreamHandler>) -> Self {
|
||||
handler.register_client(client_id.clone());
|
||||
Self {
|
||||
client_id,
|
||||
handler,
|
||||
}
|
||||
Self { client_id, handler }
|
||||
}
|
||||
|
||||
/// Get client ID
|
||||
@@ -535,8 +544,8 @@ fn frames_are_identical(a: &VideoFrame, b: &VideoFrame) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
use crate::video::{format::Resolution, PixelFormat};
|
||||
use bytes::Bytes;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stream_handler() {
|
||||
|
||||
@@ -228,16 +228,17 @@ impl MjpegStreamer {
|
||||
let device = self.current_device.read().await;
|
||||
let config = self.config.read().await;
|
||||
|
||||
let (resolution, format, frames_captured) = if let Some(ref cap) = *self.capturer.read().await {
|
||||
let stats = cap.stats().await;
|
||||
(
|
||||
Some((config.resolution.width, config.resolution.height)),
|
||||
Some(config.format.to_string()),
|
||||
stats.frames_captured,
|
||||
)
|
||||
} else {
|
||||
(None, None, 0)
|
||||
};
|
||||
let (resolution, format, frames_captured) =
|
||||
if let Some(ref cap) = *self.capturer.read().await {
|
||||
let stats = cap.stats().await;
|
||||
(
|
||||
Some((config.resolution.width, config.resolution.height)),
|
||||
Some(config.format.to_string()),
|
||||
stats.frames_captured,
|
||||
)
|
||||
} else {
|
||||
(None, None, 0)
|
||||
};
|
||||
|
||||
MjpegStreamerStats {
|
||||
state: state.to_string(),
|
||||
@@ -286,7 +287,10 @@ impl MjpegStreamer {
|
||||
|
||||
/// Initialize with specific device
|
||||
pub async fn init_with_device(self: &Arc<Self>, device: VideoDeviceInfo) -> Result<()> {
|
||||
info!("MjpegStreamer: Initializing with device: {}", device.path.display());
|
||||
info!(
|
||||
"MjpegStreamer: Initializing with device: {}",
|
||||
device.path.display()
|
||||
);
|
||||
|
||||
let config = self.config.read().await.clone();
|
||||
|
||||
@@ -322,7 +326,9 @@ impl MjpegStreamer {
|
||||
let _lock = self.start_lock.lock().await;
|
||||
|
||||
if self.config_changing.load(Ordering::SeqCst) {
|
||||
return Err(AppError::VideoError("Config change in progress".to_string()));
|
||||
return Err(AppError::VideoError(
|
||||
"Config change in progress".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let state = *self.state.read().await;
|
||||
@@ -332,7 +338,8 @@ impl MjpegStreamer {
|
||||
|
||||
// Get capturer
|
||||
let capturer = self.capturer.read().await.clone();
|
||||
let capturer = capturer.ok_or_else(|| AppError::VideoError("Not initialized".to_string()))?;
|
||||
let capturer =
|
||||
capturer.ok_or_else(|| AppError::VideoError("Not initialized".to_string()))?;
|
||||
|
||||
// Start capture
|
||||
capturer.start().await?;
|
||||
@@ -412,7 +419,9 @@ impl MjpegStreamer {
|
||||
let device = devices
|
||||
.into_iter()
|
||||
.find(|d| d.path == *path)
|
||||
.ok_or_else(|| AppError::VideoError(format!("Device not found: {}", path.display())))?;
|
||||
.ok_or_else(|| {
|
||||
AppError::VideoError(format!("Device not found: {}", path.display()))
|
||||
})?;
|
||||
|
||||
self.init_with_device(device).await?;
|
||||
}
|
||||
|
||||
@@ -13,5 +13,7 @@ pub mod mjpeg_streamer;
|
||||
pub mod ws_hid;
|
||||
|
||||
pub use mjpeg::{ClientGuard, MjpegStreamHandler};
|
||||
pub use mjpeg_streamer::{MjpegStreamer, MjpegStreamerConfig, MjpegStreamerState, MjpegStreamerStats};
|
||||
pub use mjpeg_streamer::{
|
||||
MjpegStreamer, MjpegStreamerConfig, MjpegStreamerState, MjpegStreamerStats,
|
||||
};
|
||||
pub use ws_hid::WsHidHandler;
|
||||
|
||||
@@ -142,7 +142,9 @@ impl WsHidHandler {
|
||||
shutdown_tx,
|
||||
});
|
||||
|
||||
self.clients.write().insert(client_id.clone(), client.clone());
|
||||
self.clients
|
||||
.write()
|
||||
.insert(client_id.clone(), client.clone());
|
||||
info!(
|
||||
"WsHidHandler: Client {} connected (total: {})",
|
||||
client_id,
|
||||
@@ -182,7 +184,11 @@ impl WsHidHandler {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
// Send initial status as binary: 0x00 = ok, 0x01 = error
|
||||
let status_byte = if self.is_hid_available() { 0x00u8 } else { 0x01u8 };
|
||||
let status_byte = if self.is_hid_available() {
|
||||
0x00u8
|
||||
} else {
|
||||
0x01u8
|
||||
};
|
||||
let _ = sender.send(Message::Binary(vec![status_byte].into())).await;
|
||||
|
||||
loop {
|
||||
@@ -230,7 +236,10 @@ impl WsHidHandler {
|
||||
let hid = self.hid_controller.read().clone();
|
||||
if let Some(hid) = hid {
|
||||
if let Err(e) = hid.reset().await {
|
||||
warn!("WsHidHandler: Failed to reset HID on client {} disconnect: {}", client_id, e);
|
||||
warn!(
|
||||
"WsHidHandler: Failed to reset HID on client {} disconnect: {}",
|
||||
client_id, e
|
||||
);
|
||||
} else {
|
||||
debug!("WsHidHandler: HID reset on client {} disconnect", client_id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user