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_or(None, |frame| frame.clone())
605}
606
607/// What the session is doing.
608pub fn camera_state() -> CameraState {
609    state_slot()
610        .lock()
611        .map(|state| state.clone())
612        .unwrap_or_default()
613}
614
615/// How many frames were produced while every observer was still busy.
616pub fn dropped_camera_frames() -> u64 {
617    DROPPED_FRAMES.load(Ordering::Acquire)
618}
619
620/// Publishes a frame. Backends call this from whichever thread the platform
621/// delivers frames on.
622///
623/// The newest frame always replaces the stored one, so a viewfinder never draws
624/// a stale frame; observers that are keeping up see every frame, and one that
625/// is not is counted in [`dropped_camera_frames`] rather than queued behind.
626pub fn publish_camera_frame(frame: CameraFrame) {
627    if let Ok(mut latest) = latest_frame_slot().lock() {
628        *latest = Some(frame.clone());
629    }
630    let observers = snapshot(frame_observers());
631    if observers.is_empty() {
632        return;
633    }
634    for observer in observers {
635        observer(frame.clone());
636    }
637}
638
639/// Records that the platform produced a frame nobody could take.
640///
641/// A backend running a bounded analysis queue calls this when it drops one, so
642/// the count reflects what the device produced rather than what got through.
643pub fn record_dropped_camera_frame() {
644    DROPPED_FRAMES.fetch_add(1, Ordering::AcqRel);
645}
646
647/// Publishes what the session is doing.
648pub fn publish_camera_state(state: CameraState) {
649    {
650        let Ok(mut current) = state_slot().lock() else {
651            return;
652        };
653        if *current == state {
654            return;
655        }
656        *current = state.clone();
657    }
658    if matches!(state, CameraState::Idle | CameraState::Starting) {
659        DROPPED_FRAMES.store(0, Ordering::Release);
660        if let Ok(mut latest) = latest_frame_slot().lock() {
661            *latest = None;
662        }
663    }
664    for observer in snapshot(state_observers()) {
665        observer(state.clone());
666    }
667}
668
669/// Publishes the answer to a still request.
670pub fn publish_camera_still(still: Result<CameraStill, CameraError>) {
671    for observer in snapshot(still_observers()) {
672        observer(still.clone());
673    }
674}
675
676/// Publishes the lens list and the device in use. Backends call this when a
677/// session opens and when the device changes.
678pub fn publish_camera_lenses(lenses: CameraLenses) {
679    {
680        let Ok(mut current) = lenses_slot().lock() else {
681            return;
682        };
683        if *current == lenses {
684            return;
685        }
686        *current = lenses.clone();
687    }
688    for observer in snapshot(lens_observers()) {
689        observer(lenses.clone());
690    }
691}
692
693/// The devices the application may pick between, as the backend last published
694/// them.
695pub fn camera_lenses() -> CameraLenses {
696    lenses_slot()
697        .lock()
698        .map(|lenses| lenses.clone())
699        .unwrap_or_default()
700}
701
702/// Registers `observer` for frames. Applications collect
703/// [`rememberCameraFrames`] instead of calling this.
704pub fn observe_camera_frames(
705    observer: impl Fn(CameraFrame) + Send + Sync + 'static,
706) -> CameraObserver {
707    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
708    if let Ok(mut observers) = frame_observers().lock() {
709        observers.push((id, Arc::new(observer)));
710    }
711    CameraObserver {
712        id,
713        kind: ObserverKind::Frame,
714    }
715}
716
717/// Registers `observer` for session state. The current state is delivered at
718/// once, so a screen composed mid-session shows what is happening rather than
719/// waiting for the next change.
720pub fn observe_camera_state(
721    observer: impl Fn(CameraState) + Send + Sync + 'static,
722) -> CameraObserver {
723    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
724    let observer: StateObserver = Arc::new(observer);
725    if let Ok(mut observers) = state_observers().lock() {
726        observers.push((id, Arc::clone(&observer)));
727    }
728    observer(camera_state());
729    CameraObserver {
730        id,
731        kind: ObserverKind::State,
732    }
733}
734
735/// Registers `observer` for stills.
736pub fn observe_camera_stills(
737    observer: impl Fn(Result<CameraStill, CameraError>) + Send + Sync + 'static,
738) -> CameraObserver {
739    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
740    if let Ok(mut observers) = still_observers().lock() {
741        observers.push((id, Arc::new(observer)));
742    }
743    CameraObserver {
744        id,
745        kind: ObserverKind::Still,
746    }
747}
748
749/// Registers `observer` for the lens list. The current list is delivered at
750/// once, so a screen composed mid-session shows the devices rather than
751/// waiting for the next change.
752pub fn observe_camera_lenses(
753    observer: impl Fn(CameraLenses) + Send + Sync + 'static,
754) -> CameraObserver {
755    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
756    let observer: LensObserver = Arc::new(observer);
757    if let Ok(mut observers) = lens_observers().lock() {
758        observers.push((id, Arc::clone(&observer)));
759    }
760    observer(camera_lenses());
761    CameraObserver {
762        id,
763        kind: ObserverKind::Lenses,
764    }
765}
766
767/// What the camera session is doing, observed for as long as this call stays in
768/// the composition.
769#[expect(non_snake_case)]
770#[track_caller]
771pub fn rememberCameraState() -> State<CameraState> {
772    let updates = rememberEventStream((), |sender| {
773        observe_camera_state(move |state| sender.send(state))
774    });
775    cranpose_core::collectAsState(updates, (), camera_state())
776}
777
778/// The frames the session produces, as a stream this composition collects.
779///
780/// This is the analysis path: a detector collects it, and one that cannot keep
781/// up falls behind by frames rather than by memory — see
782/// [`dropped_camera_frames`]. A viewfinder draws [`latest_camera_frame`]
783/// instead, which costs no recomposition at all.
784#[expect(non_snake_case)]
785#[track_caller]
786pub fn rememberCameraFrames() -> EventStream<CameraFrame> {
787    rememberEventStream((), |sender| {
788        observe_camera_frames(move |frame| sender.send(frame))
789    })
790}
791
792/// The stills the session produces, as a stream this composition collects.
793#[expect(non_snake_case)]
794#[track_caller]
795pub fn rememberCameraStills() -> EventStream<Result<CameraStill, CameraError>> {
796    rememberEventStream((), |sender| {
797        observe_camera_stills(move |still| sender.send(still))
798    })
799}
800
801/// The lens list and the device in use, observed for as long as this call
802/// stays in the composition.
803#[expect(non_snake_case)]
804#[track_caller]
805pub fn rememberCameraLenses() -> State<CameraLenses> {
806    let updates = rememberEventStream((), |sender| {
807        observe_camera_lenses(move |lenses| sender.send(lenses))
808    });
809    cranpose_core::collectAsState(updates, (), camera_lenses())
810}
811
812/// Starts the camera, publishing [`CameraState::Starting`] before the backend
813/// is asked so a screen shows the wait rather than a gap.
814pub fn start_camera() -> Result<(), CameraError> {
815    let Some(camera) = camera() else {
816        publish_camera_state(CameraState::Failed(CameraError::Unsupported));
817        return Err(CameraError::Unsupported);
818    };
819    publish_camera_state(CameraState::Starting);
820    camera.start().inspect_err(|error| {
821        publish_camera_state(CameraState::Failed(error.clone()));
822    })
823}
824
825/// Stops the camera and releases the device.
826pub fn stop_camera() {
827    if let Some(camera) = camera() {
828        camera.stop();
829    }
830    publish_camera_state(CameraState::Stopped);
831}
832
833/// Asks for a full-resolution still, which arrives through
834/// [`rememberCameraStills`].
835pub fn request_camera_still() -> Result<(), CameraError> {
836    let Some(camera) = camera() else {
837        return Err(CameraError::Unsupported);
838    };
839    if !camera_state().is_running() {
840        return Err(CameraError::NotRunning);
841    }
842    camera.request_still()
843}
844
845/// Takes one full-resolution still and resolves with it.
846///
847/// [`request_camera_still`] asks and [`observe_camera_stills`] answers, which is
848/// the right shape for a screen that keeps a shutter open. A caller that wants
849/// one photograph wants the two joined, and joining them by hand means holding
850/// an observer alive across an await in every application that takes a picture.
851///
852/// The observer is dropped as soon as a still arrives, so a second capture is a
853/// second call rather than a subscription to unregister.
854pub async fn capture_camera_still() -> Result<CameraStill, CameraError> {
855    let signal = crate::async_io::Signal::new();
856    let deliver = signal.clone();
857    let observer = observe_camera_stills(move |result| deliver.set(result));
858    request_camera_still()?;
859    let arrived = signal.wait().await;
860    drop(observer);
861    arrived.unwrap_or(Err(CameraError::NotRunning))
862}
863
864#[cfg(test)]
865#[path = "tests/camera_tests.rs"]
866mod tests;