From 1647e702436d7d0891bd802022824aa5aad94a6b Mon Sep 17 00:00:00 2001 From: Carbon <135588205+carbonfix@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:32:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20RK3588=20=E5=8E=9F=E7=94=9F=20HDMIRX?= =?UTF-8?q?=20=E8=A2=AB=E9=94=99=E8=AF=AF=E5=9C=B0=E5=85=B3=E8=81=94?= =?UTF-8?q?=E5=88=B0=E4=BA=86=20RK628=20subdev=20(#293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: RK3588 原生 HDMIRX 被错误地关联到了 RK628 subdev 当 RK628 和 RK3588 原生 HDMI IN 同时存在时,页面配置为 rk_hdmirx, 会无法使用,提示无信号。 是因为原来的 subdev 发现逻辑会全局扫描 v4l-subdev,并返回首个匹配节点。 原生 HDMI IN 可能错误使用 RK628 subdev 的信号状态,最终被判断为无信号。 解决方案: 根据 video 节点的设备号找到对应 media device,读取 Media Controller 中的 entity 和已启用链路,只返回与当前 video 节点实际连通的 HDMI bridge subdev。 * fix: 精确匹配视频设备的 Media 拓扑 --------- Co-authored-by: mofeng-git --- src/video/device/bridge.rs | 538 ++++++++++++++++++++++++++++++++++--- 1 file changed, 508 insertions(+), 30 deletions(-) diff --git a/src/video/device/bridge.rs b/src/video/device/bridge.rs index c855b245..a1a4fd4e 100644 --- a/src/video/device/bridge.rs +++ b/src/video/device/bridge.rs @@ -1,9 +1,10 @@ //! CSI/HDMI bridge helpers: subdev discovery, DV probe, RK628 "fake VGA" filter (must run before `S_FMT` / `STREAMON` on capture — see RK628 driver). +use std::collections::{HashMap, HashSet, VecDeque}; use std::fs::File; use std::io; use std::os::fd::{AsFd, AsRawFd, FromRawFd}; -use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::thread; @@ -97,43 +98,391 @@ impl std::fmt::Debug for DvTimingsMode { } } -/// Heuristic: scan `/sys/class/video4linux/v4l-subdev*` names for rk628 / hdmirx / tc358743. -pub fn discover_subdev_for_video(video_path: &Path) -> Option<(PathBuf, CsiBridgeKind)> { - let sysfs_base = Path::new("/sys/class/video4linux"); - let entries = std::fs::read_dir(sysfs_base).ok()?; +const SYSFS_VIDEO4LINUX: &str = "/sys/class/video4linux"; +const DEV_ROOT: &str = "/dev"; +const MEDIA_ENT_ID_FLAG_NEXT: u32 = 1 << 31; +const MEDIA_LNK_FL_ENABLED: u32 = 1 << 0; +const MEDIA_LNK_FL_LINK_TYPE: u32 = 0xf << 28; +const MEDIA_LNK_FL_DATA_LINK: u32 = 0 << 28; - for entry in entries.flatten() { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - if !name_str.starts_with("v4l-subdev") { - continue; - } - let Some(kind) = read_sysfs_name(&entry.path()) - .as_deref() - .and_then(CsiBridgeKind::from_subdev_name) - else { - continue; - }; - let dev_path = PathBuf::from("/dev").join(&*name_str); - if dev_path.exists() { +#[repr(C)] +#[derive(Clone, Copy)] +struct MediaEntityDesc { + id: u32, + name: [u8; 32], + type_: u32, + revision: u32, + flags: u32, + group_id: u32, + pads: u16, + links: u16, + reserved: [u32; 4], + info: MediaEntityInfo, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct MediaDeviceNode { + major: u32, + minor: u32, +} + +#[repr(C)] +#[derive(Clone, Copy)] +union MediaEntityInfo { + dev: MediaDeviceNode, + _raw: [u8; 184], +} + +impl Default for MediaEntityDesc { + fn default() -> Self { + // This mirrors the zero-initialization required by the media UAPI. + unsafe { std::mem::zeroed() } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct MediaPadDesc { + entity: u32, + index: u16, + flags: u32, + reserved: [u32; 2], +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct MediaLinkDesc { + source: MediaPadDesc, + sink: MediaPadDesc, + flags: u32, + reserved: [u32; 2], +} + +#[repr(C)] +struct MediaLinksEnum { + entity: u32, + pads: *mut MediaPadDesc, + links: *mut MediaLinkDesc, + reserved: [u32; 4], +} + +nix::ioctl_readwrite!(media_ioc_enum_entities, b'|', 0x01, MediaEntityDesc); +nix::ioctl_readwrite!(media_ioc_enum_links, b'|', 0x02, MediaLinksEnum); + +#[derive(Debug, Clone)] +struct MediaGraphEntity { + id: u32, + name: String, + major: u32, + minor: u32, + pads: u16, + links: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct MediaGraphLink { + source: u32, + sink: u32, +} + +#[derive(Debug)] +struct MediaGraph { + path: PathBuf, + entities: Vec, + links: Vec, +} + +/// Find the CSI/HDMI bridge that is connected to `video_path` in the same +/// media-controller graph. Name-only global scans are deliberately avoided: +/// boards can expose RK628, native HDMI RX and USB capture at the same time. +pub fn discover_subdev_for_video(video_path: &Path) -> Option<(PathBuf, CsiBridgeKind)> { + match discover_subdev_for_video_inner(video_path) { + Ok(Some((path, kind, media_path))) => { info!( - "Discovered CSI bridge subdev for {:?}: {:?} ({:?})", - video_path, dev_path, kind + "Discovered CSI bridge subdev for {:?}: {:?} ({:?}) via {:?}", + video_path, path, kind, media_path ); - return Some((dev_path, kind)); + Some((path, kind)) + } + Ok(None) => { + debug!( + "No connected CSI bridge subdev found in media topology for {:?}", + video_path + ); + None + } + Err(error) => { + warn!( + "Failed to inspect media topology for {:?}: {}", + video_path, error + ); + None + } + } +} + +fn discover_subdev_for_video_inner( + video_path: &Path, +) -> io::Result> { + let video_device = device_numbers(video_path)?; + let mut graphs = Vec::new(); + let mut first_error = None; + + // A physical device may expose more than one media controller. Inspect + // every controller and use the graph that actually contains video_path; + // choosing the first mediaN node merely moves the old global-scan bug. + for media_path in media_device_paths()? { + let graph = File::open(&media_path).and_then(|media| { + read_media_graph(&media).map(|(entities, links)| MediaGraph { + path: media_path.clone(), + entities, + links, + }) + }); + match graph { + Ok(graph) => graphs.push(graph), + Err(error) => { + debug!( + "Failed to inspect media controller {:?}: {}", + media_path, error + ); + first_error.get_or_insert(error); + } + } + } + + let graph_contains_video = graphs.iter().any(|graph| { + graph + .entities + .iter() + .any(|entity| (entity.major, entity.minor) == video_device) + }); + let Some((media_path, entity, kind)) = connected_bridge_in_media_graphs(&graphs, video_device) + else { + if !graph_contains_video { + if let Some(error) = first_error { + return Err(error); + } + } + return Ok(None); + }; + let Some(subdev_path) = video4linux_devnode((entity.major, entity.minor))? else { + return Ok(None); + }; + + if !subdev_path + .file_name() + .is_some_and(|name| name.to_string_lossy().starts_with("v4l-subdev")) + { + return Ok(None); + } + + Ok(Some((subdev_path, kind, media_path.to_path_buf()))) +} + +fn device_numbers(path: &Path) -> io::Result<(u32, u32)> { + let rdev = std::fs::metadata(path)?.rdev(); + let major = nix::sys::stat::major(rdev); + let minor = nix::sys::stat::minor(rdev); + Ok((major as u32, minor as u32)) +} + +fn parse_device_numbers(value: &str) -> Option<(u32, u32)> { + let (major, minor) = value.trim().split_once(':')?; + Some((major.parse().ok()?, minor.parse().ok()?)) +} + +fn video4linux_class_entry(device: (u32, u32)) -> io::Result> { + for entry in std::fs::read_dir(SYSFS_VIDEO4LINUX)? { + let entry = entry?; + let dev = match std::fs::read_to_string(entry.path().join("dev")) { + Ok(value) => value, + Err(_) => continue, + }; + if parse_device_numbers(&dev) == Some(device) { + return Ok(Some(entry.path())); + } + } + Ok(None) +} + +fn video4linux_devnode(device: (u32, u32)) -> io::Result> { + let Some(class_entry) = video4linux_class_entry(device)? else { + return Ok(None); + }; + let Some(name) = class_entry.file_name() else { + return Ok(None); + }; + let path = Path::new(DEV_ROOT).join(name); + Ok(path.exists().then_some(path)) +} + +fn media_device_paths() -> io::Result> { + let mut media_nodes = std::fs::read_dir(DEV_ROOT)? + .filter_map(Result::ok) + .filter_map(|entry| { + let name = entry.file_name(); + let name = name.to_str()?; + let index = media_device_index(name)?; + Some((index, entry.path())) + }) + .collect::>(); + media_nodes.sort_by(|(left_index, left_path), (right_index, right_path)| { + left_index + .cmp(right_index) + .then_with(|| left_path.cmp(right_path)) + }); + Ok(media_nodes.into_iter().map(|(_, path)| path).collect()) +} + +fn media_device_index(name: &str) -> Option { + let suffix = name.strip_prefix("media")?; + if suffix.is_empty() || !suffix.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + suffix.parse().ok() +} + +fn read_media_graph(media: &File) -> io::Result<(Vec, Vec)> { + let mut entities = Vec::new(); + let mut previous_id = 0u32; + + loop { + let mut desc = MediaEntityDesc { + id: previous_id | MEDIA_ENT_ID_FLAG_NEXT, + ..Default::default() + }; + // SAFETY: `desc` has the exact media_entity_desc UAPI layout and is + // writable for the duration of the ioctl. + match unsafe { media_ioc_enum_entities(media.as_raw_fd(), &mut desc) } { + Ok(_) => {} + Err(Errno::EINVAL) => break, + Err(error) => return Err(io::Error::from_raw_os_error(error as i32)), + } + if desc.id == previous_id || entities.len() >= 4096 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "media entity enumeration did not advance", + )); + } + previous_id = desc.id; + + let nul = desc + .name + .iter() + .position(|byte| *byte == 0) + .unwrap_or(desc.name.len()); + let name = String::from_utf8_lossy(&desc.name[..nul]).into_owned(); + // SAFETY: the kernel filled the `dev` member of the media UAPI union + // for V4L2 devnode entities. Non-devnode entities report zeroes. + let device = unsafe { desc.info.dev }; + entities.push(MediaGraphEntity { + id: desc.id, + name, + major: device.major, + minor: device.minor, + pads: desc.pads, + links: desc.links, + }); + } + + let mut graph_links = HashSet::new(); + for entity in &entities { + let mut pads = vec![MediaPadDesc::default(); entity.pads as usize]; + let mut links = vec![MediaLinkDesc::default(); entity.links as usize]; + let mut request = MediaLinksEnum { + entity: entity.id, + pads: if pads.is_empty() { + std::ptr::null_mut() + } else { + pads.as_mut_ptr() + }, + links: if links.is_empty() { + std::ptr::null_mut() + } else { + links.as_mut_ptr() + }, + reserved: [0; 4], + }; + // SAFETY: the vectors provide the number of entries reported by the + // entity descriptor and remain alive while the kernel fills them. + unsafe { media_ioc_enum_links(media.as_raw_fd(), &mut request) } + .map_err(|error| io::Error::from_raw_os_error(error as i32))?; + + for link in links { + if link.flags & MEDIA_LNK_FL_ENABLED == 0 + || link.flags & MEDIA_LNK_FL_LINK_TYPE != MEDIA_LNK_FL_DATA_LINK + { + continue; + } + graph_links.insert((link.source.entity, link.sink.entity)); + } + } + + let mut links = graph_links + .into_iter() + .map(|(source, sink)| MediaGraphLink { source, sink }) + .collect::>(); + links.sort_by_key(|link| (link.source, link.sink)); + Ok((entities, links)) +} + +fn connected_bridge_entity<'a>( + entities: &'a [MediaGraphEntity], + links: &[MediaGraphLink], + video_device: (u32, u32), +) -> Option<(&'a MediaGraphEntity, CsiBridgeKind)> { + let start = entities + .iter() + .find(|entity| (entity.major, entity.minor) == video_device)?; + let by_id = entities + .iter() + .map(|entity| (entity.id, entity)) + .collect::>(); + let mut upstream = HashMap::>::new(); + for link in links { + // Media data flows source -> sink. Starting at a capture video node, + // only sink -> source traversal can lead to its real input bridge. + upstream.entry(link.sink).or_default().push(link.source); + } + for neighbors in upstream.values_mut() { + neighbors.sort_unstable(); + neighbors.dedup(); + } + + let mut visited = HashSet::from([start.id]); + let mut queue = VecDeque::from([start.id]); + while let Some(id) = queue.pop_front() { + if id != start.id { + if let Some(entity) = by_id.get(&id) { + if entity.major != 0 || entity.minor != 0 { + if let Some(kind) = CsiBridgeKind::from_subdev_name(&entity.name) { + return Some((entity, kind)); + } + } + } + } + + if let Some(neighbors) = upstream.get(&id) { + for neighbor in neighbors { + if visited.insert(*neighbor) { + queue.push_back(*neighbor); + } + } } } - debug!( - "No CSI bridge subdev found in /sys/class/video4linux for {:?}", - video_path - ); None } -fn read_sysfs_name(subdev_sysfs: &Path) -> Option { - std::fs::read_to_string(subdev_sysfs.join("name")) - .ok() - .map(|s| s.trim().to_string()) +fn connected_bridge_in_media_graphs( + graphs: &[MediaGraph], + video_device: (u32, u32), +) -> Option<(&Path, &MediaGraphEntity, CsiBridgeKind)> { + graphs.iter().find_map(|graph| { + connected_bridge_entity(&graph.entities, &graph.links, video_device) + .map(|(entity, kind)| (graph.path.as_path(), entity, kind)) + }) } pub fn open_subdev(path: &Path) -> io::Result { @@ -339,6 +688,53 @@ pub fn wait_source_change(subdev_fd: &File, timeout: Duration) -> io::Result, + links: Vec, + ) -> MediaGraph { + MediaGraph { + path: PathBuf::from(path), + entities, + links, + } + } + + fn graph_entity(id: u32, name: &str, device: (u32, u32)) -> MediaGraphEntity { + MediaGraphEntity { + id, + name: name.to_string(), + major: device.0, + minor: device.1, + pads: 0, + links: 0, + } + } + + fn graph_link(source: u32, sink: u32) -> MediaGraphLink { + MediaGraphLink { source, sink } + } + + #[test] + fn media_uapi_layout_matches_linux_legacy_api() { + assert_eq!(std::mem::size_of::(), 256); + assert_eq!(std::mem::size_of::(), 20); + assert_eq!(std::mem::size_of::(), 52); + #[cfg(target_pointer_width = "64")] + assert_eq!(std::mem::size_of::(), 40); + #[cfg(target_pointer_width = "32")] + assert_eq!(std::mem::size_of::(), 28); + } + + #[test] + fn media_device_names_require_a_numeric_suffix() { + assert_eq!(media_device_index("media0"), Some(0)); + assert_eq!(media_device_index("media12"), Some(12)); + assert_eq!(media_device_index("media"), None); + assert_eq!(media_device_index("media-controller"), None); + assert_eq!(media_device_index("video0"), None); + } + #[test] fn subdevice_handles_are_non_blocking() { let file = tempfile::NamedTempFile::new().unwrap(); @@ -348,6 +744,88 @@ mod tests { assert_ne!(flags & libc::O_NONBLOCK, 0); } + #[test] + fn media_graph_finds_only_the_connected_rk628() { + // Captured shape of the RK3588 RKCIF graph: + // video0 <- mipi-csi2 <- dphy <- RK628. A second RK628 entity is + // present in the same synthetic topology but is not connected. + let entities = vec![ + graph_entity(1, "stream_cif_mipi_id0", (81, 0)), + graph_entity(45, "rockchip-mipi-csi2", (0, 0)), + graph_entity(58, "rockchip-csi2-dphy0", (0, 0)), + graph_entity(63, "m00_b_rk628-csi 3-0050", (81, 16)), + graph_entity(90, "other-rk628-csi 7-0050", (81, 19)), + ]; + let links = vec![graph_link(63, 58), graph_link(58, 45), graph_link(45, 1)]; + + let (entity, kind) = connected_bridge_entity(&entities, &links, (81, 0)).unwrap(); + assert_eq!(entity.id, 63); + assert_eq!(kind, CsiBridgeKind::Rk628); + } + + #[test] + fn media_graph_search_only_walks_towards_link_sources() { + let entities = vec![ + graph_entity(1, "stream_cif_mipi_id0", (81, 0)), + graph_entity(20, "rockchip-mipi-csi2", (0, 0)), + graph_entity(63, "unrelated-rk628-csi 7-0050", (81, 19)), + ]; + // Entity 20 is upstream of the capture node. Entity 63 is downstream + // of 20 and must not be reached while tracing the capture input. + let links = vec![graph_link(20, 1), graph_link(20, 63)]; + + assert!(connected_bridge_entity(&entities, &links, (81, 0)).is_none()); + } + + #[test] + fn media_graphs_select_the_controller_containing_the_video_node() { + let graphs = vec![ + media_graph( + "/dev/media0", + vec![ + graph_entity(1, "other-video", (81, 4)), + graph_entity(63, "wrong-rk628-csi", (81, 16)), + ], + vec![graph_link(63, 1)], + ), + media_graph( + "/dev/media1", + vec![ + graph_entity(10, "stream_cif_mipi_id0", (81, 0)), + graph_entity(75, "tc358743 2-000f", (81, 20)), + ], + vec![graph_link(75, 10)], + ), + ]; + + let (path, entity, kind) = connected_bridge_in_media_graphs(&graphs, (81, 0)).unwrap(); + assert_eq!(path, Path::new("/dev/media1")); + assert_eq!(entity.id, 75); + assert_eq!(kind, CsiBridgeKind::Tc358743); + } + + #[test] + fn media_graph_does_not_attach_an_unrelated_rk628_to_native_hdmirx() { + let entities = vec![ + graph_entity(1, "rk_hdmirx", (81, 11)), + graph_entity(63, "m00_b_rk628-csi 3-0050", (81, 16)), + ]; + + assert!(connected_bridge_entity(&entities, &[], (81, 11)).is_none()); + } + + #[test] + fn media_graph_leaves_usb_capture_without_a_csi_bridge() { + let entities = vec![ + graph_entity(1, "USB Video: USB Video", (81, 12)), + graph_entity(8, "Processing 2", (0, 0)), + graph_entity(11, "Input 1", (0, 0)), + ]; + let links = vec![graph_link(11, 8), graph_link(8, 1)]; + + assert!(connected_bridge_entity(&entities, &links, (81, 12)).is_none()); + } + #[test] fn rk628_fingerprint_matches_vga() { let mut bt: v4l2_bt_timings = unsafe { std::mem::zeroed() }; From dcfa3eadaf1f249c1e222aea9fa1d59d6ccfe797 Mon Sep 17 00:00:00 2001 From: Carbon <135588205+carbonfix@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:37:57 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=B8=BA=20RKCIF/RK628=20=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=8F=97=E6=94=AF=E6=8C=81=E7=9A=84=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=20(#292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RK628 通过 RKCIF 接入时,如果 One-KVM 启动时 HDMI 信号尚未锁定, SourceFollowing 路径可能回退到默认 MJPEG。由于 RKCIF 不支持 MJPEG, 后续 S_FMT 无法建立有效的采集链路。 解决方案: 在解析 SourceFollowing 配置后,根据设备的 VIDIOC_ENUM_FMT 枚举结果 校验最终格式。如果该格式不受支持且格式列表非空,则选择按优先级排序 的首个可用格式,对于 RKCIF/RK628 通常为 NV12。 该方法只替换 FourCC,保留 HDMI DV timings 提供的分辨率和帧率。 同时覆盖无信号启动和当前格式已失效两种场景。 --- src/video/device/mod.rs | 82 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/src/video/device/mod.rs b/src/video/device/mod.rs index 10b10fa3..7766ce8b 100644 --- a/src/video/device/mod.rs +++ b/src/video/device/mod.rs @@ -125,6 +125,12 @@ pub fn resolve_video_input_config( requested_resolution: Resolution, requested_fps: u32, ) -> ResolvedVideoInputConfig { + let mut resolved = ResolvedVideoInputConfig { + format: requested_format, + resolution: requested_resolution, + fps: requested_fps, + }; + if device.control_mode == VideoControlMode::SourceFollowing { if let VideoInputStatus { state: VideoInputState::Locked, @@ -135,26 +141,39 @@ pub fn resolve_video_input_config( } = &device.input_status { if let Ok(format) = format.parse::() { - return ResolvedVideoInputConfig { + resolved = ResolvedVideoInputConfig { format, resolution: Resolution::new(*width, *height), fps: fps.round().clamp(1.0, 120.0) as u32, }; } } + + // Source-following devices do not allow One-KVM to choose the HDMI + // resolution or frame rate, but their pixel format still has to be one + // of the formats enumerated by the capture node. In particular, rkcif + // commonly exposes NV12 but One-KVM's default is MJPEG. Passing that + // unsupported default to S_FMT leaves the pipeline in an invalid state. + if !device.formats.is_empty() + && !device + .formats + .iter() + .any(|format| format.format == resolved.format) + { + resolved.format = device.formats[0].format; + } } - ResolvedVideoInputConfig { - format: requested_format, - resolution: requested_resolution, - fps: requested_fps, - } + resolved } #[cfg(test)] mod tests { use super::*; + #[cfg(unix)] + use super::linux::FormatInfo; + #[cfg(unix)] fn device(control_mode: VideoControlMode, input_status: VideoInputStatus) -> VideoDeviceInfo { VideoDeviceInfo { @@ -175,6 +194,15 @@ mod tests { } } + #[cfg(unix)] + fn format(format: PixelFormat) -> FormatInfo { + FormatInfo { + format, + resolutions: Vec::new(), + description: format.to_string(), + } + } + #[test] fn recognizes_vendor_and_upstream_native_hdmirx_names() { assert!(is_rk_hdmirx_driver("rk_hdmirx", "rk_hdmirx")); @@ -246,6 +274,48 @@ mod tests { } } + #[cfg(unix)] + #[test] + fn source_following_replaces_unenumerated_default_format_without_signal() { + let mut device = device( + VideoControlMode::SourceFollowing, + VideoInputStatus::no_signal(), + ); + device.formats = vec![format(PixelFormat::Nv12), format(PixelFormat::Yuyv)]; + + let resolved = resolve_video_input_config( + &device, + PixelFormat::Mjpeg, + Resolution::new(1920, 1080), + 30, + ); + + assert_eq!(resolved.format, PixelFormat::Nv12); + assert_eq!(resolved.resolution, Resolution::new(1920, 1080)); + assert_eq!(resolved.fps, 30); + } + + #[cfg(unix)] + #[test] + fn source_following_replaces_stale_active_format_but_keeps_input_mode() { + let mut device = device( + VideoControlMode::SourceFollowing, + VideoInputStatus::locked(PixelFormat::Mjpeg, 1280, 720, 59.94), + ); + device.formats = vec![format(PixelFormat::Nv12), format(PixelFormat::Yuyv)]; + + let resolved = resolve_video_input_config( + &device, + PixelFormat::Mjpeg, + Resolution::new(1920, 1080), + 30, + ); + + assert_eq!(resolved.format, PixelFormat::Nv12); + assert_eq!(resolved.resolution, Resolution::new(1280, 720)); + assert_eq!(resolved.fps, 60); + } + #[test] fn no_signal_and_unavailable_never_expose_stale_mode_fields() { for status in [