use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use crate::error::{AppError, Result}; use crate::video::format::{PixelFormat, Resolution}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VideoDeviceInfo { pub path: PathBuf, pub name: String, pub driver: String, pub bus_info: String, pub card: String, pub formats: Vec, pub capabilities: DeviceCapabilities, pub is_capture_card: bool, pub priority: u32, pub has_signal: bool, pub subdev_path: Option, pub bridge_kind: Option, } #[derive(Debug, Clone)] pub struct VideoDeviceRecoveryHint { pub path: PathBuf, pub name: String, pub driver: String, pub bus_info: String, pub card: String, pub is_capture_card: bool, } impl From<&VideoDeviceInfo> for VideoDeviceRecoveryHint { fn from(device: &VideoDeviceInfo) -> Self { Self { path: device.path.clone(), name: device.name.clone(), driver: device.driver.clone(), bus_info: device.bus_info.clone(), card: device.card.clone(), is_capture_card: device.is_capture_card, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FormatInfo { pub format: PixelFormat, pub resolutions: Vec, pub description: String, } #[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) } } #[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, } pub struct VideoDevice { pub path: PathBuf, } pub(crate) const DIRECTSHOW_DEVICE_PREFIX: &str = "dshow:"; impl VideoDevice { pub fn open(path: impl AsRef) -> Result { let path = normalize_windows_device_path(path.as_ref()); if enumerate_devices()? .iter() .any(|device| device.path == path) { Ok(Self { path }) } else { Err(AppError::VideoError(format!( "Windows video device not found: {}", path.display() ))) } } pub fn open_readonly(path: impl AsRef) -> Result { Self::open(path) } pub fn info(&self) -> Result { enumerate_devices()? .into_iter() .find(|device| device.path == self.path) .ok_or_else(|| { AppError::VideoError(format!( "Windows video device not found: {}", self.path.display() )) }) } } pub(crate) fn normalize_windows_device_path(path: impl AsRef) -> PathBuf { let raw = path.as_ref().to_string_lossy(); if raw.eq_ignore_ascii_case("auto") { return find_best_device() .map(|device| device.path) .unwrap_or_else(|_| PathBuf::from(raw.as_ref())); } PathBuf::from(raw.as_ref()) } pub(crate) fn directshow_display_name_from_path(path: impl AsRef) -> Option { path.as_ref() .to_string_lossy() .strip_prefix(DIRECTSHOW_DEVICE_PREFIX) .map(str::to_string) } pub fn enumerate_devices() -> Result> { let names = hwcodec::capture::list_dshow_video_devices().map_err(|e| { AppError::VideoError(format!("Failed to enumerate DirectShow devices: {}", e)) })?; let mut devices = names .into_iter() .enumerate() .map(|(index, name)| directshow_device_from_name(index, name)) .collect::>(); devices.sort_by(|a, b| { b.priority .cmp(&a.priority) .then_with(|| a.name.cmp(&b.name)) }); Ok(devices) } pub fn find_best_device() -> Result { enumerate_devices()?.into_iter().next().ok_or_else(|| { AppError::VideoError("No DirectShow video capture devices found".to_string()) }) } pub fn parse_bridge_kind(value: Option<&str>) -> Option { value.and_then(|_| None) } pub fn select_recovery_device( devices: &[VideoDeviceInfo], hint: &VideoDeviceRecoveryHint, ) -> Option { devices .iter() .find(|device| device.path == hint.path || device.bus_info == hint.bus_info) .cloned() } fn directshow_device_from_name(index: usize, name: String) -> VideoDeviceInfo { let name = if name.trim().is_empty() { format!("Windows Capture Device {}", index + 1) } else { name }; let path = PathBuf::from(format!("{}{}", DIRECTSHOW_DEVICE_PREFIX, name)); let formats = enumerate_directshow_formats(&name); let priority = score_capture_device(&name, &path.to_string_lossy(), &formats); VideoDeviceInfo { path, name: name.clone(), driver: "directshow".to_string(), bus_info: name.clone(), card: name, formats, capabilities: DeviceCapabilities { video_capture: true, video_capture_mplane: false, video_output: false, streaming: true, read_write: false, }, is_capture_card: true, priority, has_signal: true, subdev_path: None, bridge_kind: None, } } fn enumerate_directshow_formats(name: &str) -> Vec { let Ok(capabilities) = hwcodec::capture::list_dshow_device_capabilities(name) else { return fallback_windows_formats(); }; let mut formats: Vec = Vec::new(); for capability in capabilities { let Some(format) = map_capture_format(capability.format) else { continue; }; if capability.width == 0 || capability.height == 0 { continue; } if let Some(existing) = formats.iter_mut().find(|info| info.format == format) { merge_resolution( &mut existing.resolutions, capability.width, capability.height, &capability.fps, ); continue; } let mut resolutions = Vec::new(); merge_resolution( &mut resolutions, capability.width, capability.height, &capability.fps, ); formats.push(FormatInfo { format, resolutions, description: format_description(format).to_string(), }); } for info in &mut formats { info.resolutions.sort_by(|left, right| { b_pixels(right) .cmp(&b_pixels(left)) .then_with(|| right.width.cmp(&left.width)) .then_with(|| right.height.cmp(&left.height)) }); } formats.sort_by(|a, b| { b.format .priority() .cmp(&a.format.priority()) .then_with(|| a.description.cmp(&b.description)) }); if formats.is_empty() { fallback_windows_formats() } else { formats } } fn merge_resolution(resolutions: &mut Vec, width: u32, height: u32, fps: &[u32]) { if let Some(existing) = resolutions .iter_mut() .find(|resolution| resolution.width == width && resolution.height == height) { existing.fps.extend(fps.iter().map(|value| *value as f64)); normalize_fps_list(&mut existing.fps); return; } let mut fps_values = fps.iter().map(|value| *value as f64).collect::>(); normalize_fps_list(&mut fps_values); resolutions.push(ResolutionInfo::new(width, height, fps_values)); } fn b_pixels(resolution: &ResolutionInfo) -> u32 { resolution.width.saturating_mul(resolution.height) } fn map_capture_format(format: hwcodec::capture::CapturePixelFormat) -> Option { match format { hwcodec::capture::CapturePixelFormat::Mjpeg => Some(PixelFormat::Mjpeg), hwcodec::capture::CapturePixelFormat::Jpeg => Some(PixelFormat::Jpeg), hwcodec::capture::CapturePixelFormat::Yuyv => Some(PixelFormat::Yuyv), hwcodec::capture::CapturePixelFormat::Yvyu => Some(PixelFormat::Yvyu), hwcodec::capture::CapturePixelFormat::Uyvy => Some(PixelFormat::Uyvy), hwcodec::capture::CapturePixelFormat::Nv12 => Some(PixelFormat::Nv12), hwcodec::capture::CapturePixelFormat::Nv21 => Some(PixelFormat::Nv21), hwcodec::capture::CapturePixelFormat::Nv16 => Some(PixelFormat::Nv16), hwcodec::capture::CapturePixelFormat::Nv24 => Some(PixelFormat::Nv24), hwcodec::capture::CapturePixelFormat::Yuv420 => Some(PixelFormat::Yuv420), hwcodec::capture::CapturePixelFormat::Yvu420 => Some(PixelFormat::Yvu420), hwcodec::capture::CapturePixelFormat::Rgb24 => Some(PixelFormat::Rgb24), hwcodec::capture::CapturePixelFormat::Bgr24 => Some(PixelFormat::Bgr24), hwcodec::capture::CapturePixelFormat::Grey => Some(PixelFormat::Grey), hwcodec::capture::CapturePixelFormat::Unknown => None, } } 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); } fn format_description(format: PixelFormat) -> &'static str { match format { PixelFormat::Mjpeg => "MJPEG", PixelFormat::Jpeg => "JPEG", PixelFormat::Yuyv => "YUYV 4:2:2", PixelFormat::Yvyu => "YVYU 4:2:2", PixelFormat::Uyvy => "UYVY 4:2:2", PixelFormat::Nv12 => "NV12", PixelFormat::Nv21 => "NV21", PixelFormat::Nv16 => "NV16", PixelFormat::Nv24 => "NV24", PixelFormat::Yuv420 => "YUV420", PixelFormat::Yvu420 => "YVU420", PixelFormat::Rgb565 => "RGB565", PixelFormat::Rgb24 => "RGB24", PixelFormat::Bgr24 => "BGR24", PixelFormat::Grey => "GREY", } } fn score_capture_device(name: &str, device_id: &str, formats: &[FormatInfo]) -> u32 { let haystack = format!("{} {}", name, device_id).to_ascii_lowercase(); let mut score = 50; if formats .iter() .any(|format| format.format == PixelFormat::Mjpeg) { score += 25; } for keyword in ["capture", "hdmi", "uvc", "video", "usb"] { if haystack.contains(keyword) { score += 10; } } score } fn fallback_windows_formats() -> Vec { vec![FormatInfo { format: PixelFormat::Mjpeg, resolutions: Vec::new(), description: "DirectShow auto-detected stream format".to_string(), }] }