//! Camera Interface Module //! //! This module provides camera enumeration, frame capture, and IR camera detection //! for the Linux Hello facial authentication system. //! //! # Overview //! //! Linux Hello requires an infrared (IR) camera for secure authentication. IR cameras //! are preferred because they: //! //! - Work in low light conditions //! - Are harder to spoof with photos (IR reflects differently from screens) //! - Provide consistent imaging regardless of ambient lighting //! //! # Camera Detection //! //! The module automatically detects IR cameras by checking: //! - Device names containing "IR", "Infrared", or "Windows Hello" //! - V4L2 capabilities and supported formats //! - Known IR camera vendor/product IDs //! //! # Platform Support //! //! - **Linux** - Full V4L2 support via the `linux` submodule //! - **Other platforms** - Mock camera for development and testing //! //! # Example: Enumerate Cameras //! //! ```rust,ignore //! use linux_hello_daemon::camera::{enumerate_cameras, Camera}; //! //! // Find all available cameras //! let cameras = enumerate_cameras().expect("Failed to enumerate cameras"); //! //! for camera in &cameras { //! println!("Found: {} (IR: {})", camera.name, camera.is_ir); //! } //! //! // Find the first IR camera //! if let Some(ir_cam) = cameras.iter().find(|c| c.is_ir) { //! let mut camera = Camera::open(&ir_cam.device_path)?; //! camera.start()?; //! let frame = camera.capture_frame()?; //! println!("Captured {}x{} frame", frame.width, frame.height); //! } //! ``` mod ir_emitter; #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] pub use linux::*; // IrEmitterControl is exported for use in tests and external code // The unused import warning is expected since it's primarily used in tests #[allow(unused_imports)] pub use ir_emitter::IrEmitterControl; /// Information about a detected camera device. /// /// This structure contains metadata about a camera, including its device path, /// name, whether it's an IR camera, and supported resolutions. /// /// # Example /// /// ```rust /// use linux_hello_daemon::CameraInfo; /// /// let info = CameraInfo { /// device_path: "/dev/video0".to_string(), /// name: "Integrated IR Camera".to_string(), /// is_ir: true, /// resolutions: vec![(640, 480), (1280, 720)], /// }; /// /// if info.is_ir { /// println!("Found IR camera: {}", info.name); /// } /// ``` #[derive(Debug, Clone)] #[allow(dead_code)] // Public API, fields may be used by external code pub struct CameraInfo { /// Device path (e.g., "/dev/video0" on Linux). pub device_path: String, /// Human-readable camera name from the driver. pub name: String, /// Whether this camera appears to be an IR (infrared) camera. /// Detected based on name patterns and capabilities. pub is_ir: bool, /// List of supported resolutions as (width, height) pairs. pub resolutions: Vec<(u32, u32)>, } impl std::fmt::Display for CameraInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{} ({}){}", self.name, self.device_path, if self.is_ir { " [IR]" } else { "" } ) } } /// A captured video frame from the camera. /// /// Contains the raw pixel data along with metadata about dimensions, /// format, and timing. Used throughout the authentication pipeline. /// /// # Memory Layout /// /// The `data` field contains raw pixel bytes. For grayscale images, /// this is one byte per pixel in row-major order. For YUYV, pixels /// are packed as Y0 U Y1 V (4 bytes per 2 pixels). /// /// # Example /// /// ```rust /// use linux_hello_daemon::{Frame, PixelFormat}; /// /// let frame = Frame { /// data: vec![128; 640 * 480], // 640x480 grayscale /// width: 640, /// height: 480, /// format: PixelFormat::Grey, /// timestamp_us: 0, /// }; /// /// assert_eq!(frame.data.len(), (frame.width * frame.height) as usize); /// ``` #[derive(Debug)] #[allow(dead_code)] // Public API, used by camera implementations pub struct Frame { /// Raw pixel data in the specified format. pub data: Vec, /// Frame width in pixels. pub width: u32, /// Frame height in pixels. pub height: u32, /// Pixel format of the data. pub format: PixelFormat, /// Timestamp in microseconds since capture start. /// Useful for temporal analysis and frame timing. pub timestamp_us: u64, } /// Supported pixel formats for camera frames. /// /// IR cameras typically output grayscale (Grey) or YUYV formats. /// The face detection pipeline works best with grayscale input. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[allow(dead_code)] // Public API, variants used by camera implementations pub enum PixelFormat { /// 8-bit grayscale (Y8/GREY). /// One byte per pixel, values 0-255 represent brightness. /// Preferred format for IR cameras and face detection. Grey, /// YUYV (packed YUV 4:2:2). /// Two bytes per pixel on average (Y0 U Y1 V for each pixel pair). /// Common format for USB webcams. Yuyv, /// MJPEG compressed. /// Variable-length JPEG frames. Requires decompression before processing. Mjpeg, /// Unknown or unsupported format. /// Frames with this format cannot be processed. Unknown, } /// Mock Camera implementation for non-Linux platforms #[cfg(not(target_os = "linux"))] pub fn enumerate_cameras() -> Result> { Ok(vec![ CameraInfo { device_path: "mock_cam_0".to_string(), name: "Mock IR Camera".to_string(), is_ir: true, resolutions: vec![(640, 480)], }, CameraInfo { device_path: "mock_cam_1".to_string(), name: "Mock WebCam".to_string(), is_ir: false, resolutions: vec![(1280, 720)], }, ]) } #[cfg(not(target_os = "linux"))] pub struct Camera { width: u32, height: u32, frame_count: u64, } #[cfg(not(target_os = "linux"))] impl Camera { pub fn open(device: &str) -> Result { tracing::info!("Opening mock camera: {}", device); Ok(Self { width: 640, height: 480, frame_count: 0, }) } pub fn start(&mut self) -> Result<()> { tracing::info!("Mock camera started"); Ok(()) } pub fn stop(&mut self) { tracing::info!("Mock camera stopped"); } pub fn capture_frame(&mut self) -> Result { self.frame_count += 1; // Generate a synthetic frame (gradient + noise) let size = (self.width * self.height) as usize; let mut data = Vec::with_capacity(size); let offset = (self.frame_count % 255) as u8; for y in 0..self.height { for x in 0..self.width { // Moving gradient pattern let val = ((x + y + offset as u32) % 255) as u8; data.push(val); } } // Emulate capture delay std::thread::sleep(std::time::Duration::from_millis(33)); Ok(Frame { data, width: self.width, height: self.height, format: PixelFormat::Grey, timestamp_us: self.frame_count * 33333, }) } pub fn resolution(&self) -> (u32, u32) { (self.width, self.height) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_camera_info_display() { let info = CameraInfo { device_path: "/dev/video0".to_string(), name: "Test Camera".to_string(), is_ir: false, resolutions: vec![(640, 480)], }; let display = format!("{}", info); assert!(display.contains("Test Camera")); assert!(display.contains("/dev/video0")); assert!(!display.contains("[IR]")); let ir_info = CameraInfo { device_path: "/dev/video2".to_string(), name: "IR Camera".to_string(), is_ir: true, resolutions: vec![(640, 480)], }; let ir_display = format!("{}", ir_info); assert!(ir_display.contains("[IR]")); } #[test] fn test_pixel_format_equality() { assert_eq!(PixelFormat::Grey, PixelFormat::Grey); assert_ne!(PixelFormat::Grey, PixelFormat::Yuyv); } #[test] fn test_frame_structure() { let frame = Frame { data: vec![0; 640 * 480], width: 640, height: 480, format: PixelFormat::Grey, timestamp_us: 1000000, }; assert_eq!(frame.width, 640); assert_eq!(frame.height, 480); assert_eq!(frame.data.len(), 640 * 480); assert_eq!(frame.format, PixelFormat::Grey); } #[cfg(not(target_os = "linux"))] #[test] fn test_mock_camera_enumeration() { let cameras = enumerate_cameras().unwrap(); assert!(!cameras.is_empty()); assert!(cameras.iter().any(|c| c.is_ir)); } #[cfg(not(target_os = "linux"))] #[test] fn test_mock_camera_capture() { let mut camera = Camera::open("mock_cam_0").unwrap(); camera.start().unwrap(); let frame = camera.capture_frame().unwrap(); assert_eq!(frame.width, 640); assert_eq!(frame.height, 480); assert_eq!(frame.format, PixelFormat::Grey); assert_eq!(frame.data.len(), 640 * 480); let frame2 = camera.capture_frame().unwrap(); assert!(frame2.timestamp_us > frame.timestamp_us); camera.stop(); } }