openentropy_core/sources/sensor/
camera.rs1use std::io::Read;
13use std::process::{Child, Command, Stdio};
14use std::sync::{Arc, Mutex};
15use std::thread::{self, JoinHandle};
16
17use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
18
19use crate::sources::helpers::{command_exists, pack_nibbles};
20
21const FRAME_WIDTH: usize = 320;
22const FRAME_HEIGHT: usize = 240;
23const FRAME_SIZE: usize = FRAME_WIDTH * FRAME_HEIGHT; static CAMERA_NOISE_INFO: SourceInfo = SourceInfo {
26 name: "camera_noise",
27 description: "Camera sensor noise (read noise + dark current) via ffmpeg",
28 physics: "Captures frames from the camera sensor in darkness. Noise sources: (1) read \
29 noise from the amplifier \u{2014} classical analog noise, dominates at short \
30 exposures (~95%+ of variance); (2) dark current from thermal carrier generation \
31 in silicon \u{2014} classical at sensor operating temperatures; (3) dark current \
32 shot noise (Poisson counting) \u{2014} ~1-5% of variance in typical webcams. \
33 The LSBs of pixel values mix all three components.",
34 category: SourceCategory::Sensor,
35 platform: Platform::MacOS,
36 requirements: &[Requirement::Camera],
37 entropy_rate_estimate: 5.0,
38 composite: false,
39 is_fast: false,
40};
41
42pub struct CameraNoiseConfig {
44 pub device_index: Option<u32>,
47}
48
49impl Default for CameraNoiseConfig {
50 fn default() -> Self {
51 let device_index = std::env::var("OPENENTROPY_CAMERA_DEVICE")
52 .ok()
53 .and_then(|s| s.parse().ok());
54 Self { device_index }
55 }
56}
57
58struct PersistentCamera {
69 child: Child,
70 latest_frame: Arc<Mutex<Option<Vec<u8>>>>,
71 _reader: JoinHandle<()>,
72}
73
74impl PersistentCamera {
75 fn spawn(device_index: Option<u32>) -> Option<Self> {
81 let inputs: Vec<String> = match device_index {
82 Some(n) => vec![format!("{n}:none")],
83 None => vec![
84 "default:none".into(),
85 "0:none".into(),
86 "1:none".into(),
87 "0:0".into(),
88 ],
89 };
90
91 for input in &inputs {
92 if let Some(cam) = Self::try_spawn(input) {
93 return Some(cam);
94 }
95 }
96 None
97 }
98
99 fn try_spawn(input: &str) -> Option<Self> {
105 let size = format!("{}x{}", FRAME_WIDTH, FRAME_HEIGHT);
106 let mut child = Command::new("ffmpeg")
107 .args([
108 "-hide_banner",
109 "-loglevel",
110 "error",
111 "-nostdin",
112 "-f",
113 "avfoundation",
114 "-framerate",
115 "30",
116 "-video_size",
117 &size,
118 "-i",
119 input,
120 "-f",
121 "rawvideo",
122 "-pix_fmt",
123 "gray",
124 "pipe:1",
125 ])
126 .stdin(Stdio::null())
127 .stdout(Stdio::piped())
128 .stderr(Stdio::null())
129 .spawn()
130 .ok()?;
131
132 let stdout = child.stdout.take()?;
133 let latest_frame: Arc<Mutex<Option<Vec<u8>>>> = Arc::new(Mutex::new(None));
134 let frame_ref = Arc::clone(&latest_frame);
135
136 let reader = thread::spawn(move || {
137 Self::reader_loop(stdout, frame_ref);
138 });
139
140 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
142 loop {
143 if std::time::Instant::now() >= deadline {
144 let _ = child.kill();
146 let _ = child.wait();
147 return None;
149 }
150 {
151 let guard = latest_frame.lock().unwrap();
152 if guard.is_some() {
153 break;
154 }
155 }
156 thread::sleep(std::time::Duration::from_millis(25));
157 }
158
159 Some(PersistentCamera {
160 child,
161 latest_frame,
162 _reader: reader,
163 })
164 }
165
166 fn reader_loop(
172 mut stdout: std::process::ChildStdout,
173 latest_frame: Arc<Mutex<Option<Vec<u8>>>>,
174 ) {
175 let mut buf = vec![0u8; FRAME_SIZE];
176 loop {
177 match stdout.read_exact(&mut buf) {
178 Ok(()) => {
179 let mut guard = latest_frame.lock().unwrap();
180 *guard = Some(buf.clone());
181 }
182 Err(_) => {
183 let mut guard = latest_frame.lock().unwrap();
185 *guard = None;
186 return;
187 }
188 }
189 }
190 }
191
192 fn take_frame(&self) -> Option<Vec<u8>> {
194 let guard = self.latest_frame.lock().unwrap();
195 guard.clone()
196 }
197
198 fn is_dead(&self) -> bool {
200 let guard = self.latest_frame.lock().unwrap();
201 guard.is_none()
202 }
203}
204
205impl Drop for PersistentCamera {
206 fn drop(&mut self) {
207 let _ = self.child.kill();
208 let _ = self.child.wait();
209 }
211}
212
213pub struct CameraNoiseSource {
219 pub config: CameraNoiseConfig,
220 camera: Mutex<Option<PersistentCamera>>,
221}
222
223impl Default for CameraNoiseSource {
224 fn default() -> Self {
225 Self {
226 config: CameraNoiseConfig::default(),
227 camera: Mutex::new(None),
228 }
229 }
230}
231
232impl EntropySource for CameraNoiseSource {
233 fn info(&self) -> &SourceInfo {
234 &CAMERA_NOISE_INFO
235 }
236
237 fn is_available(&self) -> bool {
238 cfg!(target_os = "macos") && command_exists("ffmpeg")
239 }
240
241 fn collect(&self, n_samples: usize) -> Vec<u8> {
242 let mut guard = self.camera.lock().unwrap();
243
244 if guard.is_none() {
246 *guard = PersistentCamera::spawn(self.config.device_index);
247 }
248
249 let cam = match guard.as_ref() {
251 Some(c) => c,
252 None => return Vec::new(),
253 };
254
255 let raw_frame = match cam.take_frame() {
257 Some(frame) if !frame.is_empty() => frame,
258 _ => {
259 if cam.is_dead() {
261 *guard = None;
262 }
263 return Vec::new();
264 }
265 };
266
267 let nibbles = raw_frame.iter().map(|pixel| pixel & 0x0F);
269 pack_nibbles(nibbles, n_samples)
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn camera_noise_info() {
279 let src = CameraNoiseSource::default();
280 assert_eq!(src.name(), "camera_noise");
281 assert_eq!(src.info().category, SourceCategory::Sensor);
282 assert_eq!(src.info().entropy_rate_estimate, 5.0);
283 assert!(!src.info().composite);
284 }
285
286 #[test]
287 fn camera_config_default_is_none() {
288 let config = CameraNoiseConfig { device_index: None };
290 assert!(config.device_index.is_none());
291 }
292
293 #[test]
294 fn camera_config_explicit_device() {
295 let src = CameraNoiseSource {
296 config: CameraNoiseConfig {
297 device_index: Some(1),
298 },
299 camera: Mutex::new(None),
300 };
301 assert_eq!(src.config.device_index, Some(1));
302 }
303
304 #[test]
305 #[cfg(target_os = "macos")]
306 #[ignore] fn camera_noise_collects_bytes() {
308 let src = CameraNoiseSource::default();
309 if src.is_available() {
310 let data = src.collect(64);
311 assert!(!data.is_empty());
312 assert!(data.len() <= 64);
313 }
314 }
315
316 #[test]
317 fn camera_source_is_send_sync() {
318 fn assert_send_sync<T: Send + Sync>() {}
319 assert_send_sync::<CameraNoiseSource>();
320 }
321
322 #[test]
323 fn persistent_camera_constants() {
324 assert_eq!(FRAME_WIDTH, 320);
325 assert_eq!(FRAME_HEIGHT, 240);
326 assert_eq!(FRAME_SIZE, 320 * 240);
327 assert_eq!(FRAME_SIZE, 76800);
328 }
329}