mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-13 02:54:26 +08:00
360 lines
11 KiB
Rust
360 lines
11 KiB
Rust
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<FormatInfo>,
|
|
pub capabilities: DeviceCapabilities,
|
|
pub is_capture_card: bool,
|
|
pub priority: u32,
|
|
pub has_signal: bool,
|
|
pub subdev_path: Option<PathBuf>,
|
|
pub bridge_kind: Option<String>,
|
|
}
|
|
|
|
#[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<ResolutionInfo>,
|
|
pub description: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ResolutionInfo {
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub fps: Vec<f64>,
|
|
}
|
|
|
|
impl ResolutionInfo {
|
|
pub fn new(width: u32, height: u32, fps: Vec<f64>) -> 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<Path>) -> Result<Self> {
|
|
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<Path>) -> Result<Self> {
|
|
Self::open(path)
|
|
}
|
|
|
|
pub fn info(&self) -> Result<VideoDeviceInfo> {
|
|
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<Path>) -> 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<Path>) -> Option<String> {
|
|
path.as_ref()
|
|
.to_string_lossy()
|
|
.strip_prefix(DIRECTSHOW_DEVICE_PREFIX)
|
|
.map(str::to_string)
|
|
}
|
|
|
|
pub fn enumerate_devices() -> Result<Vec<VideoDeviceInfo>> {
|
|
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::<Vec<_>>();
|
|
|
|
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<VideoDeviceInfo> {
|
|
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<super::bridge::CsiBridgeKind> {
|
|
value.and_then(|_| None)
|
|
}
|
|
|
|
pub fn select_recovery_device(
|
|
devices: &[VideoDeviceInfo],
|
|
hint: &VideoDeviceRecoveryHint,
|
|
) -> Option<VideoDeviceInfo> {
|
|
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<FormatInfo> {
|
|
let Ok(capabilities) = hwcodec::capture::list_dshow_device_capabilities(name) else {
|
|
return fallback_windows_formats();
|
|
};
|
|
|
|
let mut formats: Vec<FormatInfo> = 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<ResolutionInfo>, 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::<Vec<_>>();
|
|
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<PixelFormat> {
|
|
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<f64>) {
|
|
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<FormatInfo> {
|
|
vec![FormatInfo {
|
|
format: PixelFormat::Mjpeg,
|
|
resolutions: Vec::new(),
|
|
description: "DirectShow auto-detected stream format".to_string(),
|
|
}]
|
|
}
|