mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 18:44:25 +08:00
fix: 完善 RK3588 HDMI RX 信号检测与自动恢复
- 统一使用 QUERY_DV_TIMINGS 判断 HDMI RX 输入状态 - 增加 source-following 设备状态 API 与前端只读展示 - 支持长时间无信号后的 MJPEG/WebRTC 自动恢复 - 修复模式切换时采集设备尚未释放导致的 EBUSY - 取消陈旧 WebRTC 重连并统一采集恢复策略 - 移除视频输入状态区域的冗余标题
This commit is contained in:
@@ -13,6 +13,82 @@ pub use linux::{
|
||||
#[cfg(windows)]
|
||||
pub use windows::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::video::format::{PixelFormat, Resolution};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VideoControlMode {
|
||||
Configurable,
|
||||
SourceFollowing,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VideoInputState {
|
||||
Locked,
|
||||
NoSignal,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct VideoInputStatus {
|
||||
pub state: VideoInputState,
|
||||
pub format: Option<String>,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub fps: Option<f64>,
|
||||
}
|
||||
|
||||
impl VideoInputStatus {
|
||||
pub fn locked(format: PixelFormat, width: u32, height: u32, fps: f64) -> Self {
|
||||
Self::locked_with_optional_fps(Some(format), width, height, Some(fps))
|
||||
}
|
||||
|
||||
pub fn locked_with_optional_fps(
|
||||
format: Option<PixelFormat>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: Option<f64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: VideoInputState::Locked,
|
||||
format: format.map(|format| format.to_string()),
|
||||
width: Some(width),
|
||||
height: Some(height),
|
||||
fps,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn no_signal() -> Self {
|
||||
Self {
|
||||
state: VideoInputState::NoSignal,
|
||||
format: None,
|
||||
width: None,
|
||||
height: None,
|
||||
fps: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn unavailable() -> Self {
|
||||
Self {
|
||||
state: VideoInputState::Unavailable,
|
||||
format: None,
|
||||
width: None,
|
||||
height: None,
|
||||
fps: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ResolvedVideoInputConfig {
|
||||
pub format: PixelFormat,
|
||||
pub resolution: Resolution,
|
||||
pub fps: u32,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod bridge;
|
||||
#[cfg(windows)]
|
||||
@@ -25,23 +101,79 @@ pub(crate) fn is_rk_hdmirx_driver(driver: &str, card: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_rk_hdmirx_device(device: &VideoDeviceInfo) -> bool {
|
||||
is_rk_hdmirx_driver(&device.driver, &device.card)
|
||||
pub(crate) fn is_rkcif_driver(driver: &str) -> bool {
|
||||
driver.to_ascii_lowercase().starts_with("rkcif")
|
||||
}
|
||||
|
||||
pub(crate) fn is_rkcif_driver(driver: &str) -> bool {
|
||||
driver.eq_ignore_ascii_case("rkcif")
|
||||
pub fn control_mode(driver: &str, card: &str) -> VideoControlMode {
|
||||
if is_rkcif_driver(driver) || is_rk_hdmirx_driver(driver, card) {
|
||||
VideoControlMode::SourceFollowing
|
||||
} else {
|
||||
VideoControlMode::Configurable
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified check for CSI/HDMI bridge devices (rk_hdmirx, rkcif, etc.)
|
||||
/// that require special enumeration and format-selection logic.
|
||||
pub(crate) fn is_csi_hdmi_bridge(device: &VideoDeviceInfo) -> bool {
|
||||
is_rk_hdmirx_device(device) || is_rkcif_driver(&device.driver)
|
||||
device.control_mode == VideoControlMode::SourceFollowing
|
||||
}
|
||||
|
||||
pub fn resolve_video_input_config(
|
||||
device: &VideoDeviceInfo,
|
||||
requested_format: PixelFormat,
|
||||
requested_resolution: Resolution,
|
||||
requested_fps: u32,
|
||||
) -> ResolvedVideoInputConfig {
|
||||
if device.control_mode == VideoControlMode::SourceFollowing {
|
||||
if let VideoInputStatus {
|
||||
state: VideoInputState::Locked,
|
||||
format: Some(format),
|
||||
width: Some(width),
|
||||
height: Some(height),
|
||||
fps: Some(fps),
|
||||
} = &device.input_status
|
||||
{
|
||||
if let Ok(format) = format.parse::<PixelFormat>() {
|
||||
return ResolvedVideoInputConfig {
|
||||
format,
|
||||
resolution: Resolution::new(*width, *height),
|
||||
fps: fps.round().clamp(1.0, 120.0) as u32,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResolvedVideoInputConfig {
|
||||
format: requested_format,
|
||||
resolution: requested_resolution,
|
||||
fps: requested_fps,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_rk_hdmirx_driver;
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
fn device(control_mode: VideoControlMode, input_status: VideoInputStatus) -> VideoDeviceInfo {
|
||||
VideoDeviceInfo {
|
||||
path: "/dev/video0".into(),
|
||||
name: "test".into(),
|
||||
driver: "test".into(),
|
||||
bus_info: "test".into(),
|
||||
card: "test".into(),
|
||||
formats: Vec::new(),
|
||||
capabilities: Default::default(),
|
||||
is_capture_card: true,
|
||||
priority: 0,
|
||||
has_signal: input_status.state == VideoInputState::Locked,
|
||||
control_mode,
|
||||
input_status,
|
||||
subdev_path: None,
|
||||
bridge_kind: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recognizes_vendor_and_upstream_native_hdmirx_names() {
|
||||
@@ -50,6 +182,82 @@ mod tests {
|
||||
assert!(is_rk_hdmirx_driver("other", "SNPS_HDMIRX"));
|
||||
assert!(!is_rk_hdmirx_driver("rkcif", "stream_cif_mipi_id0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_source_following_drivers_in_one_place() {
|
||||
assert_eq!(
|
||||
control_mode("rkcif", "stream_cif_mipi_id0"),
|
||||
VideoControlMode::SourceFollowing
|
||||
);
|
||||
assert_eq!(
|
||||
control_mode("rkcif-mipi", "capture"),
|
||||
VideoControlMode::SourceFollowing
|
||||
);
|
||||
assert_eq!(
|
||||
control_mode("rk_hdmirx", "capture"),
|
||||
VideoControlMode::SourceFollowing
|
||||
);
|
||||
assert_eq!(
|
||||
control_mode("uvcvideo", "USB Capture"),
|
||||
VideoControlMode::Configurable
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn source_following_uses_locked_hardware_mode_and_exact_fps_rounding() {
|
||||
let device = device(
|
||||
VideoControlMode::SourceFollowing,
|
||||
VideoInputStatus::locked(PixelFormat::Nv12, 1920, 1080, 59.94),
|
||||
);
|
||||
let resolved = resolve_video_input_config(
|
||||
&device,
|
||||
PixelFormat::Mjpeg,
|
||||
Resolution::new(3840, 2160),
|
||||
15,
|
||||
);
|
||||
assert_eq!(resolved.format, PixelFormat::Nv12);
|
||||
assert_eq!(resolved.resolution, Resolution::new(1920, 1080));
|
||||
assert_eq!(resolved.fps, 60);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn no_signal_keeps_fallback_and_configurable_keeps_request() {
|
||||
for (mode, status) in [
|
||||
(
|
||||
VideoControlMode::SourceFollowing,
|
||||
VideoInputStatus::no_signal(),
|
||||
),
|
||||
(
|
||||
VideoControlMode::Configurable,
|
||||
VideoInputStatus::unavailable(),
|
||||
),
|
||||
] {
|
||||
let resolved = resolve_video_input_config(
|
||||
&device(mode, status),
|
||||
PixelFormat::Yuyv,
|
||||
Resolution::new(1280, 720),
|
||||
30,
|
||||
);
|
||||
assert_eq!(resolved.format, PixelFormat::Yuyv);
|
||||
assert_eq!(resolved.resolution, Resolution::new(1280, 720));
|
||||
assert_eq!(resolved.fps, 30);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_signal_and_unavailable_never_expose_stale_mode_fields() {
|
||||
for status in [
|
||||
VideoInputStatus::no_signal(),
|
||||
VideoInputStatus::unavailable(),
|
||||
] {
|
||||
assert!(status.format.is_none());
|
||||
assert!(status.width.is_none());
|
||||
assert!(status.height.is_none());
|
||||
assert!(status.fps.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
Reference in New Issue
Block a user