1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
//! The camera as an image source.
use std::path::PathBuf;
use std::time::Duration;
use asist_core::error::{Error, Result};
use crate::vision::{capture_jpeg, FrameSource};
const TOOL: &str = "mirar_por_la_camara";
#[derive(Debug, Clone)]
pub struct CameraConfig {
/// Dispositivo V4L2.
pub device: PathBuf,
/// Capture resolution. Measured with this model: 1.3 s at 320x240, 2.9 s
/// at 640x480 and 7.8 s at 1280x720, with the same useful description from
/// 640 up. Enough for a scene; not for reading text (that is the screen).
pub width: u32,
pub height: u32,
/// Frames discarded before keeping one.
///
/// The camera starts with auto-exposure not settled yet and the first frame
/// is usually blown out. Discarding a few is practically free: measured,
/// 0.45 s versus 0.53 s.
pub warmup_frames: u32,
pub timeout: Duration,
/// Directory to leave frames in. `None` = none is saved.
pub save_dir: Option<PathBuf>,
}
impl Default for CameraConfig {
fn default() -> Self {
Self {
device: PathBuf::from("/dev/video0"),
width: 640,
height: 480,
warmup_frames: 5,
timeout: Duration::from_secs(15),
save_dir: None,
}
}
}
pub struct Camera {
config: CameraConfig,
}
impl Camera {
pub fn new(config: CameraConfig) -> Self {
Self { config }
}
}
impl FrameSource for Camera {
fn label(&self) -> &str {
"cámara"
}
fn available(&self) -> Result<()> {
if !self.config.device.exists() {
return Err(Error::Tool {
tool: TOOL.into(),
message: format!(
"{} does not exist; check with «v4l2-ctl --list-devices»",
self.config.device.display()
),
});
}
Ok(())
}
fn capture(&self) -> Result<Vec<u8>> {
self.available()?;
// ffmpeg rather than raw V4L2: a USB camera delivers MJPEG, YUYV or
// whatever it likes, and reimplementing that negotiation to save one
// process is not worth it.
let mut args = vec![
"-hide_banner".into(),
"-loglevel".into(),
"error".into(),
"-nostdin".into(),
"-f".into(),
"v4l2".into(),
"-video_size".into(),
format!("{}x{}", self.config.width, self.config.height),
"-i".into(),
self.config.device.to_string_lossy().into_owned(),
];
if self.config.warmup_frames > 0 {
// N frames are read and the last one is kept: that way exposure
// settles without opening the device twice.
args.push("-vf".into());
args.push(format!("select=eq(n\\,{})", self.config.warmup_frames));
}
args.extend(
["-frames:v", "1", "-f", "image2", "-c:v", "mjpeg", "-"]
.iter()
.map(|s| s.to_string()),
);
let frame = capture_jpeg(TOOL, "ffmpeg".as_ref(), &args, self.config.timeout)?;
if let Some(dir) = &self.config.save_dir {
save(dir, &frame, "camara");
}
Ok(frame)
}
}
/// Saves a copy only if explicitly requested. By default nothing is
/// written: an assistant that leaves frames lying around is a privacy
/// problem, not a debugging convenience.
pub(crate) fn save(dir: &std::path::Path, bytes: &[u8], prefix: &str) {
let name = format!(
"{prefix}-{}.jpg",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
);
if let Err(err) =
std::fs::create_dir_all(dir).and_then(|()| std::fs::write(dir.join(name), bytes))
{
tracing::warn!(target: "vision", %err, "could not save the capture");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_device_is_detected_before_registering_the_tool() {
let camera = Camera::new(CameraConfig {
device: PathBuf::from("/dev/video-does-not-exist"),
..Default::default()
});
let err = camera.available().unwrap_err().to_string();
assert!(err.contains("does not exist"), "{err}");
assert!(
err.contains("v4l2-ctl"),
"the error must say how to check it: {err}"
);
}
#[test]
fn capturing_without_a_device_fails_before_ffmpeg() {
let camera = Camera::new(CameraConfig {
device: PathBuf::from("/dev/video-does-not-exist"),
..Default::default()
});
assert!(camera.capture().is_err());
}
#[test]
fn no_frame_is_saved_by_default() {
assert!(
CameraConfig::default().save_dir.is_none(),
"saving images by default would be a privacy leak"
);
}
#[test]
fn default_resolution_is_the_measured_balance() {
let config = CameraConfig::default();
assert_eq!((config.width, config.height), (640, 480));
}
}
|