use crate::config::{Config, Format};
use crate::Error;
use rscam::{IntervalInfo, ResolutionInfo};
#[cfg(feature = "serde")]
use serde::{Serialize, Serializer};
use std::collections::{HashMap, HashSet};
use std::{
ffi::OsStr,
fs,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Capabilities(pub HashMap<PathBuf, Formats>);
pub type Formats = HashMap<Format, Resolutions>;
pub type Resolutions = HashMap<(u32, u32), Intervals>;
pub type Intervals = Vec<(u32, u32)>;
#[cfg(feature = "serde")]
impl Serialize for Capabilities {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let map: HashMap<PathBuf, HashMap<Format, Vec<Resolution>>> = self
.0
.clone()
.into_iter()
.map(|(path, formats)| {
(
path,
formats
.into_iter()
.map(|(format, resolutions)| {
#[allow(clippy::unwrap_used)] (
format,
resolutions
.into_iter()
.map(|(resolution, intervals)| Resolution {
resolution,
intervals,
})
.collect(),
)
})
.collect(),
)
})
.collect();
map.serialize(serializer)
}
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct Resolution {
resolution: (u32, u32),
intervals: Vec<(u32, u32)>,
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug"))]
pub fn get_capabilities_all() -> crate::Result<Capabilities> {
let mut caps = HashMap::new();
for f in fs::read_dir(PathBuf::from("/dev"))? {
let path = f?.path();
if path
.file_name()
.and_then(OsStr::to_str)
.map_or(false, |name| name.starts_with("video"))
{
let path_clone = path.clone();
let path_caps = get_capabilities_from_path(&path_clone)?;
caps.insert(path.clone(), path_caps);
}
}
Ok(Capabilities(caps))
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug"))]
pub fn get_capabilities_from_path(device: &Path) -> crate::Result<Formats> {
let camera = rscam::Camera::new(
device
.to_str()
.ok_or_else(|| "Failed to convert device path to string".to_string())?,
)?;
get_capabilities(&camera)
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
fn get_capabilities(camera: &rscam::Camera) -> crate::Result<Formats> {
camera
.formats()
.filter_map(|x| x.ok())
.filter_map(|fmt| {
u32::from_be_bytes(fmt.format)
.try_into()
.ok()
.map(|format| (fmt, format))
})
.map(|(fmt, format)| {
let resolutions: Result<_, Error> = get_resolutions(camera.resolutions(&fmt.format)?)
.into_iter()
.map(|resolution| {
Ok((
resolution,
get_intervals(camera.intervals(&fmt.format, resolution)?),
))
})
.collect();
Ok((format, resolutions?))
})
.collect()
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
fn get_resolutions(resolutions: ResolutionInfo) -> Vec<(u32, u32)> {
match resolutions {
ResolutionInfo::Discretes(r) => r,
ResolutionInfo::Stepwise { min, max, step } => (min.0..max.0)
.filter(|x| (x - min.0) % step.0 == 0)
.zip((min.1..max.1).filter(|x| (x - min.1) % step.1 == 0))
.collect(),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
fn get_intervals(intervals: IntervalInfo) -> Vec<(u32, u32)> {
match intervals {
IntervalInfo::Discretes(r) => r,
IntervalInfo::Stepwise { min, max, step } => (min.0..max.0)
.filter(|x| (x - min.0) % step.0 == 0)
.zip((min.1..max.1).filter(|x| (x - min.1) % step.1 == 0))
.collect(),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(caps)))]
pub fn check_config(config: &Config, caps: &Capabilities) -> crate::Result<()> {
caps.0
.get(&config.device)
.ok_or_else(|| format!("Invalid device: {:?}", config.device))?
.get(&config.format)
.ok_or_else(|| format!("Invalid format: {}", config.format))?
.get(&config.resolution)
.ok_or_else(|| format!("Invalid resolution: {:?}", config.resolution))?
.contains(&config.interval)
.then_some(())
.ok_or_else(|| format!("Invalid interval: {:?}", config.interval))?;
let camera = rscam::Camera::new(
config
.device
.as_os_str()
.to_str()
.ok_or_else(|| "failed to convert device path to string".to_string())?,
)?;
let controls: HashSet<String> = config.v4l2_controls.keys().cloned().collect();
let valid_controls: HashSet<String> = camera
.controls()
.filter_map(|x| x.ok())
.map(|ctl| ctl.name)
.collect();
for name in controls.difference(&valid_controls) {
if controls.get(name).is_none() {
return Err(Error::Other(format!("Invalid V4L2 control: '{name}'")));
}
}
Ok(())
}