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/// A full-resolution still photograph as an encoded image.
27///
28/// Unlike [`CameraFrame`] (a viewfinder-resolution stream frame), a still is
29/// captured through the platform's dedicated photo pipeline at full sensor
30/// resolution — on iOS that is `AVCapturePhotoOutput`, roughly 12 MP versus the
31/// 720p-class viewfinder. The bytes are an encoded JPEG whose EXIF orientation
32/// tag reflects the device rotation; decode with an orientation-aware decoder.
33#[derive(Clone)]
34pub struct CameraStill {
35    pub jpeg: Vec<u8>,
36}
37
38#[derive(Debug, thiserror::Error)]
39pub enum CameraError {
40    /// No live camera backend on this platform.
41    #[error("live camera capture is not supported here")]
42    Unsupported,
43    /// The user denied camera access.
44    #[error("camera permission denied")]
45    PermissionDenied,
46    /// Any other failure (no device, configuration error, …).
47    #[error("{0}")]
48    Failed(String),
49}
50
51/// A running (or startable) live camera. Implementations are `Send + Sync` so a
52/// background preview pump can start/stop and poll frames off the UI thread.
53pub trait Camera: Send + Sync {
54    /// Start the capture session, returning a human-readable device name. Safe
55    /// to call again while already running (idempotent).
56    fn start(&self) -> Result<String, CameraError>;
57    /// The most recent frame, or `None` if none has arrived yet.
58    fn latest_frame(&self) -> Option<CameraFrame>;
59    /// Capture a full-resolution still through the platform photo pipeline.
60    ///
61    /// Blocks up to a few seconds while the device exposes and encodes.
62    /// Returns `None` where the backend has no dedicated photo path (callers
63    /// should fall back to [`latest_frame`](Self::latest_frame)) or when the
64    /// capture fails.
65    fn capture_still(&self) -> Option<CameraStill> {
66        None
67    }
68    /// Toggle the capture device's torch (flashlight) while the session runs.
69    /// Scanner-style apps light dim scenes instead of trying to analyze
70    /// photon-starved frames. Returns `false` where the device has no torch
71    /// or the backend has none wired; the torch dies with the session.
72    fn set_torch(&self, _on: bool) -> bool {
73        false
74    }
75    /// Stop the session and release the device.
76    fn stop(&self);
77}
78
79pub type CameraRef = Arc<dyn Camera>;
80
81fn slot() -> &'static Mutex<Option<CameraRef>> {
82    static SLOT: OnceLock<Mutex<Option<CameraRef>>> = OnceLock::new();
83    SLOT.get_or_init(|| Mutex::new(None))
84}
85
86/// Installs the platform live camera, replacing any previous one.
87pub fn set_platform_camera(camera: CameraRef) {
88    if let Ok(mut s) = slot().lock() {
89        *s = Some(camera);
90    }
91}
92
93/// Removes any registered platform camera (tests/teardown).
94pub fn clear_platform_camera() {
95    if let Ok(mut s) = slot().lock() {
96        *s = None;
97    }
98}
99
100/// The registered live camera, or `None` where live capture is unsupported.
101pub fn camera() -> Option<CameraRef> {
102    slot().lock().ok().and_then(|s| s.clone())
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn registration_round_trips() {
111        clear_platform_camera();
112        assert!(camera().is_none());
113        struct Fake;
114        impl Camera for Fake {
115            fn start(&self) -> Result<String, CameraError> {
116                Ok("fake".into())
117            }
118            fn latest_frame(&self) -> Option<CameraFrame> {
119                Some(CameraFrame {
120                    width: 1,
121                    height: 1,
122                    rgba: vec![0, 0, 0, 255],
123                })
124            }
125            fn stop(&self) {}
126        }
127        set_platform_camera(Arc::new(Fake));
128        let cam = camera().expect("registered");
129        assert_eq!(cam.start().unwrap(), "fake");
130        assert_eq!(cam.latest_frame().unwrap().width, 1);
131        clear_platform_camera();
132    }
133}