use std::fs;
use std::path::{Path, PathBuf};
use v4l::frameinterval::FrameIntervalEnum;
use v4l::video::Capture;
use v4l::{Device, FourCC};
use crate::Camera;
const SYSFS_V4L: &str = "/sys/class/video4linux";
const BY_ID_DIR: &str = "/dev/v4l/by-id";
pub(crate) struct Node {
pub(crate) path: PathBuf,
pub(crate) name: String,
pub(crate) vendor_id: u16,
pub(crate) product_id: u16,
pub(crate) serial_number: Option<String>,
}
pub(crate) fn nodes() -> Vec<Node> {
let Ok(entries) = fs::read_dir(SYSFS_V4L) else {
return Vec::new();
};
let mut nodes: Vec<Node> = entries
.flatten()
.filter_map(|entry| {
let sysfs = entry.path();
let dev_path = PathBuf::from("/dev").join(entry.file_name());
let (vendor_id, product_id) = usb_ids(&sysfs)?;
if !is_capture_node(&dev_path) {
return None;
}
Some(Node {
name: read_trimmed(&sysfs.join("name"))
.unwrap_or_else(|| dev_path.display().to_string()),
path: dev_path,
vendor_id,
product_id,
serial_number: usb_serial(&sysfs),
})
})
.collect();
nodes.sort_by(|a, b| a.path.cmp(&b.path));
nodes
}
pub(crate) fn node_for_unique_id(unique_id: &str) -> Option<PathBuf> {
let target = fs::canonicalize(unique_id).ok()?;
nodes()
.into_iter()
.find(|node| fs::canonicalize(&node.path).is_ok_and(|p| p == target))
.map(|node| node.path)
}
pub(crate) fn describe(node: &Node) -> Camera {
let (max_resolution, max_fps) =
Device::with_path(&node.path).map_or((None, None), |device| max_format(&device));
Camera {
name: node.name.clone(),
unique_id: unique_id_for(&node.path),
serial_number: node.serial_number.clone(),
vendor_id: node.vendor_id,
product_id: node.product_id,
max_resolution,
max_fps,
}
}
fn unique_id_for(path: &Path) -> String {
let canonical = fs::canonicalize(path).ok();
let by_id = fs::read_dir(BY_ID_DIR).ok().and_then(|entries| {
entries
.flatten()
.map(|entry| entry.path())
.find(|link| fs::canonicalize(link).ok() == canonical)
});
by_id
.unwrap_or_else(|| path.to_path_buf())
.display()
.to_string()
}
fn usb_ids(sysfs: &Path) -> Option<(u16, u16)> {
let usb = usb_device_sysfs(sysfs)?;
let vendor = read_trimmed(&usb.join("idVendor"))?;
let product = read_trimmed(&usb.join("idProduct"))?;
Some((
u16::from_str_radix(&vendor, 16).ok()?,
u16::from_str_radix(&product, 16).ok()?,
))
}
fn usb_serial(sysfs: &Path) -> Option<String> {
let usb = usb_device_sysfs(sysfs)?;
let serial = read_trimmed(&usb.join("serial"))?;
let serial = serial.trim();
if serial.is_empty() || serial == "0" {
return None;
}
Some(serial.to_string())
}
fn usb_device_sysfs(sysfs: &Path) -> Option<PathBuf> {
fs::canonicalize(sysfs.join("device").join("..")).ok()
}
fn is_capture_node(dev_path: &Path) -> bool {
Device::with_path(dev_path)
.and_then(|device| device.enum_formats())
.is_ok_and(|formats| !formats.is_empty())
}
fn max_format(device: &Device) -> (Option<(u32, u32)>, Option<u32>) {
let Ok(formats) = device.enum_formats() else {
return (None, None);
};
let mut max_resolution: Option<(u32, u32)> = None;
let mut max_fps: Option<u32> = None;
for format in formats {
let Ok(sizes) = device.enum_framesizes(format.fourcc) else {
continue;
};
for size in sizes {
for discrete in size.size.to_discrete() {
let candidate = (discrete.width, discrete.height);
if max_resolution.is_none_or(|(w, h)| {
u64::from(candidate.0) * u64::from(candidate.1) > u64::from(w) * u64::from(h)
}) {
max_resolution = Some(candidate);
}
if let Some(fps) = max_discrete_fps(device, format.fourcc, candidate) {
max_fps = Some(max_fps.map_or(fps, |best: u32| best.max(fps)));
}
}
}
}
(max_resolution, max_fps)
}
fn max_discrete_fps(device: &Device, fourcc: FourCC, size: (u32, u32)) -> Option<u32> {
let intervals = device.enum_frameintervals(fourcc, size.0, size.1).ok()?;
intervals
.into_iter()
.filter_map(|interval| match interval.interval {
FrameIntervalEnum::Discrete(fraction) if fraction.numerator > 0 => {
Some(fraction.denominator / fraction.numerator)
}
_ => None,
})
.max()
}
fn read_trimmed(path: &Path) -> Option<String> {
fs::read_to_string(path)
.ok()
.map(|text| text.trim().to_string())
}