Skip to main content

cranpose_services/
camera.rs

1//! Live camera capture: an in-app viewfinder frame source.
2//!
3//! Unlike the one-shot [`image_picker`](crate::image_picker) (which presents the
4//! system camera UI), this exposes a running capture session whose latest frame
5//! the app polls to render its own viewfinder and run per-frame detection —
6//! matching the Android/desktop live-preview experience.
7//!
8//! The platform backend installs an implementation via [`set_platform_camera`]
9//! (iOS `AVCaptureSession`, desktop `nokhwa`, …). No default: [`camera`] returns
10//! `None` where live capture is unsupported, so the app can fall back to the
11//! image picker.
12
13use std::sync::Arc;
14use std::sync::Mutex;
15use std::sync::OnceLock;
16
17/// A single captured frame as tightly-packed RGBA8 (`width * height * 4` bytes,
18/// row-major, no padding).
19#[derive(Clone)]
20pub struct CameraFrame {
21    pub width: u32,
22    pub height: u32,
23    pub rgba: Vec<u8>,
24}
25
26#[derive(Debug, thiserror::Error)]
27pub enum CameraError {
28    /// No live camera backend on this platform.
29    #[error("live camera capture is not supported here")]
30    Unsupported,
31    /// The user denied camera access.
32    #[error("camera permission denied")]
33    PermissionDenied,
34    /// Any other failure (no device, configuration error, …).
35    #[error("{0}")]
36    Failed(String),
37}
38
39/// A running (or startable) live camera. Implementations are `Send + Sync` so a
40/// background preview pump can start/stop and poll frames off the UI thread.
41pub trait Camera: Send + Sync {
42    /// Start the capture session, returning a human-readable device name. Safe
43    /// to call again while already running (idempotent).
44    fn start(&self) -> Result<String, CameraError>;
45    /// The most recent frame, or `None` if none has arrived yet.
46    fn latest_frame(&self) -> Option<CameraFrame>;
47    /// Stop the session and release the device.
48    fn stop(&self);
49}
50
51pub type CameraRef = Arc<dyn Camera>;
52
53fn slot() -> &'static Mutex<Option<CameraRef>> {
54    static SLOT: OnceLock<Mutex<Option<CameraRef>>> = OnceLock::new();
55    SLOT.get_or_init(|| Mutex::new(None))
56}
57
58/// Installs the platform live camera, replacing any previous one.
59pub fn set_platform_camera(camera: CameraRef) {
60    if let Ok(mut s) = slot().lock() {
61        *s = Some(camera);
62    }
63}
64
65/// Removes any registered platform camera (tests/teardown).
66pub fn clear_platform_camera() {
67    if let Ok(mut s) = slot().lock() {
68        *s = None;
69    }
70}
71
72/// The registered live camera, or `None` where live capture is unsupported.
73pub fn camera() -> Option<CameraRef> {
74    slot().lock().ok().and_then(|s| s.clone())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn registration_round_trips() {
83        clear_platform_camera();
84        assert!(camera().is_none());
85        struct Fake;
86        impl Camera for Fake {
87            fn start(&self) -> Result<String, CameraError> {
88                Ok("fake".into())
89            }
90            fn latest_frame(&self) -> Option<CameraFrame> {
91                Some(CameraFrame {
92                    width: 1,
93                    height: 1,
94                    rgba: vec![0, 0, 0, 255],
95                })
96            }
97            fn stop(&self) {}
98        }
99        set_platform_camera(Arc::new(Fake));
100        let cam = camera().expect("registered");
101        assert_eq!(cam.start().unwrap(), "fake");
102        assert_eq!(cam.latest_frame().unwrap().width, 1);
103        clear_platform_camera();
104    }
105}