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::{Arc, Mutex, OnceLock, atomic::Ordering};
25
26use cranpose_core::{EventStream, State, rememberEventStream};
27use portable_atomic::AtomicU64;
28
29use crate::registry::ServiceRegistry;
30
31/// How a frame's pixels are laid out.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
33pub enum FrameFormat {
34    /// Tightly packed RGBA8, row-major, no padding.
35    #[default]
36    Rgba8,
37    /// Tightly packed RGB8, row-major, no padding.
38    ///
39    /// What a USB webcam decodes to. Carried as-is so a desktop viewfinder does
40    /// not pay a widening copy per frame for an alpha channel a camera never
41    /// has.
42    Rgb8,
43    /// Full-range NV12: a `width * height` luma plane followed by an
44    /// interleaved `width * height / 2` chroma plane at half resolution in both
45    /// directions.
46    ///
47    /// What a phone camera produces. Carrying it as-is is what removes the
48    /// encode-and-decode round trip a JPEG preview pays on every frame.
49    Nv12,
50}
51
52impl FrameFormat {
53    /// How many bytes a frame of this size occupies, or `None` when the size
54    /// cannot be represented — which is a frame nobody can allocate anyway.
55    pub fn byte_len(self, width: u32, height: u32) -> Option<usize> {
56        let pixels = (width as usize).checked_mul(height as usize)?;
57        match self {
58            FrameFormat::Rgba8 => pixels.checked_mul(4),
59            FrameFormat::Rgb8 => pixels.checked_mul(3),
60            FrameFormat::Nv12 => {
61                if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
62                    return None;
63                }
64                pixels.checked_add(pixels / 2)
65            }
66        }
67    }
68}
69
70/// One frame from the viewfinder.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct CameraFrame {
73    /// Width of the frame in pixels, before the rotation in `rotation_degrees`.
74    pub width: u32,
75    /// Height of the frame in pixels, before the rotation in `rotation_degrees`.
76    pub height: u32,
77    /// How the pixels in `bytes` are laid out.
78    pub format: FrameFormat,
79    /// How far the frame must be rotated clockwise to be the right way up,
80    /// which is the sensor's mounting plus the device's rotation.
81    pub rotation_degrees: u16,
82    /// Which frame this is in the session, so a consumer can tell a repeat from
83    /// a new one and count what it missed.
84    pub sequence: u64,
85    /// The pixels, in [`format`](Self::format).
86    pub bytes: Vec<u8>,
87}
88
89impl CameraFrame {
90    /// A frame, or `None` when the bytes do not match the size and format —
91    /// which is a backend bug, and one that reads as corrupted video rather
92    /// than as an error if it is let through.
93    pub fn new(
94        width: u32,
95        height: u32,
96        format: FrameFormat,
97        rotation_degrees: u16,
98        sequence: u64,
99        bytes: Vec<u8>,
100    ) -> Option<Self> {
101        if format.byte_len(width, height)? != bytes.len() {
102            return None;
103        }
104        Some(Self {
105            width,
106            height,
107            format,
108            rotation_degrees: rotation_degrees % 360,
109            sequence,
110            bytes,
111        })
112    }
113
114    /// The frame as tightly packed RGBA8, converting only when it has to.
115    ///
116    /// One pass over the bytes with no allocation beyond the result, because
117    /// this runs per frame: a conversion that allocates per row shows up as
118    /// dropped frames rather than as a slow function.
119    pub fn to_rgba8(&self) -> Vec<u8> {
120        match self.format {
121            FrameFormat::Rgba8 => self.bytes.clone(),
122            FrameFormat::Rgb8 => rgb8_to_rgba8(&self.bytes),
123            FrameFormat::Nv12 => nv12_to_rgba8(self.width, self.height, &self.bytes),
124        }
125    }
126
127    /// The frame as tightly packed RGBA8 with
128    /// [`rotation_degrees`](Self::rotation_degrees) applied, so the pixels are
129    /// the right way up whatever the sensor's mounting was.
130    ///
131    /// The turn happens in the same pass as the format conversion, because this
132    /// runs on every previewed frame: converting and then turning would walk
133    /// the pixels twice. A rotation that is not a quarter turn is left alone —
134    /// no camera produces one.
135    pub fn upright_rgba8(&self) -> UprightRgba {
136        let rotation = self.rotation_degrees;
137        if !matches!(rotation, 90 | 180 | 270) {
138            return UprightRgba {
139                width: self.width,
140                height: self.height,
141                rgba: self.to_rgba8(),
142            };
143        }
144        let (width, height) = (self.width as usize, self.height as usize);
145        let (out_width, out_height) = match rotation {
146            90 | 270 => (self.height, self.width),
147            _ => (self.width, self.height),
148        };
149        let mut rgba = vec![0u8; width * height * 4];
150        match self.format {
151            FrameFormat::Rgba8 => {
152                for y in 0..height {
153                    let row = &self.bytes[y * width * 4..(y + 1) * width * 4];
154                    for x in 0..width {
155                        let src = &row[x * 4..x * 4 + 4];
156                        let dst = turned_index(rotation, width, height, x, y) * 4;
157                        rgba[dst..dst + 4].copy_from_slice(src);
158                    }
159                }
160            }
161            FrameFormat::Rgb8 => {
162                for y in 0..height {
163                    let row = &self.bytes[y * width * 3..(y + 1) * width * 3];
164                    for x in 0..width {
165                        let src = &row[x * 3..x * 3 + 3];
166                        let dst = turned_index(rotation, width, height, x, y) * 4;
167                        rgba[dst..dst + 3].copy_from_slice(src);
168                        rgba[dst + 3] = 255;
169                    }
170                }
171            }
172            FrameFormat::Nv12 => {
173                let pixels = width * height;
174                if self.bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
175                    return UprightRgba {
176                        width: out_width,
177                        height: out_height,
178                        rgba,
179                    };
180                }
181                let (luma, chroma) = self.bytes.split_at(pixels);
182                for y in 0..height {
183                    let luma_row = &luma[y * width..(y + 1) * width];
184                    let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
185                    for x in 0..width {
186                        let luminance = luma_row[x] as i32;
187                        let blue_difference = chroma_row[x & !1] as i32 - 128;
188                        let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
189                        let dst = turned_index(rotation, width, height, x, y) * 4;
190                        rgba[dst] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
191                        rgba[dst + 1] = clamp_byte(
192                            luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16),
193                        );
194                        rgba[dst + 2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
195                        rgba[dst + 3] = 255;
196                    }
197                }
198            }
199        }
200        UprightRgba {
201            width: out_width,
202            height: out_height,
203            rgba,
204        }
205    }
206}
207
208/// A frame's pixels as tightly packed RGBA8, already the right way up.
209///
210/// `width` and `height` describe the turned image, so a 90° or 270° turn swaps
211/// them relative to the frame that produced this.
212#[derive(Clone, Debug, PartialEq, Eq)]
213pub struct UprightRgba {
214    /// Width of the image in pixels, after the turn.
215    pub width: u32,
216    /// Height of the image in pixels, after the turn.
217    pub height: u32,
218    /// Tightly packed RGBA8 pixels, row-major, with `width * height * 4` bytes.
219    pub rgba: Vec<u8>,
220}
221
222#[inline]
223fn turned_index(rotation: u16, width: usize, height: usize, x: usize, y: usize) -> usize {
224    match rotation {
225        90 => x * height + (height - 1 - y),
226        180 => (height - 1 - y) * width + (width - 1 - x),
227        270 => (width - 1 - x) * height + y,
228        _ => y * width + x,
229    }
230}
231
232fn rgb8_to_rgba8(bytes: &[u8]) -> Vec<u8> {
233    let mut rgba = Vec::with_capacity(bytes.len() / 3 * 4);
234    for [red, green, blue] in bytes.as_chunks::<3>().0 {
235        rgba.extend_from_slice(&[*red, *green, *blue, 255]);
236    }
237    rgba
238}
239
240fn nv12_to_rgba8(width: u32, height: u32, bytes: &[u8]) -> Vec<u8> {
241    let (width, height) = (width as usize, height as usize);
242    let pixels = width * height;
243    let mut rgba = vec![0u8; pixels * 4];
244    if bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
245        return rgba;
246    }
247    let (luma, chroma) = bytes.split_at(pixels);
248
249    for y in 0..height {
250        let luma_row = &luma[y * width..(y + 1) * width];
251        let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
252        let out_row = &mut rgba[y * width * 4..(y + 1) * width * 4];
253        for x in 0..width {
254            let luminance = luma_row[x] as i32;
255            let blue_difference = chroma_row[x & !1] as i32 - 128;
256            let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
257            let out = &mut out_row[x * 4..x * 4 + 4];
258            out[0] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
259            out[1] =
260                clamp_byte(luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16));
261            out[2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
262            out[3] = 255;
263        }
264    }
265    rgba
266}
267
268fn clamp_byte(value: i32) -> u8 {
269    value.clamp(0, 255) as u8
270}
271
272/// A full-resolution still photograph as an encoded image.
273///
274/// Unlike a viewfinder frame, a still comes through the platform's dedicated
275/// photo pipeline at full sensor resolution. The bytes are an encoded JPEG
276/// whose EXIF orientation tag reflects the device rotation; decode with an
277/// orientation-aware decoder.
278#[derive(Clone, Debug, PartialEq, Eq)]
279pub struct CameraStill {
280    /// The bytes of the encoded JPEG image.
281    pub jpeg: Vec<u8>,
282}
283
284/// One capture device the application may pick.
285///
286/// `id` is the platform's own handle for the device (an `AVCaptureDevice`
287/// uniqueID on iOS, a camera2 id on Android, a device index on desktop); pass
288/// it back to [`Camera::use_lens`]. `name` is for a button label: "Ultra
289/// wide", "Wide", "Tele".
290#[derive(Clone, Debug, PartialEq, Eq)]
291pub struct CameraLens {
292    /// The platform's handle for the device. Pass it to [`Camera::use_lens`].
293    pub id: String,
294    /// Short text for a lens button, such as `Ultra wide`, `Wide` or `Tele`.
295    pub name: String,
296    /// Which way the device points, so an application can offer back lenses
297    /// and the front lens as different controls.
298    pub facing: LensFacing,
299}
300
301/// Which way a capture device points.
302#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
303pub enum LensFacing {
304    /// Away from the screen: the main photography cameras on a phone.
305    #[default]
306    Back,
307    /// At the person holding the device.
308    Front,
309    /// Not fixed to a screen at all: a webcam or another attached device.
310    External,
311}
312
313/// The devices the application may pick between, and the one in use.
314///
315/// Published by the backend when a session opens and when the device changes.
316/// A screen showing a lens control observes this instead of asking the
317/// platform, because both phone lens lists are blocking platform calls — a
318/// JNI round trip on Android, a fresh discovery session on iOS — and a
319/// recomposition must not pay that.
320#[derive(Clone, Debug, Default, PartialEq, Eq)]
321pub struct CameraLenses {
322    /// Back lenses first in field-of-view order, widest first, then the rest.
323    pub lenses: Vec<CameraLens>,
324    /// The id of the device the open session uses, or `None` while nothing
325    /// runs.
326    pub active: Option<String>,
327}
328
329/// What the light does when a still is captured.
330///
331/// `Auto` leaves the choice to the device's exposure metering. A backend with
332/// no flash reports `false` from [`Camera::set_flash`] and the application
333/// hides the control.
334#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
335pub enum FlashMode {
336    /// The light stays off.
337    #[default]
338    Off,
339    /// The device's exposure metering decides whether the light fires.
340    Auto,
341    /// The light fires for every still.
342    On,
343}
344
345/// Why a camera call failed.
346#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
347pub enum CameraError {
348    /// No live camera backend on this platform.
349    #[error("live camera capture is not supported here")]
350    Unsupported,
351    /// The user denied camera access.
352    #[error("camera permission denied")]
353    PermissionDenied,
354    /// A still was asked for while nothing was running.
355    #[error("the camera is not running")]
356    NotRunning,
357    /// Any other failure — no device, a configuration the hardware refused.
358    #[error("{0}")]
359    Failed(String),
360}
361
362/// What the camera session is doing.
363#[derive(Clone, Debug, Default, PartialEq, Eq)]
364pub enum CameraState {
365    /// Nothing has been asked of it.
366    #[default]
367    Idle,
368    /// A session is opening. Phones take a noticeable moment over this, which
369    /// is why it is a state a screen can show rather than a gap before frames.
370    Starting,
371    /// Frames are being produced.
372    Running {
373        /// The device the session opened, for a label.
374        device: String,
375    },
376    /// The session was stopped and the device released.
377    Stopped,
378    /// The session could not start, or ended on its own.
379    Failed(CameraError),
380}
381
382impl CameraState {
383    /// Whether frames are being produced now.
384    pub fn is_running(&self) -> bool {
385        matches!(self, CameraState::Running { .. })
386    }
387
388    /// Whether the session is opening or open — which is when a screen keeps
389    /// the viewfinder on screen rather than showing a placeholder.
390    pub fn is_active(&self) -> bool {
391        matches!(self, CameraState::Starting | CameraState::Running { .. })
392    }
393
394    /// The failure, if the session ended in one.
395    pub fn failure(&self) -> Option<&CameraError> {
396        match self {
397            CameraState::Failed(error) => Some(error),
398            _ => None,
399        }
400    }
401}
402
403/// A platform capture session.
404///
405/// A backend starts and stops the device and publishes what it produces through
406/// [`publish_camera_frame`] and [`publish_camera_state`]; nothing here is
407/// polled, and no method blocks for the length of a capture.
408pub trait Camera: Send + Sync {
409    /// Opens the session.
410    ///
411    /// Returns as soon as the request is accepted. The session's progress
412    /// arrives as [`CameraState`], because opening a camera takes long enough
413    /// on a phone that a screen has to show something in the meantime.
414    fn start(&self) -> Result<(), CameraError>;
415
416    /// Stops the session and releases the device.
417    fn stop(&self);
418
419    /// Asks for a full-resolution still.
420    ///
421    /// The picture arrives through [`publish_camera_still`], because the device
422    /// takes as long as it takes to expose and encode, and a call that blocked
423    /// for it would block whatever asked.
424    fn request_still(&self) -> Result<(), CameraError> {
425        Err(CameraError::Unsupported)
426    }
427
428    /// Turns the torch on or off while the session runs.
429    ///
430    /// Scanner-style applications light a dim scene rather than analysing
431    /// photon-starved frames. Returns `false` where the device has no torch;
432    /// the torch dies with the session.
433    fn set_torch(&self, _on: bool) -> bool {
434        false
435    }
436
437    /// The devices the application may pick between, back cameras first and in
438    /// field-of-view order, widest first, then the rest.
439    ///
440    /// An empty list means the application shows no lens control: either the
441    /// platform has one camera or the backend does not list them. Backends
442    /// also publish this through [`publish_camera_lenses`] when a session
443    /// opens, so a screen observes [`rememberCameraLenses`] rather than paying
444    /// this blocking platform call per recomposition.
445    fn lenses(&self) -> Vec<CameraLens> {
446        Vec::new()
447    }
448
449    /// The device the session is using, or `None` when nothing is open and the
450    /// backend has no stored choice.
451    fn lens(&self) -> Option<String> {
452        None
453    }
454
455    /// Opens `id` instead of the current device, keeping the session running.
456    /// Returns `false` when the id is unknown or the backend cannot switch.
457    fn use_lens(&self, _id: &str) -> bool {
458        false
459    }
460
461    /// Whether the current device has a flash for stills.
462    fn has_flash(&self) -> bool {
463        false
464    }
465
466    /// What the flash does on the next still. Returns `false` where the device
467    /// has no flash; the mode dies with the session.
468    fn set_flash(&self, _mode: FlashMode) -> bool {
469        false
470    }
471}
472
473/// Shared handle to the platform camera.
474pub type CameraRef = Arc<dyn Camera>;
475
476static PLATFORM_CAMERA: ServiceRegistry<dyn Camera> = ServiceRegistry::new();
477
478/// Installs the platform camera, replacing any previous one.
479pub fn set_platform_camera(camera: CameraRef) {
480    PLATFORM_CAMERA.set(camera);
481}
482
483/// Removes the platform camera and forgets everything the last session
484/// published.
485pub fn clear_platform_camera() {
486    PLATFORM_CAMERA.clear();
487    if let Ok(mut observers) = frame_observers().lock() {
488        observers.clear();
489    }
490    if let Ok(mut observers) = state_observers().lock() {
491        observers.clear();
492    }
493    if let Ok(mut observers) = still_observers().lock() {
494        observers.clear();
495    }
496    if let Ok(mut observers) = lens_observers().lock() {
497        observers.clear();
498    }
499    if let Ok(mut latest) = latest_frame_slot().lock() {
500        *latest = None;
501    }
502    if let Ok(mut state) = state_slot().lock() {
503        *state = CameraState::Idle;
504    }
505    if let Ok(mut lenses) = lenses_slot().lock() {
506        *lenses = CameraLenses::default();
507    }
508    DROPPED_FRAMES.store(0, Ordering::Release);
509}
510
511/// The installed camera, or `None` where live capture is unsupported — which is
512/// when an application falls back to the image picker.
513pub fn camera() -> Option<CameraRef> {
514    PLATFORM_CAMERA.get()
515}
516
517/// Whether this platform has a live camera at all.
518pub fn camera_supported() -> bool {
519    PLATFORM_CAMERA.get().is_some()
520}
521
522fn state_slot() -> &'static Mutex<CameraState> {
523    static SLOT: OnceLock<Mutex<CameraState>> = OnceLock::new();
524    SLOT.get_or_init(|| Mutex::new(CameraState::Idle))
525}
526
527fn latest_frame_slot() -> &'static Mutex<Option<CameraFrame>> {
528    static SLOT: OnceLock<Mutex<Option<CameraFrame>>> = OnceLock::new();
529    SLOT.get_or_init(|| Mutex::new(None))
530}
531
532fn lenses_slot() -> &'static Mutex<CameraLenses> {
533    static SLOT: OnceLock<Mutex<CameraLenses>> = OnceLock::new();
534    SLOT.get_or_init(|| Mutex::new(CameraLenses::default()))
535}
536
537static DROPPED_FRAMES: AtomicU64 = AtomicU64::new(0);
538
539type FrameObserver = Arc<dyn Fn(CameraFrame) + Send + Sync>;
540type StateObserver = Arc<dyn Fn(CameraState) + Send + Sync>;
541type StillObserver = Arc<dyn Fn(Result<CameraStill, CameraError>) + Send + Sync>;
542type LensObserver = Arc<dyn Fn(CameraLenses) + Send + Sync>;
543
544fn frame_observers() -> &'static Mutex<Vec<(u64, FrameObserver)>> {
545    static SLOT: OnceLock<Mutex<Vec<(u64, FrameObserver)>>> = OnceLock::new();
546    SLOT.get_or_init(|| Mutex::new(Vec::new()))
547}
548
549fn state_observers() -> &'static Mutex<Vec<(u64, StateObserver)>> {
550    static SLOT: OnceLock<Mutex<Vec<(u64, StateObserver)>>> = OnceLock::new();
551    SLOT.get_or_init(|| Mutex::new(Vec::new()))
552}
553
554fn still_observers() -> &'static Mutex<Vec<(u64, StillObserver)>> {
555    static SLOT: OnceLock<Mutex<Vec<(u64, StillObserver)>>> = OnceLock::new();
556    SLOT.get_or_init(|| Mutex::new(Vec::new()))
557}
558
559fn lens_observers() -> &'static Mutex<Vec<(u64, LensObserver)>> {
560    static SLOT: OnceLock<Mutex<Vec<(u64, LensObserver)>>> = OnceLock::new();
561    SLOT.get_or_init(|| Mutex::new(Vec::new()))
562}
563
564static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
565
566/// Keeps a camera observer registered until it is dropped.
567pub struct CameraObserver {
568    id: u64,
569    kind: ObserverKind,
570}
571
572#[derive(Clone, Copy)]
573enum ObserverKind {
574    Frame,
575    State,
576    Still,
577    Lenses,
578}
579
580impl Drop for CameraObserver {
581    fn drop(&mut self) {
582        match self.kind {
583            ObserverKind::Frame => retain_without(frame_observers(), self.id),
584            ObserverKind::State => retain_without(state_observers(), self.id),
585            ObserverKind::Still => retain_without(still_observers(), self.id),
586            ObserverKind::Lenses => retain_without(lens_observers(), self.id),
587        }
588    }
589}
590
591fn retain_without<T>(slot: &'static Mutex<Vec<(u64, T)>>, id: u64) {
592    if let Ok(mut observers) = slot.lock() {
593        observers.retain(|(observer, _)| *observer != id);
594    }
595}
596
597fn snapshot<T: Clone>(slot: &'static Mutex<Vec<(u64, T)>>) -> Vec<T> {
598    slot.lock()
599        .map(|observers| {
600            observers
601                .iter()
602                .map(|(_, observer)| observer.clone())
603                .collect()
604        })
605        .unwrap_or_default()
606}
607
608/// The last frame the session produced, or `None` before the first one.
609///
610/// Read outside composition — during draw — so a viewfinder shows the newest
611/// frame without a recomposition per frame.
612pub fn latest_camera_frame() -> Option<CameraFrame> {
613    latest_frame_slot()
614        .lock()
615        .map_or(None, |frame| frame.clone())
616}
617
618/// What the session is doing.
619pub fn camera_state() -> CameraState {
620    state_slot()
621        .lock()
622        .map(|state| state.clone())
623        .unwrap_or_default()
624}
625
626/// How many frames were produced while every observer was still busy.
627pub fn dropped_camera_frames() -> u64 {
628    DROPPED_FRAMES.load(Ordering::Acquire)
629}
630
631/// Publishes a frame. Backends call this from whichever thread the platform
632/// delivers frames on.
633///
634/// The newest frame always replaces the stored one, so a viewfinder never draws
635/// a stale frame; observers that are keeping up see every frame, and one that
636/// is not is counted in [`dropped_camera_frames`] rather than queued behind.
637pub fn publish_camera_frame(frame: CameraFrame) {
638    if let Ok(mut latest) = latest_frame_slot().lock() {
639        *latest = Some(frame.clone());
640    }
641    let observers = snapshot(frame_observers());
642    if observers.is_empty() {
643        return;
644    }
645    for observer in observers {
646        observer(frame.clone());
647    }
648}
649
650/// Records that the platform produced a frame nobody could take.
651///
652/// A backend running a bounded analysis queue calls this when it drops one, so
653/// the count reflects what the device produced rather than what got through.
654pub fn record_dropped_camera_frame() {
655    DROPPED_FRAMES.fetch_add(1, Ordering::AcqRel);
656}
657
658/// Publishes what the session is doing.
659pub fn publish_camera_state(state: CameraState) {
660    {
661        let Ok(mut current) = state_slot().lock() else {
662            return;
663        };
664        if *current == state {
665            return;
666        }
667        *current = state.clone();
668    }
669    if matches!(state, CameraState::Idle | CameraState::Starting) {
670        DROPPED_FRAMES.store(0, Ordering::Release);
671        if let Ok(mut latest) = latest_frame_slot().lock() {
672            *latest = None;
673        }
674    }
675    for observer in snapshot(state_observers()) {
676        observer(state.clone());
677    }
678}
679
680/// Publishes the answer to a still request.
681pub fn publish_camera_still(still: Result<CameraStill, CameraError>) {
682    for observer in snapshot(still_observers()) {
683        observer(still.clone());
684    }
685}
686
687/// Publishes the lens list and the device in use. Backends call this when a
688/// session opens and when the device changes.
689pub fn publish_camera_lenses(lenses: CameraLenses) {
690    {
691        let Ok(mut current) = lenses_slot().lock() else {
692            return;
693        };
694        if *current == lenses {
695            return;
696        }
697        *current = lenses.clone();
698    }
699    for observer in snapshot(lens_observers()) {
700        observer(lenses.clone());
701    }
702}
703
704/// The devices the application may pick between, as the backend last published
705/// them.
706pub fn camera_lenses() -> CameraLenses {
707    lenses_slot()
708        .lock()
709        .map(|lenses| lenses.clone())
710        .unwrap_or_default()
711}
712
713/// Registers `observer` for frames. Applications collect
714/// [`rememberCameraFrames`] instead of calling this.
715pub fn observe_camera_frames(
716    observer: impl Fn(CameraFrame) + Send + Sync + 'static,
717) -> CameraObserver {
718    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
719    if let Ok(mut observers) = frame_observers().lock() {
720        observers.push((id, Arc::new(observer)));
721    }
722    CameraObserver {
723        id,
724        kind: ObserverKind::Frame,
725    }
726}
727
728/// Registers `observer` for session state. The current state is delivered at
729/// once, so a screen composed mid-session shows what is happening rather than
730/// waiting for the next change.
731pub fn observe_camera_state(
732    observer: impl Fn(CameraState) + Send + Sync + 'static,
733) -> CameraObserver {
734    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
735    let observer: StateObserver = Arc::new(observer);
736    if let Ok(mut observers) = state_observers().lock() {
737        observers.push((id, Arc::clone(&observer)));
738    }
739    observer(camera_state());
740    CameraObserver {
741        id,
742        kind: ObserverKind::State,
743    }
744}
745
746/// Registers `observer` for stills.
747pub fn observe_camera_stills(
748    observer: impl Fn(Result<CameraStill, CameraError>) + Send + Sync + 'static,
749) -> CameraObserver {
750    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
751    if let Ok(mut observers) = still_observers().lock() {
752        observers.push((id, Arc::new(observer)));
753    }
754    CameraObserver {
755        id,
756        kind: ObserverKind::Still,
757    }
758}
759
760/// Registers `observer` for the lens list. The current list is delivered at
761/// once, so a screen composed mid-session shows the devices rather than
762/// waiting for the next change.
763pub fn observe_camera_lenses(
764    observer: impl Fn(CameraLenses) + Send + Sync + 'static,
765) -> CameraObserver {
766    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
767    let observer: LensObserver = Arc::new(observer);
768    if let Ok(mut observers) = lens_observers().lock() {
769        observers.push((id, Arc::clone(&observer)));
770    }
771    observer(camera_lenses());
772    CameraObserver {
773        id,
774        kind: ObserverKind::Lenses,
775    }
776}
777
778/// What the camera session is doing, observed for as long as this call stays in
779/// the composition.
780#[expect(non_snake_case)]
781#[track_caller]
782pub fn rememberCameraState() -> State<CameraState> {
783    let updates = rememberEventStream((), |sender| {
784        observe_camera_state(move |state| sender.send(state))
785    });
786    cranpose_core::collectAsState(updates, (), camera_state())
787}
788
789/// The frames the session produces, as a stream this composition collects.
790///
791/// This is the analysis path: a detector collects it, and one that cannot keep
792/// up falls behind by frames rather than by memory — see
793/// [`dropped_camera_frames`]. A viewfinder draws [`latest_camera_frame`]
794/// instead, which costs no recomposition at all.
795#[expect(non_snake_case)]
796#[track_caller]
797pub fn rememberCameraFrames() -> EventStream<CameraFrame> {
798    rememberEventStream((), |sender| {
799        observe_camera_frames(move |frame| sender.send(frame))
800    })
801}
802
803/// The stills the session produces, as a stream this composition collects.
804#[expect(non_snake_case)]
805#[track_caller]
806pub fn rememberCameraStills() -> EventStream<Result<CameraStill, CameraError>> {
807    rememberEventStream((), |sender| {
808        observe_camera_stills(move |still| sender.send(still))
809    })
810}
811
812/// The lens list and the device in use, observed for as long as this call
813/// stays in the composition.
814#[expect(non_snake_case)]
815#[track_caller]
816pub fn rememberCameraLenses() -> State<CameraLenses> {
817    let updates = rememberEventStream((), |sender| {
818        observe_camera_lenses(move |lenses| sender.send(lenses))
819    });
820    cranpose_core::collectAsState(updates, (), camera_lenses())
821}
822
823/// Starts the camera, publishing [`CameraState::Starting`] before the backend
824/// is asked so a screen shows the wait rather than a gap.
825pub fn start_camera() -> Result<(), CameraError> {
826    let Some(camera) = camera() else {
827        publish_camera_state(CameraState::Failed(CameraError::Unsupported));
828        return Err(CameraError::Unsupported);
829    };
830    publish_camera_state(CameraState::Starting);
831    camera.start().inspect_err(|error| {
832        publish_camera_state(CameraState::Failed(error.clone()));
833    })
834}
835
836/// Stops the camera and releases the device.
837pub fn stop_camera() {
838    if let Some(camera) = camera() {
839        camera.stop();
840    }
841    publish_camera_state(CameraState::Stopped);
842}
843
844/// Asks for a full-resolution still, which arrives through
845/// [`rememberCameraStills`].
846pub fn request_camera_still() -> Result<(), CameraError> {
847    let Some(camera) = camera() else {
848        return Err(CameraError::Unsupported);
849    };
850    if !camera_state().is_running() {
851        return Err(CameraError::NotRunning);
852    }
853    camera.request_still()
854}
855
856/// Takes one full-resolution still and resolves with it.
857///
858/// [`request_camera_still`] asks and [`observe_camera_stills`] answers, which is
859/// the right shape for a screen that keeps a shutter open. A caller that wants
860/// one photograph wants the two joined, and joining them by hand means holding
861/// an observer alive across an await in every application that takes a picture.
862///
863/// The observer is dropped as soon as a still arrives, so a second capture is a
864/// second call rather than a subscription to unregister.
865pub async fn capture_camera_still() -> Result<CameraStill, CameraError> {
866    let signal = crate::async_io::Signal::new();
867    let deliver = signal.clone();
868    let observer = observe_camera_stills(move |result| deliver.set(result));
869    request_camera_still()?;
870    let arrived = signal.wait().await;
871    drop(observer);
872    arrived.unwrap_or(Err(CameraError::NotRunning))
873}
874
875#[cfg(test)]
876#[path = "tests/camera_tests.rs"]
877mod tests;