fix: 完善 RK3588 HDMI RX 信号检测与自动恢复

- 统一使用 QUERY_DV_TIMINGS 判断 HDMI RX 输入状态
  - 增加 source-following 设备状态 API 与前端只读展示
  - 支持长时间无信号后的 MJPEG/WebRTC 自动恢复
  - 修复模式切换时采集设备尚未释放导致的 EBUSY
  - 取消陈旧 WebRTC 重连并统一采集恢复策略
  - 移除视频输入状态区域的冗余标题
This commit is contained in:
mofeng-git
2026-07-30 19:41:44 +08:00
parent 971c263bf8
commit 9fb23476ac
36 changed files with 1883 additions and 1492 deletions

View File

@@ -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)]