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, 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::Hardware,
20    platform_requirements: &["macos"],
21    entropy_rate_estimate: 50000.0,
22    composite: false,
23};
24
25/// Entropy source that harvests sensor noise from camera dark frames.
26pub struct CameraNoiseSource;
27
28impl EntropySource for CameraNoiseSource {
29    fn info(&self) -> &SourceInfo {
30        &CAMERA_NOISE_INFO
31    }
32
33    fn is_available(&self) -> bool {
34        cfg!(target_os = "macos") && command_exists("ffmpeg")
35    }
36
37    fn collect(&self, n_samples: usize) -> Vec<u8> {
38        // Capture one frame of raw grayscale video from the default camera.
39        // ffmpeg -f avfoundation -i "0" -frames:v 1 -f rawvideo -pix_fmt gray pipe:1
40        let result = std::process::Command::new("ffmpeg")
41            .args([
42                "-f",
43                "avfoundation",
44                "-i",
45                "0",
46                "-frames:v",
47                "1",
48                "-f",
49                "rawvideo",
50                "-pix_fmt",
51                "gray",
52                "pipe:1",
53            ])
54            .stdin(std::process::Stdio::null())
55            .stdout(std::process::Stdio::piped())
56            .stderr(std::process::Stdio::null())
57            .output();
58
59        let raw_frame = match result {
60            Ok(output) if output.status.success() => output.stdout,
61            _ => return Vec::new(),
62        };
63
64        // Extract the lower 4 bits of each pixel value and pack nibbles.
65        let nibbles = raw_frame.iter().map(|pixel| pixel & 0x0F);
66        pack_nibbles(nibbles, n_samples)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn camera_noise_info() {
76        let src = CameraNoiseSource;
77        assert_eq!(src.name(), "camera_noise");
78        assert_eq!(src.info().category, SourceCategory::Hardware);
79        assert_eq!(src.info().entropy_rate_estimate, 50000.0);
80    }
81}