Files
Linux-Hello/linux-hello-daemon/src/camera/mod.rs
2026-01-30 09:44:12 +01:00

326 lines
9.4 KiB
Rust

//! 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<u8>,
/// 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<Vec<CameraInfo>> {
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<Self> {
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<Frame> {
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();
}
}