Skip to main content

openentropy_core/sources/
camera.rs

1//! CameraNoiseSource — Camera sensor dark current noise.
2//!
3//! Captures a single frame from the camera via ffmpeg's avfoundation backend
4//! as raw grayscale video, then extracts the lower 4 bits of each pixel value.
5//! In darkness, these LSBs are dominated by shot noise and dark current.
6
7use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
8
9use super::helpers::{command_exists, pack_nibbles};
10
11static CAMERA_NOISE_INFO: SourceInfo = SourceInfo {
12    name: "camera_noise",
13    description: "Camera sensor dark current and shot noise via ffmpeg",
14    physics: "Captures frames from the camera sensor in darkness. The sensor's photodiodes \
15              generate dark current from thermal electron-hole pair generation in silicon \
16              \u{2014} a quantum process. Read noise from the amplifier adds further randomness. \
17              The LSBs of pixel values in dark frames are dominated by shot noise \
18              (Poisson-distributed photon counting).",
19    category: SourceCategory::Sensor,
20    platform: Platform::MacOS,
21    requirements: &[Requirement::Camera],
22    entropy_rate_estimate: 50000.0,
23    composite: false,
24};
25
26/// Entropy source that harvests sensor noise from camera dark frames.
27pub struct CameraNoiseSource;
28
29impl EntropySource for CameraNoiseSource {
30    fn info(&self) -> &SourceInfo {
31        &CAMERA_NOISE_INFO
32    }
33
34    fn is_available(&self) -> bool {
35        cfg!(target_os = "macos") && command_exists("ffmpeg")
36    }
37
38    fn collect(&self, n_samples: usize) -> Vec<u8> {
39        // Capture one frame of raw grayscale video from the default camera.
40        // ffmpeg -f avfoundation -i "0" -frames:v 1 -f rawvideo -pix_fmt gray pipe:1
41        let result = std::process::Command::new("ffmpeg")
42            .args([
43                "-f",
44                "avfoundation",
45                "-i",
46                "0",
47                "-frames:v",
48                "1",
49                "-f",
50                "rawvideo",
51                "-pix_fmt",
52                "gray",
53                "pipe:1",
54            ])
55            .stdin(std::process::Stdio::null())
56            .stdout(std::process::Stdio::piped())
57            .stderr(std::process::Stdio::null())
58            .output();
59
60        let raw_frame = match result {
61            Ok(output) if output.status.success() => output.stdout,
62            _ => return Vec::new(),
63        };
64
65        // Extract the lower 4 bits of each pixel value and pack nibbles.
66        let nibbles = raw_frame.iter().map(|pixel| pixel & 0x0F);
67        pack_nibbles(nibbles, n_samples)
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn camera_noise_info() {
77        let src = CameraNoiseSource;
78        assert_eq!(src.name(), "camera_noise");
79        assert_eq!(src.info().category, SourceCategory::Sensor);
80        assert_eq!(src.info().entropy_rate_estimate, 50000.0);
81        assert!(!src.info().composite);
82    }
83
84    #[test]
85    #[cfg(target_os = "macos")]
86    #[ignore] // Requires camera and ffmpeg
87    fn camera_noise_collects_bytes() {
88        let src = CameraNoiseSource;
89        if src.is_available() {
90            let data = src.collect(64);
91            assert!(!data.is_empty());
92            assert!(data.len() <= 64);
93        }
94    }
95}