refactor(web): 前端代码规范化重构

- 集中化 HID 类型定义到 types/hid.ts,消除重复代码
- 统一 WebSocket 连接管理,提取共享工具到 types/websocket.ts
- 拆分 ConsoleView.vue 关注点,创建 useVideoStream、useHidInput、useConsoleEvents composables
- 添加 useConfigPopover 抽象配置弹窗公共逻辑
- 优化视频容器布局,支持动态比例自适应
This commit is contained in:
mofeng-git
2026-01-02 21:24:47 +08:00
parent 427751da24
commit ad401cdf1c
19 changed files with 1579 additions and 225 deletions

View File

@@ -225,6 +225,16 @@ impl WsHidHandler {
}
}
}
// Reset HID state when client disconnects to release any held keys/buttons
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);
} else {
debug!("WsHidHandler: HID reset on client {} disconnect", client_id);
}
}
}
/// Handle binary HID message

View File

@@ -138,6 +138,34 @@ impl PixelFormat {
}
}
/// Get recommended format for video encoding (WebRTC)
///
/// Hardware encoding prefers: NV12 > YUYV
/// Software encoding prefers: YUYV > NV12
///
/// Returns None if no suitable format is available
pub fn recommended_for_encoding(available: &[PixelFormat], is_hardware: bool) -> Option<PixelFormat> {
if is_hardware {
// Hardware encoding: NV12 > YUYV
if available.contains(&PixelFormat::Nv12) {
return Some(PixelFormat::Nv12);
}
if available.contains(&PixelFormat::Yuyv) {
return Some(PixelFormat::Yuyv);
}
} else {
// Software encoding: YUYV > NV12
if available.contains(&PixelFormat::Yuyv) {
return Some(PixelFormat::Yuyv);
}
if available.contains(&PixelFormat::Nv12) {
return Some(PixelFormat::Nv12);
}
}
// Fallback to any non-compressed format
available.iter().find(|f| !f.is_compressed()).copied()
}
/// Get all supported formats
pub fn all() -> &'static [PixelFormat] {
&[

View File

@@ -208,6 +208,10 @@ impl VideoStreamManager {
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(());
}
@@ -223,6 +227,43 @@ impl VideoStreamManager {
result
}
/// Ensure video capture is running (for WebRTC mode)
async fn ensure_video_capture_running(self: &Arc<Self>) -> Result<()> {
// Initialize streamer if not already initialized
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);
}
}
// Start video capture if not streaming
if self.streamer.state().await != StreamerState::Streaming {
info!("Starting video capture for WebRTC (ensure)");
if let Err(e) = self.streamer.start().await {
error!("Failed to start video capture: {}", e);
return Err(e);
}
// Wait a bit for capture to stabilize
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
// Reconnect frame source to WebRTC
if let Some(frame_tx) = self.streamer.frame_sender().await {
let (format, resolution, fps) = self.streamer.current_video_config().await;
info!(
"Reconnecting frame source to WebRTC: {}x{} {:?} @ {}fps",
resolution.width, resolution.height, format, fps
);
self.webrtc_streamer.update_video_config(resolution, format, fps).await;
self.webrtc_streamer.set_video_source(frame_tx).await;
}
Ok(())
}
/// Internal implementation of mode switching (called with lock held)
async fn do_switch_mode(self: &Arc<Self>, current_mode: StreamMode, new_mode: StreamMode) -> Result<()> {
info!("Switching video mode: {:?} -> {:?}", current_mode, new_mode);
@@ -276,6 +317,22 @@ impl VideoStreamManager {
match new_mode {
StreamMode::Mjpeg => {
info!("Starting MJPEG streaming");
// Auto-switch to MJPEG format if device supports it
if let Some(device) = self.streamer.current_device().await {
let (current_format, resolution, fps) = self.streamer.current_video_config().await;
let available_formats: Vec<PixelFormat> = device.formats.iter().map(|f| f.format).collect();
// If current format is not MJPEG and device supports MJPEG, switch to it
if current_format != PixelFormat::Mjpeg && available_formats.contains(&PixelFormat::Mjpeg) {
info!("Auto-switching to MJPEG format for MJPEG mode");
let device_path = device.path.to_string_lossy().to_string();
if let Err(e) = self.streamer.apply_video_config(&device_path, PixelFormat::Mjpeg, resolution, fps).await {
warn!("Failed to auto-switch to MJPEG format: {}, keeping current format", e);
}
}
}
if let Err(e) = self.streamer.start().await {
error!("Failed to start MJPEG streamer: {}", e);
return Err(e);
@@ -294,6 +351,29 @@ impl VideoStreamManager {
}
}
// Auto-switch to non-compressed format if current format is MJPEG/JPEG
if let Some(device) = self.streamer.current_device().await {
let (current_format, resolution, fps) = self.streamer.current_video_config().await;
if current_format.is_compressed() {
let available_formats: Vec<PixelFormat> = device.formats.iter().map(|f| f.format).collect();
// Determine if using hardware encoding
let is_hardware = self.webrtc_streamer.is_hardware_encoding().await;
if let Some(recommended) = PixelFormat::recommended_for_encoding(&available_formats, is_hardware) {
info!(
"Auto-switching from {:?} to {:?} for WebRTC encoding (hardware={})",
current_format, recommended, is_hardware
);
let device_path = device.path.to_string_lossy().to_string();
if let Err(e) = self.streamer.apply_video_config(&device_path, recommended, resolution, fps).await {
warn!("Failed to auto-switch format for WebRTC: {}, keeping current format", e);
}
}
}
}
// Start video capture if not streaming
if self.streamer.state().await != StreamerState::Streaming {
info!("Starting video capture for WebRTC");

View File

@@ -351,6 +351,15 @@ impl PeerConnection {
/// Close the connection
pub async fn close(&self) -> Result<()> {
// 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);
} else {
tracing::debug!("HID reset on peer {} close", self.session_id);
}
}
if let Some(ref track) = self.video_track {
track.stop();
}

View File

@@ -568,6 +568,32 @@ impl WebRtcStreamer {
);
}
/// Check if current encoder configuration uses hardware encoding
///
/// Returns true if:
/// - A specific hardware backend is configured, OR
/// - Auto mode is used and hardware encoders are available
pub async fn is_hardware_encoding(&self) -> bool {
let config = self.config.read().await;
match config.encoder_backend {
Some(backend) => backend.is_hardware(),
None => {
// Auto mode: check if hardware encoder is available for current codec
use crate::video::encoder::registry::{EncoderRegistry, VideoEncoderType};
let codec_type = match *self.video_codec.read().await {
VideoCodecType::H264 => VideoEncoderType::H264,
VideoCodecType::H265 => VideoEncoderType::H265,
VideoCodecType::VP8 => VideoEncoderType::VP8,
VideoCodecType::VP9 => VideoEncoderType::VP9,
};
EncoderRegistry::global()
.best_encoder(codec_type, false)
.map(|e| e.is_hardware)
.unwrap_or(false)
}
}
}
/// Update ICE configuration (STUN/TURN servers)
///
/// Note: Changes take effect for new sessions only.