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