Skip to main content

cranpose_services/
camera.rs

1//! The live camera: what it is doing, the frames it is producing, and the
2//! stills it takes.
3//!
4//! Three things about a camera are easy to get wrong and expensive to get wrong
5//! twice, so the framework owns all three.
6//!
7//! A camera is **observable, not polled**. A viewfinder that asks "is there a
8//! new frame yet?" every frame does that work whether or not one arrived, and
9//! learns about a failure only by noticing that frames stopped. Here the
10//! session publishes what it is doing and the frames it produces, and a screen
11//! reacts.
12//!
13//! Frames arrive in the **format the sensor produced**. Encoding a preview
14//! frame as JPEG to hand it across a language boundary and decoding it back
15//! costs several milliseconds per frame, every frame, to arrive at the pixels
16//! the camera already had. [`FrameFormat::Nv12`] is what a phone camera
17//! actually produces, and it converts to RGBA in one pass over the bytes.
18//!
19//! Analysis is **bounded and latest-wins**. A detector that takes longer than a
20//! frame interval must fall behind rather than accumulate: a queue of stale
21//! frames costs memory to hold and produces answers about a scene that has
22//! already moved.
23
24use std::sync::{
25    Arc, Mutex, OnceLock,
26    atomic::{AtomicU64, Ordering},
27};
28
29use cranpose_core::{EventStream, State, rememberEventStream};
30
31use crate::registry::ServiceRegistry;
32
33/// How a frame's pixels are laid out.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum FrameFormat {
36    /// Tightly packed RGBA8, row-major, no padding.
37    #[default]
38    Rgba8,
39    /// Tightly packed RGB8, row-major, no padding.
40    ///
41    /// What a USB webcam decodes to. Carried as-is so a desktop viewfinder does
42    /// not pay a widening copy per frame for an alpha channel a camera never
43    /// has.
44    Rgb8,
45    /// Full-range NV12: a `width * height` luma plane followed by an
46    /// interleaved `width * height / 2` chroma plane at half resolution in both
47    /// directions.
48    ///
49    /// What a phone camera produces. Carrying it as-is is what removes the
50    /// encode-and-decode round trip a JPEG preview pays on every frame.
51    Nv12,
52}
53
54impl FrameFormat {
55    /// How many bytes a frame of this size occupies, or `None` when the size
56    /// cannot be represented — which is a frame nobody can allocate anyway.
57    pub fn byte_len(self, width: u32, height: u32) -> Option<usize> {
58        let pixels = (width as usize).checked_mul(height as usize)?;
59        match self {
60            FrameFormat::Rgba8 => pixels.checked_mul(4),
61            FrameFormat::Rgb8 => pixels.checked_mul(3),
62            FrameFormat::Nv12 => {
63                if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
64                    return None;
65                }
66                pixels.checked_add(pixels / 2)
67            }
68        }
69    }
70}
71
72/// One frame from the viewfinder.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct CameraFrame {
75    pub width: u32,
76    pub height: u32,
77    pub format: FrameFormat,
78    /// How far the frame must be rotated clockwise to be the right way up,
79    /// which is the sensor's mounting plus the device's rotation.
80    pub rotation_degrees: u16,
81    /// Which frame this is in the session, so a consumer can tell a repeat from
82    /// a new one and count what it missed.
83    pub sequence: u64,
84    /// The pixels, in [`format`](Self::format).
85    pub bytes: Vec<u8>,
86}
87
88impl CameraFrame {
89    /// A frame, or `None` when the bytes do not match the size and format —
90    /// which is a backend bug, and one that reads as corrupted video rather
91    /// than as an error if it is let through.
92    pub fn new(
93        width: u32,
94        height: u32,
95        format: FrameFormat,
96        rotation_degrees: u16,
97        sequence: u64,
98        bytes: Vec<u8>,
99    ) -> Option<Self> {
100        if format.byte_len(width, height)? != bytes.len() {
101            return None;
102        }
103        Some(Self {
104            width,
105            height,
106            format,
107            rotation_degrees: rotation_degrees % 360,
108            sequence,
109            bytes,
110        })
111    }
112
113    /// The frame as tightly packed RGBA8, converting only when it has to.
114    ///
115    /// One pass over the bytes with no allocation beyond the result, because
116    /// this runs per frame: a conversion that allocates per row shows up as
117    /// dropped frames rather than as a slow function.
118    pub fn to_rgba8(&self) -> Vec<u8> {
119        match self.format {
120            FrameFormat::Rgba8 => self.bytes.clone(),
121            FrameFormat::Rgb8 => rgb8_to_rgba8(&self.bytes),
122            FrameFormat::Nv12 => nv12_to_rgba8(self.width, self.height, &self.bytes),
123        }
124    }
125
126    /// The frame as tightly packed RGBA8 with
127    /// [`rotation_degrees`](Self::rotation_degrees) applied, so the pixels are
128    /// the right way up whatever the sensor's mounting was.
129    ///
130    /// The turn happens in the same pass as the format conversion, because this
131    /// runs on every previewed frame: converting and then turning would walk
132    /// the pixels twice. A rotation that is not a quarter turn is left alone —
133    /// no camera produces one.
134    pub fn upright_rgba8(&self) -> UprightRgba {
135        let rotation = self.rotation_degrees;
136        if !matches!(rotation, 90 | 180 | 270) {
137            return UprightRgba {
138                width: self.width,
139                height: self.height,
140                rgba: self.to_rgba8(),
141            };
142        }
143        let (width, height) = (self.width as usize, self.height as usize);
144        let (out_width, out_height) = match rotation {
145            90 | 270 => (self.height, self.width),
146            _ => (self.width, self.height),
147        };
148        let mut rgba = vec![0u8; width * height * 4];
149        match self.format {
150            FrameFormat::Rgba8 => {
151                for y in 0..height {
152                    let row = &self.bytes[y * width * 4..(y + 1) * width * 4];
153                    for x in 0..width {
154                        let src = &row[x * 4..x * 4 + 4];
155                        let dst = turned_index(rotation, width, height, x, y) * 4;
156                        rgba[dst..dst + 4].copy_from_slice(src);
157                    }
158                }
159            }
160            FrameFormat::Rgb8 => {
161                for y in 0..height {
162                    let row = &self.bytes[y * width * 3..(y + 1) * width * 3];
163                    for x in 0..width {
164                        let src = &row[x * 3..x * 3 + 3];
165                        let dst = turned_index(rotation, width, height, x, y) * 4;
166                        rgba[dst..dst + 3].copy_from_slice(src);
167                        rgba[dst + 3] = 255;
168                    }
169                }
170            }
171            FrameFormat::Nv12 => {
172                let pixels = width * height;
173                if self.bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
174                    return UprightRgba {
175                        width: out_width,
176                        height: out_height,
177                        rgba,
178                    };
179                }
180                let (luma, chroma) = self.bytes.split_at(pixels);
181                for y in 0..height {
182                    let luma_row = &luma[y * width..(y + 1) * width];
183                    let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
184                    for x in 0..width {
185                        let luminance = luma_row[x] as i32;
186                        let blue_difference = chroma_row[x & !1] as i32 - 128;
187                        let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
188                        let dst = turned_index(rotation, width, height, x, y) * 4;
189                        rgba[dst] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
190                        rgba[dst + 1] = clamp_byte(
191                            luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16),
192                        );
193                        rgba[dst + 2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
194                        rgba[dst + 3] = 255;
195                    }
196                }
197            }
198        }
199        UprightRgba {
200            width: out_width,
201            height: out_height,
202            rgba,
203        }
204    }
205}
206
207/// A frame's pixels as tightly packed RGBA8, already the right way up.
208///
209/// `width` and `height` describe the turned image, so a 90° or 270° turn swaps
210/// them relative to the frame that produced this.
211#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct UprightRgba {
213    pub width: u32,
214    pub height: u32,
215    pub rgba: Vec<u8>,
216}
217
218#[inline]
219fn turned_index(rotation: u16, width: usize, height: usize, x: usize, y: usize) -> usize {
220    match rotation {
221        90 => x * height + (height - 1 - y),
222        180 => (height - 1 - y) * width + (width - 1 - x),
223        270 => (width - 1 - x) * height + y,
224        _ => y * width + x,
225    }
226}
227
228fn rgb8_to_rgba8(bytes: &[u8]) -> Vec<u8> {
229    let mut rgba = Vec::with_capacity(bytes.len() / 3 * 4);
230    for [red, green, blue] in bytes.as_chunks::<3>().0 {
231        rgba.extend_from_slice(&[*red, *green, *blue, 255]);
232    }
233    rgba
234}
235
236fn nv12_to_rgba8(width: u32, height: u32, bytes: &[u8]) -> Vec<u8> {
237    let (width, height) = (width as usize, height as usize);
238    let pixels = width * height;
239    let mut rgba = vec![0u8; pixels * 4];
240    if bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
241        return rgba;
242    }
243    let (luma, chroma) = bytes.split_at(pixels);
244
245    for y in 0..height {
246        let luma_row = &luma[y * width..(y + 1) * width];
247        let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
248        let out_row = &mut rgba[y * width * 4..(y + 1) * width * 4];
249        for x in 0..width {
250            let luminance = luma_row[x] as i32;
251            let blue_difference = chroma_row[x & !1] as i32 - 128;
252            let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
253            let out = &mut out_row[x * 4..x * 4 + 4];
254            out[0] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
255            out[1] =
256                clamp_byte(luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16));
257            out[2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
258            out[3] = 255;
259        }
260    }
261    rgba
262}
263
264fn clamp_byte(value: i32) -> u8 {
265    value.clamp(0, 255) as u8
266}
267
268/// A full-resolution still photograph as an encoded image.
269///
270/// Unlike a viewfinder frame, a still comes through the platform's dedicated
271/// photo pipeline at full sensor resolution. The bytes are an encoded JPEG
272/// whose EXIF orientation tag reflects the device rotation; decode with an
273/// orientation-aware decoder.
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub struct CameraStill {
276    pub jpeg: Vec<u8>,
277}
278
279/// One capture device the application may pick.
280///
281/// `id` is the platform's own handle for the device (an `AVCaptureDevice`
282/// uniqueID on iOS, a camera2 id on Android, a device index on desktop); pass
283/// it back to [`Camera::use_lens`]. `name` is for a button label: "Ultra
284/// wide", "Wide", "Tele".
285#[derive(Clone, Debug, PartialEq, Eq)]
286pub struct CameraLens {
287    pub id: String,
288    pub name: String,
289    /// Which way the device points, so an application can offer back lenses
290    /// and the front lens as different controls.
291    pub facing: LensFacing,
292}
293
294/// Which way a capture device points.
295#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
296pub enum LensFacing {
297    /// Away from the screen: the main photography cameras on a phone.
298    #[default]
299    Back,
300    /// At the person holding the device.
301    Front,
302    /// Not fixed to a screen at all: a webcam or another attached device.
303    External,
304}
305
306/// The devices the application may pick between, and the one in use.
307///
308/// Published by the backend when a session opens and when the device changes.
309/// A screen showing a lens control observes this instead of asking the
310/// platform, because both phone lens lists are blocking platform calls — a
311/// JNI round trip on Android, a fresh discovery session on iOS — and a
312/// recomposition must not pay that.
313#[derive(Clone, Debug, Default, PartialEq, Eq)]
314pub struct CameraLenses {
315    /// Back lenses first in field-of-view order, widest first, then the rest.
316    pub lenses: Vec<CameraLens>,
317    /// The id of the device the open session uses, or `None` while nothing
318    /// runs.
319    pub active: Option<String>,
320}
321
322/// What the light does when a still is captured.
323///
324/// `Auto` leaves the choice to the device's exposure metering. A backend with
325/// no flash reports `false` from [`Camera::set_flash`] and the application
326/// hides the control.
327#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
328pub enum FlashMode {
329    #[default]
330    Off,
331    Auto,
332    On,
333}
334
335#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
336pub enum CameraError {
337    /// No live camera backend on this platform.
338    #[error("live camera capture is not supported here")]
339    Unsupported,
340    /// The user denied camera access.
341    #[error("camera permission denied")]
342    PermissionDenied,
343    /// A still was asked for while nothing was running.
344    #[error("the camera is not running")]
345    NotRunning,
346    /// Any other failure — no device, a configuration the hardware refused.
347    #[error("{0}")]
348    Failed(String),
349}
350
351/// What the camera session is doing.
352#[derive(Clone, Debug, Default, PartialEq, Eq)]
353pub enum CameraState {
354    /// Nothing has been asked of it.
355    #[default]
356    Idle,
357    /// A session is opening. Phones take a noticeable moment over this, which
358    /// is why it is a state a screen can show rather than a gap before frames.
359    Starting,
360    /// Frames are being produced.
361    Running {
362        /// The device the session opened, for a label.
363        device: String,
364    },
365    /// The session was stopped and the device released.
366    Stopped,
367    /// The session could not start, or ended on its own.
368    Failed(CameraError),
369}
370
371impl CameraState {
372    /// Whether frames are being produced now.
373    pub fn is_running(&self) -> bool {
374        matches!(self, CameraState::Running { .. })
375    }
376
377    /// Whether the session is opening or open — which is when a screen keeps
378    /// the viewfinder on screen rather than showing a placeholder.
379    pub fn is_active(&self) -> bool {
380        matches!(self, CameraState::Starting | CameraState::Running { .. })
381    }
382
383    /// The failure, if the session ended in one.
384    pub fn failure(&self) -> Option<&CameraError> {
385        match self {
386            CameraState::Failed(error) => Some(error),
387            _ => None,
388        }
389    }
390}
391
392/// A platform capture session.
393///
394/// A backend starts and stops the device and publishes what it produces through
395/// [`publish_camera_frame`] and [`publish_camera_state`]; nothing here is
396/// polled, and no method blocks for the length of a capture.
397pub trait Camera: Send + Sync {
398    /// Opens the session.
399    ///
400    /// Returns as soon as the request is accepted. The session's progress
401    /// arrives as [`CameraState`], because opening a camera takes long enough
402    /// on a phone that a screen has to show something in the meantime.
403    fn start(&self) -> Result<(), CameraError>;
404
405    /// Stops the session and releases the device.
406    fn stop(&self);
407
408    /// Asks for a full-resolution still.
409    ///
410    /// The picture arrives through [`publish_camera_still`], because the device
411    /// takes as long as it takes to expose and encode, and a call that blocked
412    /// for it would block whatever asked.
413    fn request_still(&self) -> Result<(), CameraError> {
414        Err(CameraError::Unsupported)
415    }
416
417    /// Turns the torch on or off while the session runs.
418    ///
419    /// Scanner-style applications light a dim scene rather than analysing
420    /// photon-starved frames. Returns `false` where the device has no torch;
421    /// the torch dies with the session.
422    fn set_torch(&self, _on: bool) -> bool {
423        false
424    }
425
426    /// The devices the application may pick between, back cameras first and in
427    /// field-of-view order, widest first, then the rest.
428    ///
429    /// An empty list means the application shows no lens control: either the
430    /// platform has one camera or the backend does not list them. Backends
431    /// also publish this through [`publish_camera_lenses`] when a session
432    /// opens, so a screen observes [`rememberCameraLenses`] rather than paying
433    /// this blocking platform call per recomposition.
434    fn lenses(&self) -> Vec<CameraLens> {
435        Vec::new()
436    }
437
438    /// The device the session is using, or `None` when nothing is open and the
439    /// backend has no stored choice.
440    fn lens(&self) -> Option<String> {
441        None
442    }
443
444    /// Opens `id` instead of the current device, keeping the session running.
445    /// Returns `false` when the id is unknown or the backend cannot switch.
446    fn use_lens(&self, _id: &str) -> bool {
447        false
448    }
449
450    /// Whether the current device has a flash for stills.
451    fn has_flash(&self) -> bool {
452        false
453    }
454
455    /// What the flash does on the next still. Returns `false` where the device
456    /// has no flash; the mode dies with the session.
457    fn set_flash(&self, _mode: FlashMode) -> bool {
458        false
459    }
460}
461
462/// Shared handle to the platform camera.
463pub type CameraRef = Arc<dyn Camera>;
464
465static PLATFORM_CAMERA: ServiceRegistry<dyn Camera> = ServiceRegistry::new();
466
467/// Installs the platform camera, replacing any previous one.
468pub fn set_platform_camera(camera: CameraRef) {
469    PLATFORM_CAMERA.set(camera);
470}
471
472/// Removes the platform camera and forgets everything the last session
473/// published.
474pub fn clear_platform_camera() {
475    PLATFORM_CAMERA.clear();
476    if let Ok(mut observers) = frame_observers().lock() {
477        observers.clear();
478    }
479    if let Ok(mut observers) = state_observers().lock() {
480        observers.clear();
481    }
482    if let Ok(mut observers) = still_observers().lock() {
483        observers.clear();
484    }
485    if let Ok(mut observers) = lens_observers().lock() {
486        observers.clear();
487    }
488    if let Ok(mut latest) = latest_frame_slot().lock() {
489        *latest = None;
490    }
491    if let Ok(mut state) = state_slot().lock() {
492        *state = CameraState::Idle;
493    }
494    if let Ok(mut lenses) = lenses_slot().lock() {
495        *lenses = CameraLenses::default();
496    }
497    DROPPED_FRAMES.store(0, Ordering::Release);
498}
499
500/// The installed camera, or `None` where live capture is unsupported — which is
501/// when an application falls back to the image picker.
502pub fn camera() -> Option<CameraRef> {
503    PLATFORM_CAMERA.get()
504}
505
506/// Whether this platform has a live camera at all.
507pub fn camera_supported() -> bool {
508    PLATFORM_CAMERA.get().is_some()
509}
510
511fn state_slot() -> &'static Mutex<CameraState> {
512    static SLOT: OnceLock<Mutex<CameraState>> = OnceLock::new();
513    SLOT.get_or_init(|| Mutex::new(CameraState::Idle))
514}
515
516fn latest_frame_slot() -> &'static Mutex<Option<CameraFrame>> {
517    static SLOT: OnceLock<Mutex<Option<CameraFrame>>> = OnceLock::new();
518    SLOT.get_or_init(|| Mutex::new(None))
519}
520
521fn lenses_slot() -> &'static Mutex<CameraLenses> {
522    static SLOT: OnceLock<Mutex<CameraLenses>> = OnceLock::new();
523    SLOT.get_or_init(|| Mutex::new(CameraLenses::default()))
524}
525
526static DROPPED_FRAMES: AtomicU64 = AtomicU64::new(0);
527
528type FrameObserver = Arc<dyn Fn(CameraFrame) + Send + Sync>;
529type StateObserver = Arc<dyn Fn(CameraState) + Send + Sync>;
530type StillObserver = Arc<dyn Fn(Result<CameraStill, CameraError>) + Send + Sync>;
531type LensObserver = Arc<dyn Fn(CameraLenses) + Send + Sync>;
532
533fn frame_observers() -> &'static Mutex<Vec<(u64, FrameObserver)>> {
534    static SLOT: OnceLock<Mutex<Vec<(u64, FrameObserver)>>> = OnceLock::new();
535    SLOT.get_or_init(|| Mutex::new(Vec::new()))
536}
537
538fn state_observers() -> &'static Mutex<Vec<(u64, StateObserver)>> {
539    static SLOT: OnceLock<Mutex<Vec<(u64, StateObserver)>>> = OnceLock::new();
540    SLOT.get_or_init(|| Mutex::new(Vec::new()))
541}
542
543fn still_observers() -> &'static Mutex<Vec<(u64, StillObserver)>> {
544    static SLOT: OnceLock<Mutex<Vec<(u64, StillObserver)>>> = OnceLock::new();
545    SLOT.get_or_init(|| Mutex::new(Vec::new()))
546}
547
548fn lens_observers() -> &'static Mutex<Vec<(u64, LensObserver)>> {
549    static SLOT: OnceLock<Mutex<Vec<(u64, LensObserver)>>> = OnceLock::new();
550    SLOT.get_or_init(|| Mutex::new(Vec::new()))
551}
552
553static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
554
555/// Keeps a camera observer registered until it is dropped.
556pub struct CameraObserver {
557    id: u64,
558    kind: ObserverKind,
559}
560
561#[derive(Clone, Copy)]
562enum ObserverKind {
563    Frame,
564    State,
565    Still,
566    Lenses,
567}
568
569impl Drop for CameraObserver {
570    fn drop(&mut self) {
571        match self.kind {
572            ObserverKind::Frame => retain_without(frame_observers(), self.id),
573            ObserverKind::State => retain_without(state_observers(), self.id),
574            ObserverKind::Still => retain_without(still_observers(), self.id),
575            ObserverKind::Lenses => retain_without(lens_observers(), self.id),
576        }
577    }
578}
579
580fn retain_without<T>(slot: &'static Mutex<Vec<(u64, T)>>, id: u64) {
581    if let Ok(mut observers) = slot.lock() {
582        observers.retain(|(observer, _)| *observer != id);
583    }
584}
585
586fn snapshot<T: Clone>(slot: &'static Mutex<Vec<(u64, T)>>) -> Vec<T> {
587    slot.lock()
588        .map(|observers| {
589            observers
590                .iter()
591                .map(|(_, observer)| observer.clone())
592                .collect()
593        })
594        .unwrap_or_default()
595}
596
597/// The last frame the session produced, or `None` before the first one.
598///
599/// Read outside composition — during draw — so a viewfinder shows the newest
600/// frame without a recomposition per frame.
601pub fn latest_camera_frame() -> Option<CameraFrame> {
602    latest_frame_slot()
603        .lock()
604        .map(|frame| frame.clone())
605        .unwrap_or(None)
606}
607
608/// What the session is doing.
609pub fn camera_state() -> CameraState {
610    state_slot()
611        .lock()
612        .map(|state| state.clone())
613        .unwrap_or_default()
614}
615
616/// How many frames were produced while every observer was still busy.
617pub fn dropped_camera_frames() -> u64 {
618    DROPPED_FRAMES.load(Ordering::Acquire)
619}
620
621/// Publishes a frame. Backends call this from whichever thread the platform
622/// delivers frames on.
623///
624/// The newest frame always replaces the stored one, so a viewfinder never draws
625/// a stale frame; observers that are keeping up see every frame, and one that
626/// is not is counted in [`dropped_camera_frames`] rather than queued behind.
627pub fn publish_camera_frame(frame: CameraFrame) {
628    if let Ok(mut latest) = latest_frame_slot().lock() {
629        *latest = Some(frame.clone());
630    }
631    let observers = snapshot(frame_observers());
632    if observers.is_empty() {
633        return;
634    }
635    for observer in observers {
636        observer(frame.clone());
637    }
638}
639
640/// Records that the platform produced a frame nobody could take.
641///
642/// A backend running a bounded analysis queue calls this when it drops one, so
643/// the count reflects what the device produced rather than what got through.
644pub fn record_dropped_camera_frame() {
645    DROPPED_FRAMES.fetch_add(1, Ordering::AcqRel);
646}
647
648/// Publishes what the session is doing.
649pub fn publish_camera_state(state: CameraState) {
650    {
651        let Ok(mut current) = state_slot().lock() else {
652            return;
653        };
654        if *current == state {
655            return;
656        }
657        *current = state.clone();
658    }
659    if matches!(state, CameraState::Idle | CameraState::Starting) {
660        DROPPED_FRAMES.store(0, Ordering::Release);
661        if let Ok(mut latest) = latest_frame_slot().lock() {
662            *latest = None;
663        }
664    }
665    for observer in snapshot(state_observers()) {
666        observer(state.clone());
667    }
668}
669
670/// Publishes the answer to a still request.
671pub fn publish_camera_still(still: Result<CameraStill, CameraError>) {
672    for observer in snapshot(still_observers()) {
673        observer(still.clone());
674    }
675}
676
677/// Publishes the lens list and the device in use. Backends call this when a
678/// session opens and when the device changes.
679pub fn publish_camera_lenses(lenses: CameraLenses) {
680    {
681        let Ok(mut current) = lenses_slot().lock() else {
682            return;
683        };
684        if *current == lenses {
685            return;
686        }
687        *current = lenses.clone();
688    }
689    for observer in snapshot(lens_observers()) {
690        observer(lenses.clone());
691    }
692}
693
694/// The devices the application may pick between, as the backend last published
695/// them.
696pub fn camera_lenses() -> CameraLenses {
697    lenses_slot()
698        .lock()
699        .map(|lenses| lenses.clone())
700        .unwrap_or_default()
701}
702
703/// Registers `observer` for frames. Applications collect
704/// [`rememberCameraFrames`] instead of calling this.
705pub fn observe_camera_frames(
706    observer: impl Fn(CameraFrame) + Send + Sync + 'static,
707) -> CameraObserver {
708    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
709    if let Ok(mut observers) = frame_observers().lock() {
710        observers.push((id, Arc::new(observer)));
711    }
712    CameraObserver {
713        id,
714        kind: ObserverKind::Frame,
715    }
716}
717
718/// Registers `observer` for session state. The current state is delivered at
719/// once, so a screen composed mid-session shows what is happening rather than
720/// waiting for the next change.
721pub fn observe_camera_state(
722    observer: impl Fn(CameraState) + Send + Sync + 'static,
723) -> CameraObserver {
724    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
725    let observer: StateObserver = Arc::new(observer);
726    if let Ok(mut observers) = state_observers().lock() {
727        observers.push((id, Arc::clone(&observer)));
728    }
729    observer(camera_state());
730    CameraObserver {
731        id,
732        kind: ObserverKind::State,
733    }
734}
735
736/// Registers `observer` for stills.
737pub fn observe_camera_stills(
738    observer: impl Fn(Result<CameraStill, CameraError>) + Send + Sync + 'static,
739) -> CameraObserver {
740    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
741    if let Ok(mut observers) = still_observers().lock() {
742        observers.push((id, Arc::new(observer)));
743    }
744    CameraObserver {
745        id,
746        kind: ObserverKind::Still,
747    }
748}
749
750/// Registers `observer` for the lens list. The current list is delivered at
751/// once, so a screen composed mid-session shows the devices rather than
752/// waiting for the next change.
753pub fn observe_camera_lenses(
754    observer: impl Fn(CameraLenses) + Send + Sync + 'static,
755) -> CameraObserver {
756    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
757    let observer: LensObserver = Arc::new(observer);
758    if let Ok(mut observers) = lens_observers().lock() {
759        observers.push((id, Arc::clone(&observer)));
760    }
761    observer(camera_lenses());
762    CameraObserver {
763        id,
764        kind: ObserverKind::Lenses,
765    }
766}
767
768/// What the camera session is doing, observed for as long as this call stays in
769/// the composition.
770#[allow(non_snake_case)]
771#[track_caller]
772pub fn rememberCameraState() -> State<CameraState> {
773    let updates = rememberEventStream((), |sender| {
774        observe_camera_state(move |state| sender.send(state))
775    });
776    cranpose_core::collectAsState(updates, (), camera_state())
777}
778
779/// The frames the session produces, as a stream this composition collects.
780///
781/// This is the analysis path: a detector collects it, and one that cannot keep
782/// up falls behind by frames rather than by memory — see
783/// [`dropped_camera_frames`]. A viewfinder draws [`latest_camera_frame`]
784/// instead, which costs no recomposition at all.
785#[allow(non_snake_case)]
786#[track_caller]
787pub fn rememberCameraFrames() -> EventStream<CameraFrame> {
788    rememberEventStream((), |sender| {
789        observe_camera_frames(move |frame| sender.send(frame))
790    })
791}
792
793/// The stills the session produces, as a stream this composition collects.
794#[allow(non_snake_case)]
795#[track_caller]
796pub fn rememberCameraStills() -> EventStream<Result<CameraStill, CameraError>> {
797    rememberEventStream((), |sender| {
798        observe_camera_stills(move |still| sender.send(still))
799    })
800}
801
802/// The lens list and the device in use, observed for as long as this call
803/// stays in the composition.
804#[allow(non_snake_case)]
805#[track_caller]
806pub fn rememberCameraLenses() -> State<CameraLenses> {
807    let updates = rememberEventStream((), |sender| {
808        observe_camera_lenses(move |lenses| sender.send(lenses))
809    });
810    cranpose_core::collectAsState(updates, (), camera_lenses())
811}
812
813/// Starts the camera, publishing [`CameraState::Starting`] before the backend
814/// is asked so a screen shows the wait rather than a gap.
815pub fn start_camera() -> Result<(), CameraError> {
816    let Some(camera) = camera() else {
817        publish_camera_state(CameraState::Failed(CameraError::Unsupported));
818        return Err(CameraError::Unsupported);
819    };
820    publish_camera_state(CameraState::Starting);
821    camera.start().inspect_err(|error| {
822        publish_camera_state(CameraState::Failed(error.clone()));
823    })
824}
825
826/// Stops the camera and releases the device.
827pub fn stop_camera() {
828    if let Some(camera) = camera() {
829        camera.stop();
830    }
831    publish_camera_state(CameraState::Stopped);
832}
833
834/// Asks for a full-resolution still, which arrives through
835/// [`rememberCameraStills`].
836pub fn request_camera_still() -> Result<(), CameraError> {
837    let Some(camera) = camera() else {
838        return Err(CameraError::Unsupported);
839    };
840    if !camera_state().is_running() {
841        return Err(CameraError::NotRunning);
842    }
843    camera.request_still()
844}
845
846/// Takes one full-resolution still and resolves with it.
847///
848/// [`request_camera_still`] asks and [`observe_camera_stills`] answers, which is
849/// the right shape for a screen that keeps a shutter open. A caller that wants
850/// one photograph wants the two joined, and joining them by hand means holding
851/// an observer alive across an await in every application that takes a picture.
852///
853/// The observer is dropped as soon as a still arrives, so a second capture is a
854/// second call rather than a subscription to unregister.
855pub async fn capture_camera_still() -> Result<CameraStill, CameraError> {
856    let signal = crate::async_io::Signal::new();
857    let deliver = signal.clone();
858    let observer = observe_camera_stills(move |result| deliver.set(result));
859    request_camera_still()?;
860    let arrived = signal.wait().await;
861    drop(observer);
862    arrived.unwrap_or(Err(CameraError::NotRunning))
863}
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868
869    struct FakeCamera {
870        started: AtomicU64,
871        stopped: AtomicU64,
872        stills: AtomicU64,
873        fails: bool,
874    }
875
876    impl FakeCamera {
877        fn new() -> Arc<Self> {
878            Arc::new(Self {
879                started: AtomicU64::new(0),
880                stopped: AtomicU64::new(0),
881                stills: AtomicU64::new(0),
882                fails: false,
883            })
884        }
885
886        fn failing() -> Arc<Self> {
887            Arc::new(Self {
888                started: AtomicU64::new(0),
889                stopped: AtomicU64::new(0),
890                stills: AtomicU64::new(0),
891                fails: true,
892            })
893        }
894    }
895
896    impl Camera for FakeCamera {
897        fn start(&self) -> Result<(), CameraError> {
898            self.started.fetch_add(1, Ordering::Relaxed);
899            if self.fails {
900                return Err(CameraError::PermissionDenied);
901            }
902            publish_camera_state(CameraState::Running {
903                device: "fake".to_string(),
904            });
905            Ok(())
906        }
907
908        fn stop(&self) {
909            self.stopped.fetch_add(1, Ordering::Relaxed);
910        }
911
912        fn request_still(&self) -> Result<(), CameraError> {
913            self.stills.fetch_add(1, Ordering::Relaxed);
914            publish_camera_still(Ok(CameraStill {
915                jpeg: vec![0xff, 0xd8],
916            }));
917            Ok(())
918        }
919    }
920
921    fn rgba_frame(sequence: u64) -> CameraFrame {
922        CameraFrame::new(2, 2, FrameFormat::Rgba8, 90, sequence, vec![7; 16])
923            .expect("a well-formed frame")
924    }
925
926    #[test]
927    fn a_frame_size_is_the_one_its_format_implies() {
928        assert_eq!(FrameFormat::Rgba8.byte_len(4, 2), Some(32));
929        assert_eq!(FrameFormat::Nv12.byte_len(4, 2), Some(12));
930        assert_eq!(
931            FrameFormat::Nv12.byte_len(3, 2),
932            None,
933            "NV12 has no half column for an odd width"
934        );
935        assert_eq!(FrameFormat::Nv12.byte_len(4, 3), None);
936    }
937
938    #[test]
939    fn a_frame_that_does_not_match_its_size_is_refused() {
940        assert!(CameraFrame::new(2, 2, FrameFormat::Rgba8, 0, 0, vec![0; 15]).is_none());
941        assert!(CameraFrame::new(2, 2, FrameFormat::Nv12, 0, 0, vec![0; 5]).is_none());
942        assert!(CameraFrame::new(2, 2, FrameFormat::Rgba8, 0, 0, vec![0; 16]).is_some());
943    }
944
945    #[test]
946    fn a_rotation_is_kept_inside_one_turn() {
947        let frame = CameraFrame::new(2, 2, FrameFormat::Rgba8, 450, 0, vec![0; 16])
948            .expect("a well-formed frame");
949        assert_eq!(frame.rotation_degrees, 90);
950    }
951
952    #[test]
953    fn an_rgba_frame_is_handed_over_unchanged() {
954        let frame = rgba_frame(0);
955        assert_eq!(frame.to_rgba8(), frame.bytes);
956    }
957
958    #[test]
959    fn nv12_black_white_and_primaries_convert_to_the_expected_colours() {
960        fn convert(luma: u8, blue: u8, red: u8) -> [u8; 4] {
961            let bytes = vec![luma, luma, luma, luma, blue, red];
962            let frame = CameraFrame::new(2, 2, FrameFormat::Nv12, 0, 0, bytes)
963                .expect("a well-formed frame");
964            let rgba = frame.to_rgba8();
965            [rgba[0], rgba[1], rgba[2], rgba[3]]
966        }
967
968        assert_eq!(convert(0, 128, 128), [0, 0, 0, 255], "black");
969        assert_eq!(convert(255, 128, 128), [255, 255, 255, 255], "white");
970
971        let red = convert(76, 84, 255);
972        assert!(
973            red[0] > 240 && red[1] < 20 && red[2] < 20,
974            "red, got {red:?}"
975        );
976        let blue = convert(29, 255, 107);
977        assert!(
978            blue[2] > 240 && blue[0] < 20 && blue[1] < 20,
979            "blue, got {blue:?}"
980        );
981        assert!(
982            convert(128, 128, 128).iter().all(|value| *value > 0),
983            "a grey frame must not clamp to black"
984        );
985    }
986
987    #[test]
988    fn a_short_nv12_frame_converts_to_black_rather_than_reading_past_its_end() {
989        let rgba = nv12_to_rgba8(4, 4, &[0u8; 3]);
990        assert_eq!(rgba.len(), 4 * 4 * 4);
991        assert!(rgba.iter().all(|value| *value == 0));
992    }
993
994    #[test]
995    fn a_platform_without_a_camera_says_so_rather_than_pretending() {
996        let _guard = crate::registry::test_service_guard();
997        clear_platform_camera();
998        assert!(!camera_supported());
999        assert_eq!(start_camera(), Err(CameraError::Unsupported));
1000        assert_eq!(
1001            camera_state(),
1002            CameraState::Failed(CameraError::Unsupported)
1003        );
1004        assert_eq!(request_camera_still(), Err(CameraError::Unsupported));
1005        clear_platform_camera();
1006    }
1007
1008    #[test]
1009    fn the_session_reports_starting_before_it_reports_running() {
1010        let _guard = crate::registry::test_service_guard();
1011        clear_platform_camera();
1012        let seen = Arc::new(Mutex::new(Vec::new()));
1013        let recorder = Arc::clone(&seen);
1014        let observer = observe_camera_state(move |state| {
1015            recorder
1016                .lock()
1017                .unwrap_or_else(|error| error.into_inner())
1018                .push(state)
1019        });
1020        set_platform_camera(FakeCamera::new());
1021        start_camera().expect("the session starts");
1022
1023        assert_eq!(
1024            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1025            vec![
1026                CameraState::Idle,
1027                CameraState::Starting,
1028                CameraState::Running {
1029                    device: "fake".to_string()
1030                }
1031            ]
1032        );
1033        assert!(camera_state().is_running());
1034        assert!(camera_state().is_active());
1035        drop(observer);
1036        clear_platform_camera();
1037    }
1038
1039    #[test]
1040    fn a_session_that_cannot_open_reports_why() {
1041        let _guard = crate::registry::test_service_guard();
1042        clear_platform_camera();
1043        set_platform_camera(FakeCamera::failing());
1044        assert_eq!(start_camera(), Err(CameraError::PermissionDenied));
1045        assert_eq!(
1046            camera_state().failure(),
1047            Some(&CameraError::PermissionDenied)
1048        );
1049        assert!(!camera_state().is_active());
1050        clear_platform_camera();
1051    }
1052
1053    #[test]
1054    fn stopping_releases_the_device_and_says_so() {
1055        let _guard = crate::registry::test_service_guard();
1056        clear_platform_camera();
1057        let backend = FakeCamera::new();
1058        set_platform_camera(backend.clone());
1059        start_camera().expect("the session starts");
1060        stop_camera();
1061        assert_eq!(backend.stopped.load(Ordering::Relaxed), 1);
1062        assert_eq!(camera_state(), CameraState::Stopped);
1063        clear_platform_camera();
1064    }
1065
1066    #[test]
1067    fn the_stored_frame_is_always_the_newest_one() {
1068        let _guard = crate::registry::test_service_guard();
1069        clear_platform_camera();
1070        assert_eq!(latest_camera_frame(), None);
1071        publish_camera_frame(rgba_frame(1));
1072        publish_camera_frame(rgba_frame(2));
1073        publish_camera_frame(rgba_frame(3));
1074        assert_eq!(latest_camera_frame().map(|frame| frame.sequence), Some(3));
1075        clear_platform_camera();
1076    }
1077
1078    #[test]
1079    fn frame_observers_see_frames_and_stop_when_dropped() {
1080        let _guard = crate::registry::test_service_guard();
1081        clear_platform_camera();
1082        let seen = Arc::new(Mutex::new(Vec::new()));
1083        let recorder = Arc::clone(&seen);
1084        let observer = observe_camera_frames(move |frame| {
1085            recorder
1086                .lock()
1087                .unwrap_or_else(|error| error.into_inner())
1088                .push(frame.sequence)
1089        });
1090        publish_camera_frame(rgba_frame(1));
1091        publish_camera_frame(rgba_frame(2));
1092        drop(observer);
1093        publish_camera_frame(rgba_frame(3));
1094        assert_eq!(
1095            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1096            vec![1, 2]
1097        );
1098        clear_platform_camera();
1099    }
1100
1101    #[test]
1102    fn frames_the_platform_could_not_deliver_are_counted_rather_than_queued() {
1103        let _guard = crate::registry::test_service_guard();
1104        clear_platform_camera();
1105        assert_eq!(dropped_camera_frames(), 0);
1106        record_dropped_camera_frame();
1107        record_dropped_camera_frame();
1108        assert_eq!(dropped_camera_frames(), 2);
1109        publish_camera_state(CameraState::Starting);
1110        assert_eq!(dropped_camera_frames(), 0);
1111        assert_eq!(latest_camera_frame(), None);
1112        clear_platform_camera();
1113    }
1114
1115    #[test]
1116    fn a_still_is_asked_for_and_arrives_separately() {
1117        let _guard = crate::registry::test_service_guard();
1118        clear_platform_camera();
1119        let backend = FakeCamera::new();
1120        set_platform_camera(backend.clone());
1121
1122        assert_eq!(
1123            request_camera_still(),
1124            Err(CameraError::NotRunning),
1125            "nothing is running, so there is nothing to photograph"
1126        );
1127
1128        let seen = Arc::new(Mutex::new(Vec::new()));
1129        let recorder = Arc::clone(&seen);
1130        let observer = observe_camera_stills(move |still| {
1131            recorder
1132                .lock()
1133                .unwrap_or_else(|error| error.into_inner())
1134                .push(still)
1135        });
1136        start_camera().expect("the session starts");
1137        request_camera_still().expect("a still is asked for");
1138        assert_eq!(backend.stills.load(Ordering::Relaxed), 1);
1139        assert_eq!(
1140            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1141            vec![Ok(CameraStill {
1142                jpeg: vec![0xff, 0xd8]
1143            })]
1144        );
1145        drop(observer);
1146        clear_platform_camera();
1147    }
1148
1149    #[test]
1150    fn a_backend_that_lists_no_lens_and_no_flash_says_so() {
1151        let _guard = crate::registry::test_service_guard();
1152        clear_platform_camera();
1153        set_platform_camera(FakeCamera::new());
1154        let backend = camera().expect("registered");
1155        assert!(backend.lenses().is_empty());
1156        assert_eq!(backend.lens(), None);
1157        assert!(!backend.use_lens("0"));
1158        assert!(!backend.has_flash());
1159        assert!(!backend.set_flash(FlashMode::On));
1160        assert!(!backend.set_torch(true));
1161        clear_platform_camera();
1162    }
1163
1164    #[test]
1165    fn a_backend_that_lists_two_lenses_hands_them_over_in_order() {
1166        let _guard = crate::registry::test_service_guard();
1167        clear_platform_camera();
1168        struct TwoLenses;
1169        impl Camera for TwoLenses {
1170            fn start(&self) -> Result<(), CameraError> {
1171                Ok(())
1172            }
1173            fn stop(&self) {}
1174            fn lenses(&self) -> Vec<CameraLens> {
1175                vec![
1176                    CameraLens {
1177                        id: "u".into(),
1178                        name: "Ultra wide".into(),
1179                        facing: LensFacing::Back,
1180                    },
1181                    CameraLens {
1182                        id: "w".into(),
1183                        name: "Wide".into(),
1184                        facing: LensFacing::Back,
1185                    },
1186                ]
1187            }
1188            fn lens(&self) -> Option<String> {
1189                Some("w".into())
1190            }
1191            fn use_lens(&self, id: &str) -> bool {
1192                id == "u" || id == "w"
1193            }
1194        }
1195        set_platform_camera(Arc::new(TwoLenses));
1196        let backend = camera().expect("registered");
1197        let lenses = backend.lenses();
1198        assert_eq!(lenses.len(), 2);
1199        assert_eq!(lenses[0].name, "Ultra wide");
1200        assert_eq!(backend.lens().as_deref(), Some("w"));
1201        assert!(backend.use_lens("u"));
1202        assert!(!backend.use_lens("tele"));
1203        clear_platform_camera();
1204    }
1205
1206    fn two_pixel_frame(rotation: u16) -> CameraFrame {
1207        let mut bytes = vec![10u8, 10, 10, 255];
1208        bytes.extend_from_slice(&[20, 20, 20, 255]);
1209        CameraFrame::new(2, 1, FrameFormat::Rgba8, rotation, 0, bytes).expect("a well-formed frame")
1210    }
1211
1212    fn pixel_values(image: &UprightRgba) -> Vec<u8> {
1213        image.rgba.iter().step_by(4).copied().collect()
1214    }
1215
1216    #[test]
1217    fn a_frame_turns_upright_by_its_rotation() {
1218        let unturned = two_pixel_frame(0).upright_rgba8();
1219        assert_eq!((unturned.width, unturned.height), (2, 1));
1220        assert_eq!(pixel_values(&unturned), vec![10, 20]);
1221
1222        let quarter = two_pixel_frame(90).upright_rgba8();
1223        assert_eq!((quarter.width, quarter.height), (1, 2));
1224        assert_eq!(pixel_values(&quarter), vec![10, 20]);
1225
1226        let half = two_pixel_frame(180).upright_rgba8();
1227        assert_eq!((half.width, half.height), (2, 1));
1228        assert_eq!(pixel_values(&half), vec![20, 10]);
1229
1230        let three_quarters = two_pixel_frame(270).upright_rgba8();
1231        assert_eq!((three_quarters.width, three_quarters.height), (1, 2));
1232        assert_eq!(pixel_values(&three_quarters), vec![20, 10]);
1233    }
1234
1235    #[test]
1236    fn an_nv12_frame_turns_and_converts_in_one_pass() {
1237        let bytes = vec![0, 255, 0, 0, 128, 128];
1238        let frame =
1239            CameraFrame::new(2, 2, FrameFormat::Nv12, 90, 0, bytes).expect("a well-formed frame");
1240        let upright = frame.upright_rgba8();
1241        assert_eq!((upright.width, upright.height), (2, 2));
1242        let values = pixel_values(&upright);
1243        assert_eq!(values[0], 0, "top-left stays dark");
1244        assert_eq!(
1245            values[3], 255,
1246            "the bright top-right pixel lands bottom-right"
1247        );
1248    }
1249
1250    #[test]
1251    fn an_rgb8_frame_turns_upright_with_a_full_alpha() {
1252        let frame = CameraFrame::new(
1253            2,
1254            1,
1255            FrameFormat::Rgb8,
1256            180,
1257            0,
1258            vec![10, 10, 10, 20, 20, 20],
1259        )
1260        .expect("a well-formed frame");
1261        let upright = frame.upright_rgba8();
1262        assert_eq!(pixel_values(&upright), vec![20, 10]);
1263        assert!(upright.rgba.iter().skip(3).step_by(4).all(|a| *a == 255));
1264    }
1265
1266    #[test]
1267    fn a_turn_that_is_not_a_quarter_is_left_alone() {
1268        let frame = CameraFrame::new(2, 1, FrameFormat::Rgba8, 45, 0, vec![7; 8])
1269            .expect("a well-formed frame");
1270        let upright = frame.upright_rgba8();
1271        assert_eq!((upright.width, upright.height), (2, 1));
1272        assert_eq!(upright.rgba, frame.to_rgba8());
1273    }
1274
1275    #[test]
1276    fn the_lens_list_is_published_and_observed() {
1277        let _guard = crate::registry::test_service_guard();
1278        clear_platform_camera();
1279        assert_eq!(camera_lenses(), CameraLenses::default());
1280
1281        let seen = Arc::new(Mutex::new(Vec::new()));
1282        let recorder = Arc::clone(&seen);
1283        let observer = observe_camera_lenses(move |lenses| {
1284            recorder
1285                .lock()
1286                .unwrap_or_else(|error| error.into_inner())
1287                .push(lenses)
1288        });
1289
1290        let published = CameraLenses {
1291            lenses: vec![CameraLens {
1292                id: "0".into(),
1293                name: "Back".into(),
1294                facing: LensFacing::Back,
1295            }],
1296            active: Some("0".into()),
1297        };
1298        publish_camera_lenses(published.clone());
1299        publish_camera_lenses(published.clone());
1300        assert_eq!(camera_lenses(), published);
1301        assert_eq!(
1302            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1303            vec![CameraLenses::default(), published.clone()],
1304            "the current list arrives at once, and a repeat is not re-delivered"
1305        );
1306
1307        drop(observer);
1308        publish_camera_lenses(CameraLenses::default());
1309        assert_eq!(
1310            seen.lock().unwrap_or_else(|error| error.into_inner()).len(),
1311            2,
1312            "a dropped observer hears nothing more"
1313        );
1314        clear_platform_camera();
1315        assert_eq!(camera_lenses(), CameraLenses::default());
1316    }
1317}