mp4_stream/
capabilities.rs

1//! Utilities to detect a camera's capabilities.
2//!
3//! The easiest way to get the capabilities is with the [`get_capabilities_all`]
4//! function. It will look for all camera devices (paths that match `/dev/video*`) and get the
5//! available formats, resolutions, and framerates for each. If you want to get capabilities
6//! for device paths that don't match that pattern, you can use [`get_capabilities_from_path`].
7//!
8//! This module also provides a [`check_config`] function to check whether a [`Config`](crate::config::Config)
9//! is supported by a set of capabilities. You should always validate a config with [`check_config`]
10//! before giving it to [`stream_media_segments`](crate::stream_media_segments).
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! use mp4_stream::{
16//!     capabilities::{get_capabilities_all, check_config},
17//!     config::Config,
18//! };
19//!
20//! let config = Config::default();
21//! let capabilities = get_capabilities_all()?;
22//! if check_config(&config, &capabilities).is_ok() {
23//!     println!("All good!");
24//! }
25//! # Ok::<(), Box<dyn std::error::Error>>(())
26//! ```
27
28use 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/// A map of device paths to available formats.
41///
42/// It serializes like this since JSON only supports string keys:
43///
44/// ```json
45/// {
46///   "/dev/video0": {
47///     "YUYV": [
48///       {
49///         "resolution": [640, 480],
50///         "intervals": [
51///           [1, 30]
52///         ]
53///       }
54///     ]
55///   }
56/// }
57/// ```
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct Capabilities(pub HashMap<PathBuf, Formats>);
60
61/// A map of format codes to available resolutions.
62pub type Formats = HashMap<Format, Resolutions>;
63
64/// A map of resolutions to available intervals.
65///
66/// The resolutions are in (width, height) format.
67pub type Resolutions = HashMap<(u32, u32), Intervals>;
68
69/// A list of available intervals.
70///
71/// The framerate for an interval is the first tuple field divided by the second.
72pub 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)] // FourCC codes are always printable ASCII
88                            (
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/// Gets the camera devices and capabilities for each.
115///
116/// See the [module-level docs](self) for more information.
117///
118/// # Errors
119///
120/// This function may return a [`Error::Io`] if interaction with the filesystem
121/// fails or a [`Error::Camera`] if the camera returns an error.
122#[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/// Gets the capabilities of a camera at a certain path.
143///
144/// See the [module-level docs](self) for more information.
145///
146/// # Errors
147///
148/// This function may return a [`Error::Io`] if interaction with the filesystem
149/// fails or a [`Error::Camera`] if the camera returns an error.
150#[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/// Verifies that a config is valid using a given set of capabilities.
209///
210/// See the [module-level docs](self) for more information.
211///
212/// # Errors
213///
214/// This function may return a [`Error::Other`] if any part of the config is invalid,
215/// including the V4L2 controls.
216#[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    // all controls that are in `controls` but not `valid_controls`.
245    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}