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 crate::registry::ServiceRegistry;
14use std::sync::Arc;
15
16/// A single captured frame as tightly-packed RGBA8 (`width * height * 4` bytes,
17/// row-major, no padding).
18#[derive(Clone)]
19pub struct CameraFrame {
20    pub width: u32,
21    pub height: u32,
22    pub rgba: Vec<u8>,
23}
24
25/// A full-resolution still photograph as an encoded image.
26///
27/// Unlike [`CameraFrame`] (a viewfinder-resolution stream frame), a still is
28/// captured through the platform's dedicated photo pipeline at full sensor
29/// resolution — on iOS that is `AVCapturePhotoOutput`, roughly 12 MP versus the
30/// 720p-class viewfinder. The bytes are an encoded JPEG whose EXIF orientation
31/// tag reflects the device rotation; decode with an orientation-aware decoder.
32#[derive(Clone)]
33pub struct CameraStill {
34    pub jpeg: Vec<u8>,
35}
36
37/// One capture device the app may pick, as reported by [`Camera::lenses`].
38///
39/// `id` is the platform's own handle for the device (an `AVCaptureDevice`
40/// uniqueID on iOS, a camera2 id on Android); pass it back to
41/// [`Camera::use_lens`]. `name` is for a button label: "Ultra wide", "Wide",
42/// "Tele".
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct CameraLens {
45    pub id: String,
46    pub name: String,
47}
48
49/// What the light does when a still is captured.
50///
51/// `Auto` leaves the choice to the device's exposure metering. A backend with
52/// no flash reports `false` from [`Camera::set_flash`] and the app hides the
53/// control.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum FlashMode {
56    #[default]
57    Off,
58    Auto,
59    On,
60}
61
62#[derive(Debug, thiserror::Error)]
63pub enum CameraError {
64    /// No live camera backend on this platform.
65    #[error("live camera capture is not supported here")]
66    Unsupported,
67    /// The user denied camera access.
68    #[error("camera permission denied")]
69    PermissionDenied,
70    /// Any other failure (no device, configuration error, …).
71    #[error("{0}")]
72    Failed(String),
73}
74
75/// A running (or startable) live camera. Implementations are `Send + Sync` so a
76/// background preview pump can start/stop and poll frames off the UI thread.
77pub trait Camera: Send + Sync {
78    /// Start the capture session, returning a human-readable device name. Safe
79    /// to call again while already running (idempotent).
80    fn start(&self) -> Result<String, CameraError>;
81    /// The most recent frame, or `None` if none has arrived yet.
82    fn latest_frame(&self) -> Option<CameraFrame>;
83    /// Capture a full-resolution still through the platform photo pipeline.
84    ///
85    /// Blocks up to a few seconds while the device exposes and encodes.
86    /// Returns `None` where the backend has no dedicated photo path (callers
87    /// should fall back to [`latest_frame`](Self::latest_frame)) or when the
88    /// capture fails.
89    fn capture_still(&self) -> Option<CameraStill> {
90        None
91    }
92    /// Toggle the capture device's torch (flashlight) while the session runs.
93    /// Scanner-style apps light dim scenes instead of trying to analyze
94    /// photon-starved frames. Returns `false` where the device has no torch
95    /// or the backend has none wired; the torch dies with the session.
96    fn set_torch(&self, _on: bool) -> bool {
97        false
98    }
99    /// The capture devices the app may pick between, back cameras first and in
100    /// field-of-view order (widest first). An empty list means the app shows no
101    /// lens control: either the platform has one camera or the backend does not
102    /// list them.
103    fn lenses(&self) -> Vec<CameraLens> {
104        Vec::new()
105    }
106
107    /// The id of the device the session uses, or `None` when nothing is open
108    /// and the backend has no stored choice.
109    fn lens(&self) -> Option<String> {
110        None
111    }
112
113    /// Open `id` instead of the current device, keeping the session running.
114    /// Returns `false` when the id is unknown or the backend cannot switch.
115    fn use_lens(&self, _id: &str) -> bool {
116        false
117    }
118
119    /// Whether the current device has a flash for stills.
120    fn has_flash(&self) -> bool {
121        false
122    }
123
124    /// What the flash does on the next [`capture_still`](Self::capture_still).
125    /// Returns `false` where the device has no flash or the backend has none
126    /// wired; the mode dies with the session.
127    fn set_flash(&self, _mode: FlashMode) -> bool {
128        false
129    }
130
131    /// Stop the session and release the device.
132    fn stop(&self);
133}
134
135pub type CameraRef = Arc<dyn Camera>;
136
137static PLATFORM_CAMERA: ServiceRegistry<dyn Camera> = ServiceRegistry::new();
138
139/// Installs the platform live camera, replacing any previous one.
140pub fn set_platform_camera(camera: CameraRef) {
141    PLATFORM_CAMERA.set(camera);
142}
143
144/// Removes any registered platform camera (tests/teardown).
145pub fn clear_platform_camera() {
146    PLATFORM_CAMERA.clear();
147}
148
149/// The registered live camera, or `None` where live capture is unsupported.
150pub fn camera() -> Option<CameraRef> {
151    PLATFORM_CAMERA.get()
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn registration_round_trips() {
160        let _guard = crate::registry::test_service_guard();
161        clear_platform_camera();
162        assert!(camera().is_none());
163        struct Fake;
164        impl Camera for Fake {
165            fn start(&self) -> Result<String, CameraError> {
166                Ok("fake".into())
167            }
168            fn latest_frame(&self) -> Option<CameraFrame> {
169                Some(CameraFrame {
170                    width: 1,
171                    height: 1,
172                    rgba: vec![0, 0, 0, 255],
173                })
174            }
175            fn stop(&self) {}
176        }
177        set_platform_camera(Arc::new(Fake));
178        let cam = camera().expect("registered");
179        assert_eq!(cam.start().unwrap(), "fake");
180        assert_eq!(cam.latest_frame().unwrap().width, 1);
181        clear_platform_camera();
182    }
183
184    #[test]
185    fn a_backend_that_lists_no_lens_and_no_flash_says_so() {
186        let _guard = crate::registry::test_service_guard();
187        clear_platform_camera();
188        struct Bare;
189        impl Camera for Bare {
190            fn start(&self) -> Result<String, CameraError> {
191                Ok("bare".into())
192            }
193            fn latest_frame(&self) -> Option<CameraFrame> {
194                None
195            }
196            fn stop(&self) {}
197        }
198        set_platform_camera(Arc::new(Bare));
199        let cam = camera().expect("registered");
200        assert!(cam.lenses().is_empty());
201        assert_eq!(cam.lens(), None);
202        assert!(!cam.use_lens("0"));
203        assert!(!cam.has_flash());
204        assert!(!cam.set_flash(FlashMode::On));
205        clear_platform_camera();
206    }
207
208    #[test]
209    fn a_backend_that_lists_two_lenses_hands_them_over_in_order() {
210        let _guard = crate::registry::test_service_guard();
211        clear_platform_camera();
212        struct Two;
213        impl Camera for Two {
214            fn start(&self) -> Result<String, CameraError> {
215                Ok("two".into())
216            }
217            fn latest_frame(&self) -> Option<CameraFrame> {
218                None
219            }
220            fn lenses(&self) -> Vec<CameraLens> {
221                vec![
222                    CameraLens {
223                        id: "u".into(),
224                        name: "Ultra wide".into(),
225                    },
226                    CameraLens {
227                        id: "w".into(),
228                        name: "Wide".into(),
229                    },
230                ]
231            }
232            fn lens(&self) -> Option<String> {
233                Some("w".into())
234            }
235            fn use_lens(&self, id: &str) -> bool {
236                id == "u" || id == "w"
237            }
238            fn stop(&self) {}
239        }
240        set_platform_camera(Arc::new(Two));
241        let cam = camera().expect("registered");
242        let lenses = cam.lenses();
243        assert_eq!(lenses.len(), 2);
244        assert_eq!(lenses[0].name, "Ultra wide");
245        assert_eq!(cam.lens().as_deref(), Some("w"));
246        assert!(cam.use_lens("u"));
247        assert!(!cam.use_lens("tele"));
248        clear_platform_camera();
249    }
250}