mp4_stream/
capabilities.rs1use crate::config::{Config, Format};
29use crate::Error;
30use rscam::{IntervalInfo, ResolutionInfo};
31#[cfg(feature = "serde")]
32use serde::{Serialize, Serializer};
33use std::collections::{HashMap, HashSet};
34use std::{
35 ffi::OsStr,
36 fs,
37 path::{Path, PathBuf},
38};
39
40#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct Capabilities(pub HashMap<PathBuf, Formats>);
60
61pub type Formats = HashMap<Format, Resolutions>;
63
64pub type Resolutions = HashMap<(u32, u32), Intervals>;
68
69pub type Intervals = Vec<(u32, u32)>;
73
74#[cfg(feature = "serde")]
75impl Serialize for Capabilities {
76 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
77 let map: HashMap<PathBuf, HashMap<Format, Vec<Resolution>>> = self
78 .0
79 .clone()
80 .into_iter()
81 .map(|(path, formats)| {
82 (
83 path,
84 formats
85 .into_iter()
86 .map(|(format, resolutions)| {
87 #[allow(clippy::unwrap_used)] (
89 format,
90 resolutions
91 .into_iter()
92 .map(|(resolution, intervals)| Resolution {
93 resolution,
94 intervals,
95 })
96 .collect(),
97 )
98 })
99 .collect(),
100 )
101 })
102 .collect();
103 map.serialize(serializer)
104 }
105}
106
107#[cfg(feature = "serde")]
108#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
109struct Resolution {
110 resolution: (u32, u32),
111 intervals: Vec<(u32, u32)>,
112}
113
114#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug"))]
123pub fn get_capabilities_all() -> crate::Result<Capabilities> {
124 let mut caps = HashMap::new();
125
126 for f in fs::read_dir(PathBuf::from("/dev"))? {
127 let path = f?.path();
128 if path
129 .file_name()
130 .and_then(OsStr::to_str)
131 .map_or(false, |name| name.starts_with("video"))
132 {
133 let path_clone = path.clone();
134 let path_caps = get_capabilities_from_path(&path_clone)?;
135 caps.insert(path.clone(), path_caps);
136 }
137 }
138
139 Ok(Capabilities(caps))
140}
141
142#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug"))]
151pub fn get_capabilities_from_path(device: &Path) -> crate::Result<Formats> {
152 let camera = rscam::Camera::new(
153 device
154 .to_str()
155 .ok_or_else(|| "Failed to convert device path to string".to_string())?,
156 )?;
157 get_capabilities(&camera)
158}
159
160#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))]
161fn get_capabilities(camera: &rscam::Camera) -> crate::Result<Formats> {
162 camera
163 .formats()
164 .filter_map(|x| x.ok())
165 .filter_map(|fmt| {
166 u32::from_be_bytes(fmt.format)
167 .try_into()
168 .ok()
169 .map(|format| (fmt, format))
170 })
171 .map(|(fmt, format)| {
172 let resolutions: Result<_, Error> = get_resolutions(camera.resolutions(&fmt.format)?)
173 .into_iter()
174 .map(|resolution| {
175 Ok((
176 resolution,
177 get_intervals(camera.intervals(&fmt.format, resolution)?),
178 ))
179 })
180 .collect();
181 Ok((format, resolutions?))
182 })
183 .collect()
184}
185
186#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
187fn get_resolutions(resolutions: ResolutionInfo) -> Vec<(u32, u32)> {
188 match resolutions {
189 ResolutionInfo::Discretes(r) => r,
190 ResolutionInfo::Stepwise { min, max, step } => (min.0..max.0)
191 .filter(|x| (x - min.0) % step.0 == 0)
192 .zip((min.1..max.1).filter(|x| (x - min.1) % step.1 == 0))
193 .collect(),
194 }
195}
196
197#[cfg_attr(feature = "tracing", tracing::instrument(level = "trace"))]
198fn get_intervals(intervals: IntervalInfo) -> Vec<(u32, u32)> {
199 match intervals {
200 IntervalInfo::Discretes(r) => r,
201 IntervalInfo::Stepwise { min, max, step } => (min.0..max.0)
202 .filter(|x| (x - min.0) % step.0 == 0)
203 .zip((min.1..max.1).filter(|x| (x - min.1) % step.1 == 0))
204 .collect(),
205 }
206}
207
208#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(caps)))]
217pub fn check_config(config: &Config, caps: &Capabilities) -> crate::Result<()> {
218 caps.0
219 .get(&config.device)
220 .ok_or_else(|| format!("Invalid device: {:?}", config.device))?
221 .get(&config.format)
222 .ok_or_else(|| format!("Invalid format: {}", config.format))?
223 .get(&config.resolution)
224 .ok_or_else(|| format!("Invalid resolution: {:?}", config.resolution))?
225 .contains(&config.interval)
226 .then_some(())
227 .ok_or_else(|| format!("Invalid interval: {:?}", config.interval))?;
228
229 let camera = rscam::Camera::new(
230 config
231 .device
232 .as_os_str()
233 .to_str()
234 .ok_or_else(|| "failed to convert device path to string".to_string())?,
235 )?;
236
237 let controls: HashSet<String> = config.v4l2_controls.keys().cloned().collect();
238 let valid_controls: HashSet<String> = camera
239 .controls()
240 .filter_map(|x| x.ok())
241 .map(|ctl| ctl.name)
242 .collect();
243
244 for name in controls.difference(&valid_controls) {
246 if controls.get(name).is_none() {
247 return Err(Error::Other(format!("Invalid V4L2 control: '{name}'")));
248 }
249 }
250
251 Ok(())
252}