326 lines
9.4 KiB
Rust
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();
|
|
}
|
|
}
|