//! V4L2 device enumeration and capability query use serde::{Deserialize, Serialize}; use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::time::Duration; use tracing::{debug, info, warn}; use v4l2r::bindings::{ v4l2_bt_timings, v4l2_dv_timings, v4l2_frmivalenum, v4l2_frmsizeenum, v4l2_streamparm, V4L2_DV_BT_656_1120, }; use v4l2r::ioctl::{ self, Capabilities, Capability as V4l2rCapability, FormatIterator, FrmIvalTypes, FrmSizeTypes, }; use v4l2r::nix::errno::Errno; use v4l2r::{Format as V4l2rFormat, QueueType}; use super::format::{PixelFormat, Resolution}; use super::is_rk_hdmirx_driver; use crate::error::{AppError, Result}; const DEVICE_PROBE_TIMEOUT_MS: u64 = 400; /// Information about a video device #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VideoDeviceInfo { /// Device path (e.g., /dev/video0) pub path: PathBuf, /// Device name from driver pub name: String, /// Driver name pub driver: String, /// Bus info pub bus_info: String, /// Card name pub card: String, /// Supported pixel formats pub formats: Vec, /// Device capabilities pub capabilities: DeviceCapabilities, /// Whether this is likely an HDMI capture card pub is_capture_card: bool, /// Priority score for device selection (higher is better) pub priority: u32, } /// Information about a supported format #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FormatInfo { /// Pixel format pub format: PixelFormat, /// Supported resolutions pub resolutions: Vec, /// Description from driver pub description: String, } /// Information about a supported resolution and frame rates #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResolutionInfo { pub width: u32, pub height: u32, pub fps: Vec, } impl ResolutionInfo { pub fn new(width: u32, height: u32, fps: Vec) -> Self { Self { width, height, fps } } pub fn resolution(&self) -> Resolution { Resolution::new(self.width, self.height) } } /// Device capabilities #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct DeviceCapabilities { pub video_capture: bool, pub video_capture_mplane: bool, pub video_output: bool, pub streaming: bool, pub read_write: bool, } /// Wrapper around a V4L2 video device pub struct VideoDevice { pub path: PathBuf, fd: File, } impl VideoDevice { /// Open a video device by path pub fn open(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); debug!("Opening video device: {:?}", path); let fd = File::options() .read(true) .write(true) .open(&path) .map_err(|e| { AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)) })?; Ok(Self { path, fd }) } /// Open a video device read-only (for probing/enumeration) pub fn open_readonly(path: impl AsRef) -> Result { let path = path.as_ref().to_path_buf(); debug!("Opening video device (read-only): {:?}", path); let fd = File::options().read(true).open(&path).map_err(|e| { AppError::VideoError(format!("Failed to open device {:?}: {}", path, e)) })?; Ok(Self { path, fd }) } /// Get device capabilities pub fn capabilities(&self) -> Result { let caps: V4l2rCapability = ioctl::querycap(&self.fd) .map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?; let flags = caps.device_caps(); Ok(DeviceCapabilities { video_capture: flags.contains(Capabilities::VIDEO_CAPTURE), video_capture_mplane: flags.contains(Capabilities::VIDEO_CAPTURE_MPLANE), video_output: flags.contains(Capabilities::VIDEO_OUTPUT), streaming: flags.contains(Capabilities::STREAMING), read_write: flags.contains(Capabilities::READWRITE), }) } /// Get detailed device information pub fn info(&self) -> Result { let caps: V4l2rCapability = ioctl::querycap(&self.fd) .map_err(|e| AppError::VideoError(format!("Failed to query capabilities: {}", e)))?; let flags = caps.device_caps(); let capabilities = DeviceCapabilities { video_capture: flags.contains(Capabilities::VIDEO_CAPTURE), video_capture_mplane: flags.contains(Capabilities::VIDEO_CAPTURE_MPLANE), video_output: flags.contains(Capabilities::VIDEO_OUTPUT), streaming: flags.contains(Capabilities::STREAMING), read_write: flags.contains(Capabilities::READWRITE), }; let formats = if is_rk_hdmirx_driver(&caps.driver, &caps.card) { self.enumerate_current_format_only()? } else { self.enumerate_formats()? }; // Determine if this is likely an HDMI capture card let is_capture_card = Self::detect_capture_card(&caps.card, &caps.driver, &formats); // Calculate priority score let priority = Self::calculate_priority(&caps.card, &caps.driver, &formats, is_capture_card); Ok(VideoDeviceInfo { path: self.path.clone(), name: caps.card.clone(), driver: caps.driver.clone(), bus_info: caps.bus_info.clone(), card: caps.card, formats, capabilities, is_capture_card, priority, }) } /// Enumerate supported formats pub fn enumerate_formats(&self) -> Result> { let mut formats = Vec::new(); let queue = self.capture_queue_type()?; let format_descs = FormatIterator::new(&self.fd, queue); for desc in format_descs { // Try to convert FourCC to our PixelFormat if let Some(format) = PixelFormat::from_v4l2r(desc.pixelformat) { let resolutions = self.enumerate_resolutions(desc.pixelformat)?; let is_current_format = self.current_active_format() == Some(format); if resolutions.is_empty() && !is_current_format { debug!( "Skipping format {:?} ({}): not usable for current active mode", desc.pixelformat, desc.description ); continue; } formats.push(FormatInfo { format, resolutions, description: desc.description.clone(), }); } else { debug!( "Skipping unsupported format: {:?} ({})", desc.pixelformat, desc.description ); } } // Sort by format priority (MJPEG first) formats.sort_by(|a, b| b.format.priority().cmp(&a.format.priority())); Ok(formats) } fn enumerate_current_format_only(&self) -> Result> { let current = self.get_format()?; let Some(format) = PixelFormat::from_v4l2r(current.pixelformat) else { debug!( "Current active format {:?} is not supported by One-KVM, falling back to full enumeration", current.pixelformat ); return self.enumerate_formats(); }; let description = self .format_description(current.pixelformat) .unwrap_or_else(|| format.to_string()); let mut resolutions = self.enumerate_resolutions(current.pixelformat)?; if resolutions.is_empty() { if let Some(current_mode) = self.current_mode_resolution_info() { resolutions.push(current_mode); } } Ok(vec![FormatInfo { format, resolutions, description, }]) } /// Enumerate resolutions for a specific format fn enumerate_resolutions(&self, fourcc: v4l2r::PixelFormat) -> Result> { let mut resolutions = Vec::new(); let mut should_fallback_to_current_mode = false; let mut index = 0u32; loop { match ioctl::enum_frame_sizes::(&self.fd, index, fourcc) { Ok(size) => { if let Some(size) = size.size() { match size { FrmSizeTypes::Discrete(d) => { let fps = self .enumerate_fps(fourcc, d.width, d.height) .unwrap_or_default(); resolutions.push(ResolutionInfo::new(d.width, d.height, fps)); } FrmSizeTypes::StepWise(s) => { for res in [ Resolution::VGA, Resolution::HD720, Resolution::HD1080, Resolution::UHD4K, ] { if res.width >= s.min_width && res.width <= s.max_width && res.height >= s.min_height && res.height <= s.max_height { let fps = self .enumerate_fps(fourcc, res.width, res.height) .unwrap_or_default(); resolutions .push(ResolutionInfo::new(res.width, res.height, fps)); } } } } } index += 1; } Err(e) => { let is_einval = matches!( e, v4l2r::ioctl::FrameSizeError::IoctlError(err) if err == Errno::EINVAL ); let is_unsupported = matches!( e, v4l2r::ioctl::FrameSizeError::IoctlError(err) if matches!(err, Errno::ENOTTY | Errno::ENOSYS | Errno::EOPNOTSUPP) ); if is_unsupported && resolutions.is_empty() { should_fallback_to_current_mode = true; } if !is_einval && !is_unsupported { debug!("Failed to enumerate frame sizes for {:?}: {}", fourcc, e); } break; } } } if should_fallback_to_current_mode { if let Some(resolution) = self.current_mode_resolution_info() { if self.format_works_for_resolution(fourcc, resolution.width, resolution.height) { debug!( "Falling back to current active mode for {:?}: {}x{} @ {:?} fps", fourcc, resolution.width, resolution.height, resolution.fps ); resolutions.push(resolution); } else { debug!( "Skipping current-mode fallback for {:?}: TRY_FMT rejected {}x{}", fourcc, resolution.width, resolution.height ); } } } // Sort by resolution (largest first) resolutions.sort_by(|a, b| (b.width * b.height).cmp(&(a.width * a.height))); resolutions.dedup_by(|a, b| a.width == b.width && a.height == b.height); Ok(resolutions) } /// Enumerate FPS for a specific resolution fn enumerate_fps( &self, fourcc: v4l2r::PixelFormat, width: u32, height: u32, ) -> Result> { let mut fps_list = Vec::new(); let mut should_fallback_to_current_mode = false; let mut index = 0u32; loop { match ioctl::enum_frame_intervals::( &self.fd, index, fourcc, width, height, ) { Ok(interval) => { if let Some(interval) = interval.intervals() { match interval { FrmIvalTypes::Discrete(fraction) => { if fraction.numerator > 0 && fraction.denominator > 0 { let fps = fraction.denominator as f64 / fraction.numerator as f64; fps_list.push(fps); } } FrmIvalTypes::StepWise(step) => { if step.max.numerator > 0 && step.max.denominator > 0 { let min_fps = step.max.denominator as f64 / step.max.numerator as f64; let max_fps = step.min.denominator as f64 / step.min.numerator as f64; fps_list.push(min_fps); if max_fps != min_fps { fps_list.push(max_fps); } } } } } index += 1; } Err(e) => { let is_einval = matches!( e, v4l2r::ioctl::FrameIntervalsError::IoctlError(err) if err == Errno::EINVAL ); let is_unsupported = matches!( e, v4l2r::ioctl::FrameIntervalsError::IoctlError(err) if matches!(err, Errno::ENOTTY | Errno::ENOSYS | Errno::EOPNOTSUPP) ); if is_unsupported && fps_list.is_empty() { should_fallback_to_current_mode = true; } if !is_einval && !is_unsupported { debug!( "Failed to enumerate frame intervals for {:?} {}x{}: {}", fourcc, width, height, e ); } break; } } } if should_fallback_to_current_mode { fps_list.extend(self.current_mode_fps()); } normalize_fps_list(&mut fps_list); Ok(fps_list) } /// Get current format pub fn get_format(&self) -> Result { let queue = self.capture_queue_type()?; ioctl::g_fmt(&self.fd, queue) .map_err(|e| AppError::VideoError(format!("Failed to get format: {}", e))) } /// Set capture format pub fn set_format(&self, width: u32, height: u32, format: PixelFormat) -> Result { let queue = self.capture_queue_type()?; let mut fmt: V4l2rFormat = ioctl::g_fmt(&self.fd, queue) .map_err(|e| AppError::VideoError(format!("Failed to get format: {}", e)))?; fmt.width = width; fmt.height = height; fmt.pixelformat = format.to_v4l2r(); let mut fd = self .fd .try_clone() .map_err(|e| AppError::VideoError(format!("Failed to clone device fd: {}", e)))?; let actual: V4l2rFormat = ioctl::s_fmt(&mut fd, (queue, &fmt)) .map_err(|e| AppError::VideoError(format!("Failed to set format: {}", e)))?; if actual.width != width || actual.height != height { warn!( "Requested {}x{}, got {}x{}", width, height, actual.width, actual.height ); } Ok(actual) } /// Detect if device is likely an HDMI capture card fn detect_capture_card(card: &str, driver: &str, formats: &[FormatInfo]) -> bool { let card_lower = card.to_lowercase(); let driver_lower = driver.to_lowercase(); // Known capture card patterns let capture_patterns = [ "hdmi", "capture", "grabber", "usb3", "ms2109", "ms2130", "macrosilicon", "tc358743", "uvc", ]; // Check card/driver names for pattern in capture_patterns { if card_lower.contains(pattern) || driver_lower.contains(pattern) { return true; } } // Capture cards usually support MJPEG and high resolutions let has_mjpeg = formats.iter().any(|f| f.format == PixelFormat::Mjpeg); let has_1080p = formats.iter().any(|f| { f.resolutions .iter() .any(|r| r.width >= 1920 && r.height >= 1080) }); has_mjpeg && has_1080p } /// Calculate device priority for selection fn calculate_priority( _card: &str, driver: &str, formats: &[FormatInfo], is_capture_card: bool, ) -> u32 { let mut priority = 0u32; // Capture cards get highest priority if is_capture_card { priority += 1000; } // MJPEG support is valuable if formats.iter().any(|f| f.format == PixelFormat::Mjpeg) { priority += 100; } // High resolution support let max_resolution = formats .iter() .flat_map(|f| &f.resolutions) .map(|r| r.width * r.height) .max() .unwrap_or(0); priority += max_resolution / 100000; // Known good drivers get bonus let good_drivers = ["uvcvideo", "tc358743"]; if good_drivers.iter().any(|d| driver.contains(d)) { priority += 50; } priority } /// Get the inner device reference (for advanced usage) pub fn inner(&self) -> &File { &self.fd } fn current_mode_resolution_info(&self) -> Option { let (width, height) = self .current_dv_timings_mode() .map(|(width, height, _)| (width, height)) .or_else(|| self.current_format_resolution())?; Some(ResolutionInfo::new(width, height, self.current_mode_fps())) } fn current_mode_fps(&self) -> Vec { let mut fps = Vec::new(); if let Some(frame_rate) = self.current_parm_fps() { fps.push(frame_rate); } if let Some((_, _, Some(frame_rate))) = self.current_dv_timings_mode() { fps.push(frame_rate); } normalize_fps_list(&mut fps); fps } fn current_parm_fps(&self) -> Option { let queue = self.capture_queue_type().ok()?; let params: v4l2_streamparm = ioctl::g_parm(&self.fd, queue).ok()?; let capture = unsafe { params.parm.capture }; let timeperframe = capture.timeperframe; if timeperframe.numerator == 0 || timeperframe.denominator == 0 { return None; } Some(timeperframe.denominator as f64 / timeperframe.numerator as f64) } fn current_dv_timings_mode(&self) -> Option<(u32, u32, Option)> { let timings = ioctl::query_dv_timings::(&self.fd) .or_else(|_| ioctl::g_dv_timings::(&self.fd)) .ok()?; if timings.type_ != V4L2_DV_BT_656_1120 { return None; } let bt = unsafe { timings.__bindgen_anon_1.bt }; if bt.width == 0 || bt.height == 0 { return None; } Some((bt.width, bt.height, dv_timings_fps(&bt))) } fn current_format_resolution(&self) -> Option<(u32, u32)> { let format = self.get_format().ok()?; if format.width == 0 || format.height == 0 { return None; } Some((format.width, format.height)) } fn current_active_format(&self) -> Option { let format = self.get_format().ok()?; PixelFormat::from_v4l2r(format.pixelformat) } fn format_description(&self, fourcc: v4l2r::PixelFormat) -> Option { let queue = self.capture_queue_type().ok()?; FormatIterator::new(&self.fd, queue) .find(|desc| desc.pixelformat == fourcc) .map(|desc| desc.description) } fn format_works_for_resolution( &self, fourcc: v4l2r::PixelFormat, width: u32, height: u32, ) -> bool { let queue = match self.capture_queue_type() { Ok(queue) => queue, Err(_) => return false, }; let mut fmt = match ioctl::g_fmt::(&self.fd, queue) { Ok(fmt) => fmt, Err(_) => return false, }; fmt.width = width; fmt.height = height; fmt.pixelformat = fourcc; let actual = match ioctl::try_fmt::<_, V4l2rFormat>(&self.fd, (queue, &fmt)) { Ok(actual) => actual, Err(_) => return false, }; actual.pixelformat == fourcc && actual.width == width && actual.height == height } fn capture_queue_type(&self) -> Result { let caps = self.capabilities()?; if caps.video_capture { Ok(QueueType::VideoCapture) } else if caps.video_capture_mplane { Ok(QueueType::VideoCaptureMplane) } else { Err(AppError::VideoError( "Device does not expose a capture queue".to_string(), )) } } } /// Enumerate all video capture devices pub fn enumerate_devices() -> Result> { info!("Enumerating video devices..."); let mut devices = Vec::new(); // Scan /dev/video* devices for entry in std::fs::read_dir("/dev") .map_err(|e| AppError::VideoError(format!("Failed to read /dev: {}", e)))? { let entry = match entry { Ok(e) => e, Err(_) => continue, }; let path = entry.path(); let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); if !name.starts_with("video") { continue; } debug!("Found video device: {:?}", path); if !sysfs_maybe_capture(&path) { debug!("Skipping non-capture candidate (sysfs): {:?}", path); continue; } // Try to open and query the device (with timeout) match probe_device_with_timeout(&path, Duration::from_millis(DEVICE_PROBE_TIMEOUT_MS)) { Some(info) => { // Only include devices with video capture capability if info.capabilities.video_capture || info.capabilities.video_capture_mplane { info!( "Found capture device: {} ({}) - {} formats", info.name, info.driver, info.formats.len() ); devices.push(info); } else { debug!("Skipping non-capture device: {:?}", path); } } None => { debug!("Failed to probe {:?}", path); } } } // Sort by priority (highest first) devices.sort_by(|a, b| b.priority.cmp(&a.priority)); info!("Found {} video capture devices", devices.len()); Ok(devices) } fn probe_device_with_timeout(path: &Path, timeout: Duration) -> Option { let path = path.to_path_buf(); let path_for_thread = path.clone(); let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { let result = (|| -> Result { let device = VideoDevice::open_readonly(&path_for_thread)?; device.info() })(); let _ = tx.send(result); }); match rx.recv_timeout(timeout) { Ok(Ok(info)) => Some(info), Ok(Err(e)) => { debug!("Failed to get info for {:?}: {}", path, e); None } Err(mpsc::RecvTimeoutError::Timeout) => { warn!("Timed out probing video device: {:?}", path); None } Err(_) => None, } } fn sysfs_maybe_capture(path: &Path) -> bool { let name = match path.file_name().and_then(|n| n.to_str()) { Some(name) => name, None => return true, }; let sysfs_base = Path::new("/sys/class/video4linux").join(name); let sysfs_name = read_sysfs_string(&sysfs_base.join("name")) .unwrap_or_default() .to_lowercase(); let uevent = read_sysfs_string(&sysfs_base.join("device/uevent")) .unwrap_or_default() .to_lowercase(); let driver = extract_uevent_value(&uevent, "driver"); let mut maybe_capture = false; let capture_hints = [ "capture", "hdmi", "usb", "uvc", "ms2109", "ms2130", "macrosilicon", "tc358743", "grabber", ]; if capture_hints.iter().any(|hint| sysfs_name.contains(hint)) { maybe_capture = true; } if let Some(driver) = driver { if driver.contains("uvcvideo") || driver.contains("tc358743") { maybe_capture = true; } } let skip_hints = [ "codec", "decoder", "encoder", "isp", "mem2mem", "m2m", "vbi", "radio", "metadata", "output", ]; if skip_hints.iter().any(|hint| sysfs_name.contains(hint)) && !maybe_capture { return false; } true } fn read_sysfs_string(path: &Path) -> Option { std::fs::read_to_string(path) .ok() .map(|value| value.trim().to_string()) } fn extract_uevent_value(content: &str, key: &str) -> Option { let key_upper = key.to_ascii_uppercase(); for line in content.lines() { if let Some(value) = line.strip_prefix(&format!("{}=", key_upper)) { return Some(value.to_lowercase()); } } None } fn dv_timings_fps(bt: &v4l2_bt_timings) -> Option { let total_width = bt.width + bt.hfrontporch + bt.hsync + bt.hbackporch; let total_height = if bt.interlaced != 0 { bt.height + bt.vfrontporch + bt.vsync + bt.vbackporch + bt.il_vfrontporch + bt.il_vsync + bt.il_vbackporch } else { bt.height + bt.vfrontporch + bt.vsync + bt.vbackporch }; if bt.pixelclock == 0 || total_width == 0 || total_height == 0 { return None; } Some(bt.pixelclock as f64 / total_width as f64 / total_height as f64) } fn normalize_fps_list(fps_list: &mut Vec) { fps_list.retain(|fps| fps.is_finite() && *fps > 0.0); for fps in fps_list.iter_mut() { *fps = (*fps * 100.0).round() / 100.0; } fps_list.sort_by(|a, b| b.total_cmp(a)); fps_list.dedup_by(|a, b| (*a - *b).abs() < 0.01); } /// Find the best video device for KVM use pub fn find_best_device() -> Result { let devices = enumerate_devices()?; devices .into_iter() .next() .ok_or_else(|| AppError::VideoError("No video capture devices found".to_string())) } #[cfg(test)] mod tests { use super::*; #[test] fn test_pixel_format_conversion() { let format = PixelFormat::Mjpeg; let fourcc = format.to_fourcc(); let back = PixelFormat::from_fourcc(fourcc); assert_eq!(back, Some(format)); } #[test] fn test_resolution() { let res = Resolution::HD1080; assert_eq!(res.width, 1920); assert_eq!(res.height, 1080); assert!(res.is_valid()); } }