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    atomic::{AtomicU64, Ordering},
26    Arc, Mutex, OnceLock,
27};
28
29use cranpose_core::{rememberEventStream, EventStream, State};
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            // NV12 needs even dimensions: the chroma plane is half-resolution
63            // in both directions, and an odd row or column has no half.
64            FrameFormat::Nv12 => {
65                if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
66                    return None;
67                }
68                pixels.checked_add(pixels / 2)
69            }
70        }
71    }
72}
73
74/// One frame from the viewfinder.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct CameraFrame {
77    pub width: u32,
78    pub height: u32,
79    pub format: FrameFormat,
80    /// How far the frame must be rotated clockwise to be the right way up,
81    /// which is the sensor's mounting plus the device's rotation.
82    pub rotation_degrees: u16,
83    /// Which frame this is in the session, so a consumer can tell a repeat from
84    /// a new one and count what it missed.
85    pub sequence: u64,
86    /// The pixels, in [`format`](Self::format).
87    pub bytes: Vec<u8>,
88}
89
90impl CameraFrame {
91    /// A frame, or `None` when the bytes do not match the size and format —
92    /// which is a backend bug, and one that reads as corrupted video rather
93    /// than as an error if it is let through.
94    pub fn new(
95        width: u32,
96        height: u32,
97        format: FrameFormat,
98        rotation_degrees: u16,
99        sequence: u64,
100        bytes: Vec<u8>,
101    ) -> Option<Self> {
102        if format.byte_len(width, height)? != bytes.len() {
103            return None;
104        }
105        Some(Self {
106            width,
107            height,
108            format,
109            rotation_degrees: rotation_degrees % 360,
110            sequence,
111            bytes,
112        })
113    }
114
115    /// The frame as tightly packed RGBA8, converting only when it has to.
116    ///
117    /// One pass over the bytes with no allocation beyond the result, because
118    /// this runs per frame: a conversion that allocates per row shows up as
119    /// dropped frames rather than as a slow function.
120    pub fn to_rgba8(&self) -> Vec<u8> {
121        match self.format {
122            FrameFormat::Rgba8 => self.bytes.clone(),
123            FrameFormat::Rgb8 => rgb8_to_rgba8(&self.bytes),
124            FrameFormat::Nv12 => nv12_to_rgba8(self.width, self.height, &self.bytes),
125        }
126    }
127}
128
129/// Widens tightly packed RGB8 to RGBA8, opaque throughout.
130fn rgb8_to_rgba8(bytes: &[u8]) -> Vec<u8> {
131    let mut rgba = Vec::with_capacity(bytes.len() / 3 * 4);
132    // Fixed-size chunks rather than a runtime length: the three reads below
133    // then carry no bounds check, which is what lets this vectorise.
134    for [red, green, blue] in bytes.as_chunks::<3>().0 {
135        rgba.extend_from_slice(&[*red, *green, *blue, 255]);
136    }
137    rgba
138}
139
140/// Converts a full-range NV12 frame to tightly packed RGBA8.
141///
142/// The BT.601 full-range matrix, which is what `ImageFormat.YUV_420_888` and
143/// the equivalent capture formats deliver. Written over whole rows so the
144/// bounds checks fall out of the inner loop and the compiler can vectorise it,
145/// which matters because this runs on every previewed frame.
146fn nv12_to_rgba8(width: u32, height: u32, bytes: &[u8]) -> Vec<u8> {
147    let (width, height) = (width as usize, height as usize);
148    let pixels = width * height;
149    let mut rgba = vec![0u8; pixels * 4];
150    if bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
151        return rgba;
152    }
153    let (luma, chroma) = bytes.split_at(pixels);
154
155    for y in 0..height {
156        let luma_row = &luma[y * width..(y + 1) * width];
157        let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
158        let out_row = &mut rgba[y * width * 4..(y + 1) * width * 4];
159        for x in 0..width {
160            let luminance = luma_row[x] as i32;
161            let blue_difference = chroma_row[x & !1] as i32 - 128;
162            let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
163            let out = &mut out_row[x * 4..x * 4 + 4];
164            out[0] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
165            out[1] =
166                clamp_byte(luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16));
167            out[2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
168            out[3] = 255;
169        }
170    }
171    rgba
172}
173
174fn clamp_byte(value: i32) -> u8 {
175    value.clamp(0, 255) as u8
176}
177
178/// A full-resolution still photograph as an encoded image.
179///
180/// Unlike a viewfinder frame, a still comes through the platform's dedicated
181/// photo pipeline at full sensor resolution. The bytes are an encoded JPEG
182/// whose EXIF orientation tag reflects the device rotation; decode with an
183/// orientation-aware decoder.
184#[derive(Clone, Debug, PartialEq, Eq)]
185pub struct CameraStill {
186    pub jpeg: Vec<u8>,
187}
188
189/// One capture device the application may pick.
190///
191/// `id` is the platform's own handle for the device (an `AVCaptureDevice`
192/// uniqueID on iOS, a camera2 id on Android); pass it back to
193/// [`Camera::use_lens`]. `name` is for a button label: "Ultra wide", "Wide",
194/// "Tele".
195#[derive(Clone, Debug, PartialEq, Eq)]
196pub struct CameraLens {
197    pub id: String,
198    pub name: String,
199}
200
201/// What the light does when a still is captured.
202///
203/// `Auto` leaves the choice to the device's exposure metering. A backend with
204/// no flash reports `false` from [`Camera::set_flash`] and the application
205/// hides the control.
206#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
207pub enum FlashMode {
208    #[default]
209    Off,
210    Auto,
211    On,
212}
213
214#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
215pub enum CameraError {
216    /// No live camera backend on this platform.
217    #[error("live camera capture is not supported here")]
218    Unsupported,
219    /// The user denied camera access.
220    #[error("camera permission denied")]
221    PermissionDenied,
222    /// A still was asked for while nothing was running.
223    #[error("the camera is not running")]
224    NotRunning,
225    /// Any other failure — no device, a configuration the hardware refused.
226    #[error("{0}")]
227    Failed(String),
228}
229
230/// What the camera session is doing.
231#[derive(Clone, Debug, Default, PartialEq, Eq)]
232pub enum CameraState {
233    /// Nothing has been asked of it.
234    #[default]
235    Idle,
236    /// A session is opening. Phones take a noticeable moment over this, which
237    /// is why it is a state a screen can show rather than a gap before frames.
238    Starting,
239    /// Frames are being produced.
240    Running {
241        /// The device the session opened, for a label.
242        device: String,
243    },
244    /// The session was stopped and the device released.
245    Stopped,
246    /// The session could not start, or ended on its own.
247    Failed(CameraError),
248}
249
250impl CameraState {
251    /// Whether frames are being produced now.
252    pub fn is_running(&self) -> bool {
253        matches!(self, CameraState::Running { .. })
254    }
255
256    /// Whether the session is opening or open — which is when a screen keeps
257    /// the viewfinder on screen rather than showing a placeholder.
258    pub fn is_active(&self) -> bool {
259        matches!(self, CameraState::Starting | CameraState::Running { .. })
260    }
261
262    /// The failure, if the session ended in one.
263    pub fn failure(&self) -> Option<&CameraError> {
264        match self {
265            CameraState::Failed(error) => Some(error),
266            _ => None,
267        }
268    }
269}
270
271/// A platform capture session.
272///
273/// A backend starts and stops the device and publishes what it produces through
274/// [`publish_camera_frame`] and [`publish_camera_state`]; nothing here is
275/// polled, and no method blocks for the length of a capture.
276pub trait Camera: Send + Sync {
277    /// Opens the session.
278    ///
279    /// Returns as soon as the request is accepted. The session's progress
280    /// arrives as [`CameraState`], because opening a camera takes long enough
281    /// on a phone that a screen has to show something in the meantime.
282    fn start(&self) -> Result<(), CameraError>;
283
284    /// Stops the session and releases the device.
285    fn stop(&self);
286
287    /// Asks for a full-resolution still.
288    ///
289    /// The picture arrives through [`publish_camera_still`], because the device
290    /// takes as long as it takes to expose and encode, and a call that blocked
291    /// for it would block whatever asked.
292    fn request_still(&self) -> Result<(), CameraError> {
293        Err(CameraError::Unsupported)
294    }
295
296    /// Turns the torch on or off while the session runs.
297    ///
298    /// Scanner-style applications light a dim scene rather than analysing
299    /// photon-starved frames. Returns `false` where the device has no torch;
300    /// the torch dies with the session.
301    fn set_torch(&self, _on: bool) -> bool {
302        false
303    }
304
305    /// The devices the application may pick between, back cameras first and in
306    /// field-of-view order, widest first.
307    ///
308    /// An empty list means the application shows no lens control: either the
309    /// platform has one camera or the backend does not list them.
310    fn lenses(&self) -> Vec<CameraLens> {
311        Vec::new()
312    }
313
314    /// The device the session is using, or `None` when nothing is open and the
315    /// backend has no stored choice.
316    fn lens(&self) -> Option<String> {
317        None
318    }
319
320    /// Opens `id` instead of the current device, keeping the session running.
321    /// Returns `false` when the id is unknown or the backend cannot switch.
322    fn use_lens(&self, _id: &str) -> bool {
323        false
324    }
325
326    /// Whether the current device has a flash for stills.
327    fn has_flash(&self) -> bool {
328        false
329    }
330
331    /// What the flash does on the next still. Returns `false` where the device
332    /// has no flash; the mode dies with the session.
333    fn set_flash(&self, _mode: FlashMode) -> bool {
334        false
335    }
336}
337
338/// Shared handle to the platform camera.
339pub type CameraRef = Arc<dyn Camera>;
340
341static PLATFORM_CAMERA: ServiceRegistry<dyn Camera> = ServiceRegistry::new();
342
343/// Installs the platform camera, replacing any previous one.
344pub fn set_platform_camera(camera: CameraRef) {
345    PLATFORM_CAMERA.set(camera);
346}
347
348/// Removes the platform camera and forgets everything the last session
349/// published.
350pub fn clear_platform_camera() {
351    PLATFORM_CAMERA.clear();
352    if let Ok(mut observers) = frame_observers().lock() {
353        observers.clear();
354    }
355    if let Ok(mut observers) = state_observers().lock() {
356        observers.clear();
357    }
358    if let Ok(mut observers) = still_observers().lock() {
359        observers.clear();
360    }
361    if let Ok(mut latest) = latest_frame_slot().lock() {
362        *latest = None;
363    }
364    if let Ok(mut state) = state_slot().lock() {
365        *state = CameraState::Idle;
366    }
367    DROPPED_FRAMES.store(0, Ordering::Release);
368}
369
370/// The installed camera, or `None` where live capture is unsupported — which is
371/// when an application falls back to the image picker.
372pub fn camera() -> Option<CameraRef> {
373    PLATFORM_CAMERA.get()
374}
375
376/// Whether this platform has a live camera at all.
377pub fn camera_supported() -> bool {
378    PLATFORM_CAMERA.get().is_some()
379}
380
381fn state_slot() -> &'static Mutex<CameraState> {
382    static SLOT: OnceLock<Mutex<CameraState>> = OnceLock::new();
383    SLOT.get_or_init(|| Mutex::new(CameraState::Idle))
384}
385
386fn latest_frame_slot() -> &'static Mutex<Option<CameraFrame>> {
387    static SLOT: OnceLock<Mutex<Option<CameraFrame>>> = OnceLock::new();
388    SLOT.get_or_init(|| Mutex::new(None))
389}
390
391/// Frames produced while every observer was still busy with an earlier one.
392///
393/// Counted rather than queued: a detector that falls behind should see the
394/// scene as it is now and know how much it missed, not work through a backlog
395/// describing a scene that has moved.
396static DROPPED_FRAMES: AtomicU64 = AtomicU64::new(0);
397
398type FrameObserver = Arc<dyn Fn(CameraFrame) + Send + Sync>;
399type StateObserver = Arc<dyn Fn(CameraState) + Send + Sync>;
400type StillObserver = Arc<dyn Fn(Result<CameraStill, CameraError>) + Send + Sync>;
401
402fn frame_observers() -> &'static Mutex<Vec<(u64, FrameObserver)>> {
403    static SLOT: OnceLock<Mutex<Vec<(u64, FrameObserver)>>> = OnceLock::new();
404    SLOT.get_or_init(|| Mutex::new(Vec::new()))
405}
406
407fn state_observers() -> &'static Mutex<Vec<(u64, StateObserver)>> {
408    static SLOT: OnceLock<Mutex<Vec<(u64, StateObserver)>>> = OnceLock::new();
409    SLOT.get_or_init(|| Mutex::new(Vec::new()))
410}
411
412fn still_observers() -> &'static Mutex<Vec<(u64, StillObserver)>> {
413    static SLOT: OnceLock<Mutex<Vec<(u64, StillObserver)>>> = OnceLock::new();
414    SLOT.get_or_init(|| Mutex::new(Vec::new()))
415}
416
417static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
418
419/// Keeps a camera observer registered until it is dropped.
420pub struct CameraObserver {
421    id: u64,
422    kind: ObserverKind,
423}
424
425#[derive(Clone, Copy)]
426enum ObserverKind {
427    Frame,
428    State,
429    Still,
430}
431
432impl Drop for CameraObserver {
433    fn drop(&mut self) {
434        match self.kind {
435            ObserverKind::Frame => retain_without(frame_observers(), self.id),
436            ObserverKind::State => retain_without(state_observers(), self.id),
437            ObserverKind::Still => retain_without(still_observers(), self.id),
438        }
439    }
440}
441
442fn retain_without<T>(slot: &'static Mutex<Vec<(u64, T)>>, id: u64) {
443    if let Ok(mut observers) = slot.lock() {
444        observers.retain(|(observer, _)| *observer != id);
445    }
446}
447
448fn snapshot<T: Clone>(slot: &'static Mutex<Vec<(u64, T)>>) -> Vec<T> {
449    slot.lock()
450        .map(|observers| {
451            observers
452                .iter()
453                .map(|(_, observer)| observer.clone())
454                .collect()
455        })
456        .unwrap_or_default()
457}
458
459/// The last frame the session produced, or `None` before the first one.
460///
461/// Read outside composition — during draw — so a viewfinder shows the newest
462/// frame without a recomposition per frame.
463pub fn latest_camera_frame() -> Option<CameraFrame> {
464    latest_frame_slot()
465        .lock()
466        .map(|frame| frame.clone())
467        .unwrap_or(None)
468}
469
470/// What the session is doing.
471pub fn camera_state() -> CameraState {
472    state_slot()
473        .lock()
474        .map(|state| state.clone())
475        .unwrap_or_default()
476}
477
478/// How many frames were produced while every observer was still busy.
479pub fn dropped_camera_frames() -> u64 {
480    DROPPED_FRAMES.load(Ordering::Acquire)
481}
482
483/// Publishes a frame. Backends call this from whichever thread the platform
484/// delivers frames on.
485///
486/// The newest frame always replaces the stored one, so a viewfinder never draws
487/// a stale frame; observers that are keeping up see every frame, and one that
488/// is not is counted in [`dropped_camera_frames`] rather than queued behind.
489pub fn publish_camera_frame(frame: CameraFrame) {
490    if let Ok(mut latest) = latest_frame_slot().lock() {
491        *latest = Some(frame.clone());
492    }
493    let observers = snapshot(frame_observers());
494    if observers.is_empty() {
495        return;
496    }
497    for observer in observers {
498        observer(frame.clone());
499    }
500}
501
502/// Records that the platform produced a frame nobody could take.
503///
504/// A backend running a bounded analysis queue calls this when it drops one, so
505/// the count reflects what the device produced rather than what got through.
506pub fn record_dropped_camera_frame() {
507    DROPPED_FRAMES.fetch_add(1, Ordering::AcqRel);
508}
509
510/// Publishes what the session is doing.
511pub fn publish_camera_state(state: CameraState) {
512    {
513        let Ok(mut current) = state_slot().lock() else {
514            return;
515        };
516        if *current == state {
517            return;
518        }
519        *current = state.clone();
520    }
521    if matches!(state, CameraState::Idle | CameraState::Starting) {
522        DROPPED_FRAMES.store(0, Ordering::Release);
523        if let Ok(mut latest) = latest_frame_slot().lock() {
524            *latest = None;
525        }
526    }
527    for observer in snapshot(state_observers()) {
528        observer(state.clone());
529    }
530}
531
532/// Publishes the answer to a still request.
533pub fn publish_camera_still(still: Result<CameraStill, CameraError>) {
534    for observer in snapshot(still_observers()) {
535        observer(still.clone());
536    }
537}
538
539/// Registers `observer` for frames. Applications collect
540/// [`rememberCameraFrames`] instead of calling this.
541pub fn observe_camera_frames(
542    observer: impl Fn(CameraFrame) + Send + Sync + 'static,
543) -> CameraObserver {
544    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
545    if let Ok(mut observers) = frame_observers().lock() {
546        observers.push((id, Arc::new(observer)));
547    }
548    CameraObserver {
549        id,
550        kind: ObserverKind::Frame,
551    }
552}
553
554/// Registers `observer` for session state. The current state is delivered at
555/// once, so a screen composed mid-session shows what is happening rather than
556/// waiting for the next change.
557pub fn observe_camera_state(
558    observer: impl Fn(CameraState) + Send + Sync + 'static,
559) -> CameraObserver {
560    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
561    let observer: StateObserver = Arc::new(observer);
562    if let Ok(mut observers) = state_observers().lock() {
563        observers.push((id, Arc::clone(&observer)));
564    }
565    observer(camera_state());
566    CameraObserver {
567        id,
568        kind: ObserverKind::State,
569    }
570}
571
572/// Registers `observer` for stills.
573pub fn observe_camera_stills(
574    observer: impl Fn(Result<CameraStill, CameraError>) + Send + Sync + 'static,
575) -> CameraObserver {
576    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
577    if let Ok(mut observers) = still_observers().lock() {
578        observers.push((id, Arc::new(observer)));
579    }
580    CameraObserver {
581        id,
582        kind: ObserverKind::Still,
583    }
584}
585
586/// What the camera session is doing, observed for as long as this call stays in
587/// the composition.
588#[allow(non_snake_case)]
589pub fn rememberCameraState() -> State<CameraState> {
590    let updates = rememberEventStream((), |sender| {
591        observe_camera_state(move |state| sender.send(state))
592    });
593    cranpose_core::collectAsState(updates, (), camera_state())
594}
595
596/// The frames the session produces, as a stream this composition collects.
597///
598/// This is the analysis path: a detector collects it, and one that cannot keep
599/// up falls behind by frames rather than by memory — see
600/// [`dropped_camera_frames`]. A viewfinder draws [`latest_camera_frame`]
601/// instead, which costs no recomposition at all.
602#[allow(non_snake_case)]
603pub fn rememberCameraFrames() -> EventStream<CameraFrame> {
604    rememberEventStream((), |sender| {
605        observe_camera_frames(move |frame| sender.send(frame))
606    })
607}
608
609/// The stills the session produces, as a stream this composition collects.
610#[allow(non_snake_case)]
611pub fn rememberCameraStills() -> EventStream<Result<CameraStill, CameraError>> {
612    rememberEventStream((), |sender| {
613        observe_camera_stills(move |still| sender.send(still))
614    })
615}
616
617/// Starts the camera, publishing [`CameraState::Starting`] before the backend
618/// is asked so a screen shows the wait rather than a gap.
619pub fn start_camera() -> Result<(), CameraError> {
620    let Some(camera) = camera() else {
621        publish_camera_state(CameraState::Failed(CameraError::Unsupported));
622        return Err(CameraError::Unsupported);
623    };
624    publish_camera_state(CameraState::Starting);
625    camera.start().inspect_err(|error| {
626        publish_camera_state(CameraState::Failed(error.clone()));
627    })
628}
629
630/// Stops the camera and releases the device.
631pub fn stop_camera() {
632    if let Some(camera) = camera() {
633        camera.stop();
634    }
635    publish_camera_state(CameraState::Stopped);
636}
637
638/// Asks for a full-resolution still, which arrives through
639/// [`rememberCameraStills`].
640pub fn request_camera_still() -> Result<(), CameraError> {
641    let Some(camera) = camera() else {
642        return Err(CameraError::Unsupported);
643    };
644    if !camera_state().is_running() {
645        return Err(CameraError::NotRunning);
646    }
647    camera.request_still()
648}
649
650/// Takes one full-resolution still and resolves with it.
651///
652/// [`request_camera_still`] asks and [`observe_camera_stills`] answers, which is
653/// the right shape for a screen that keeps a shutter open. A caller that wants
654/// one photograph wants the two joined, and joining them by hand means holding
655/// an observer alive across an await in every application that takes a picture.
656///
657/// The observer is dropped as soon as a still arrives, so a second capture is a
658/// second call rather than a subscription to unregister.
659pub async fn capture_camera_still() -> Result<CameraStill, CameraError> {
660    let signal = crate::async_io::Signal::new();
661    let deliver = signal.clone();
662    let observer = observe_camera_stills(move |result| deliver.set(result));
663    request_camera_still()?;
664    let arrived = signal.wait().await;
665    drop(observer);
666    // The signal only closes empty if the backend went away mid-capture, which
667    // is a camera that stopped rather than a still that failed.
668    arrived.unwrap_or(Err(CameraError::NotRunning))
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674
675    /// A backend that records what it was asked and publishes what a real one
676    /// would.
677    struct FakeCamera {
678        started: AtomicU64,
679        stopped: AtomicU64,
680        stills: AtomicU64,
681        fails: bool,
682    }
683
684    impl FakeCamera {
685        fn new() -> Arc<Self> {
686            Arc::new(Self {
687                started: AtomicU64::new(0),
688                stopped: AtomicU64::new(0),
689                stills: AtomicU64::new(0),
690                fails: false,
691            })
692        }
693
694        fn failing() -> Arc<Self> {
695            Arc::new(Self {
696                started: AtomicU64::new(0),
697                stopped: AtomicU64::new(0),
698                stills: AtomicU64::new(0),
699                fails: true,
700            })
701        }
702    }
703
704    impl Camera for FakeCamera {
705        fn start(&self) -> Result<(), CameraError> {
706            self.started.fetch_add(1, Ordering::Relaxed);
707            if self.fails {
708                return Err(CameraError::PermissionDenied);
709            }
710            publish_camera_state(CameraState::Running {
711                device: "fake".to_string(),
712            });
713            Ok(())
714        }
715
716        fn stop(&self) {
717            self.stopped.fetch_add(1, Ordering::Relaxed);
718        }
719
720        fn request_still(&self) -> Result<(), CameraError> {
721            self.stills.fetch_add(1, Ordering::Relaxed);
722            publish_camera_still(Ok(CameraStill {
723                jpeg: vec![0xff, 0xd8],
724            }));
725            Ok(())
726        }
727    }
728
729    fn rgba_frame(sequence: u64) -> CameraFrame {
730        CameraFrame::new(2, 2, FrameFormat::Rgba8, 90, sequence, vec![7; 16])
731            .expect("a well-formed frame")
732    }
733
734    #[test]
735    fn a_frame_size_is_the_one_its_format_implies() {
736        assert_eq!(FrameFormat::Rgba8.byte_len(4, 2), Some(32));
737        assert_eq!(FrameFormat::Nv12.byte_len(4, 2), Some(12));
738        assert_eq!(
739            FrameFormat::Nv12.byte_len(3, 2),
740            None,
741            "NV12 has no half column for an odd width"
742        );
743        assert_eq!(FrameFormat::Nv12.byte_len(4, 3), None);
744    }
745
746    /// A frame whose bytes do not match its size reads as corrupted video
747    /// rather than as an error, so it is refused where it enters.
748    #[test]
749    fn a_frame_that_does_not_match_its_size_is_refused() {
750        assert!(CameraFrame::new(2, 2, FrameFormat::Rgba8, 0, 0, vec![0; 15]).is_none());
751        assert!(CameraFrame::new(2, 2, FrameFormat::Nv12, 0, 0, vec![0; 5]).is_none());
752        assert!(CameraFrame::new(2, 2, FrameFormat::Rgba8, 0, 0, vec![0; 16]).is_some());
753    }
754
755    #[test]
756    fn a_rotation_is_kept_inside_one_turn() {
757        let frame = CameraFrame::new(2, 2, FrameFormat::Rgba8, 450, 0, vec![0; 16])
758            .expect("a well-formed frame");
759        assert_eq!(frame.rotation_degrees, 90);
760    }
761
762    #[test]
763    fn an_rgba_frame_is_handed_over_unchanged() {
764        let frame = rgba_frame(0);
765        assert_eq!(frame.to_rgba8(), frame.bytes);
766    }
767
768    /// The conversion has to agree with what every other BT.601 full-range
769    /// implementation produces, or the viewfinder is a different colour from
770    /// the photo the same camera takes.
771    #[test]
772    fn nv12_black_white_and_primaries_convert_to_the_expected_colours() {
773        fn convert(luma: u8, blue: u8, red: u8) -> [u8; 4] {
774            let bytes = vec![luma, luma, luma, luma, blue, red];
775            let frame = CameraFrame::new(2, 2, FrameFormat::Nv12, 0, 0, bytes)
776                .expect("a well-formed frame");
777            let rgba = frame.to_rgba8();
778            [rgba[0], rgba[1], rgba[2], rgba[3]]
779        }
780
781        assert_eq!(convert(0, 128, 128), [0, 0, 0, 255], "black");
782        assert_eq!(convert(255, 128, 128), [255, 255, 255, 255], "white");
783
784        let red = convert(76, 84, 255);
785        assert!(
786            red[0] > 240 && red[1] < 20 && red[2] < 20,
787            "red, got {red:?}"
788        );
789        let blue = convert(29, 255, 107);
790        assert!(
791            blue[2] > 240 && blue[0] < 20 && blue[1] < 20,
792            "blue, got {blue:?}"
793        );
794        assert!(
795            convert(128, 128, 128).iter().all(|value| *value > 0),
796            "a grey frame must not clamp to black"
797        );
798    }
799
800    #[test]
801    fn a_short_nv12_frame_converts_to_black_rather_than_reading_past_its_end() {
802        let rgba = nv12_to_rgba8(4, 4, &[0u8; 3]);
803        assert_eq!(rgba.len(), 4 * 4 * 4);
804        assert!(rgba.iter().all(|value| *value == 0));
805    }
806
807    #[test]
808    fn a_platform_without_a_camera_says_so_rather_than_pretending() {
809        let _guard = crate::registry::test_service_guard();
810        clear_platform_camera();
811        assert!(!camera_supported());
812        assert_eq!(start_camera(), Err(CameraError::Unsupported));
813        assert_eq!(
814            camera_state(),
815            CameraState::Failed(CameraError::Unsupported)
816        );
817        assert_eq!(request_camera_still(), Err(CameraError::Unsupported));
818        clear_platform_camera();
819    }
820
821    /// Opening a camera takes long enough on a phone that a screen has to show
822    /// something in the meantime, so the wait is a state and not a gap.
823    #[test]
824    fn the_session_reports_starting_before_it_reports_running() {
825        let _guard = crate::registry::test_service_guard();
826        clear_platform_camera();
827        let seen = Arc::new(Mutex::new(Vec::new()));
828        let recorder = Arc::clone(&seen);
829        let observer = observe_camera_state(move |state| {
830            recorder
831                .lock()
832                .unwrap_or_else(|error| error.into_inner())
833                .push(state)
834        });
835        set_platform_camera(FakeCamera::new());
836        start_camera().expect("the session starts");
837
838        assert_eq!(
839            *seen.lock().unwrap_or_else(|error| error.into_inner()),
840            vec![
841                CameraState::Idle,
842                CameraState::Starting,
843                CameraState::Running {
844                    device: "fake".to_string()
845                }
846            ]
847        );
848        assert!(camera_state().is_running());
849        assert!(camera_state().is_active());
850        drop(observer);
851        clear_platform_camera();
852    }
853
854    #[test]
855    fn a_session_that_cannot_open_reports_why() {
856        let _guard = crate::registry::test_service_guard();
857        clear_platform_camera();
858        set_platform_camera(FakeCamera::failing());
859        assert_eq!(start_camera(), Err(CameraError::PermissionDenied));
860        assert_eq!(
861            camera_state().failure(),
862            Some(&CameraError::PermissionDenied)
863        );
864        assert!(!camera_state().is_active());
865        clear_platform_camera();
866    }
867
868    #[test]
869    fn stopping_releases_the_device_and_says_so() {
870        let _guard = crate::registry::test_service_guard();
871        clear_platform_camera();
872        let backend = FakeCamera::new();
873        set_platform_camera(backend.clone());
874        start_camera().expect("the session starts");
875        stop_camera();
876        assert_eq!(backend.stopped.load(Ordering::Relaxed), 1);
877        assert_eq!(camera_state(), CameraState::Stopped);
878        clear_platform_camera();
879    }
880
881    /// A viewfinder draws the newest frame, so the stored one is always the
882    /// newest — never a queue a slow drawer works through.
883    #[test]
884    fn the_stored_frame_is_always_the_newest_one() {
885        let _guard = crate::registry::test_service_guard();
886        clear_platform_camera();
887        assert_eq!(latest_camera_frame(), None);
888        publish_camera_frame(rgba_frame(1));
889        publish_camera_frame(rgba_frame(2));
890        publish_camera_frame(rgba_frame(3));
891        assert_eq!(latest_camera_frame().map(|frame| frame.sequence), Some(3));
892        clear_platform_camera();
893    }
894
895    #[test]
896    fn frame_observers_see_frames_and_stop_when_dropped() {
897        let _guard = crate::registry::test_service_guard();
898        clear_platform_camera();
899        let seen = Arc::new(Mutex::new(Vec::new()));
900        let recorder = Arc::clone(&seen);
901        let observer = observe_camera_frames(move |frame| {
902            recorder
903                .lock()
904                .unwrap_or_else(|error| error.into_inner())
905                .push(frame.sequence)
906        });
907        publish_camera_frame(rgba_frame(1));
908        publish_camera_frame(rgba_frame(2));
909        drop(observer);
910        publish_camera_frame(rgba_frame(3));
911        assert_eq!(
912            *seen.lock().unwrap_or_else(|error| error.into_inner()),
913            vec![1, 2]
914        );
915        clear_platform_camera();
916    }
917
918    /// A detector that cannot keep up must fall behind by frames, and the
919    /// framework has to be able to say how far.
920    #[test]
921    fn frames_the_platform_could_not_deliver_are_counted_rather_than_queued() {
922        let _guard = crate::registry::test_service_guard();
923        clear_platform_camera();
924        assert_eq!(dropped_camera_frames(), 0);
925        record_dropped_camera_frame();
926        record_dropped_camera_frame();
927        assert_eq!(dropped_camera_frames(), 2);
928        // A new session starts the count and the stored frame over: what the
929        // last one missed says nothing about this one.
930        publish_camera_state(CameraState::Starting);
931        assert_eq!(dropped_camera_frames(), 0);
932        assert_eq!(latest_camera_frame(), None);
933        clear_platform_camera();
934    }
935
936    /// A still takes as long as the device takes; asking must not block
937    /// whatever asked.
938    #[test]
939    fn a_still_is_asked_for_and_arrives_separately() {
940        let _guard = crate::registry::test_service_guard();
941        clear_platform_camera();
942        let backend = FakeCamera::new();
943        set_platform_camera(backend.clone());
944
945        assert_eq!(
946            request_camera_still(),
947            Err(CameraError::NotRunning),
948            "nothing is running, so there is nothing to photograph"
949        );
950
951        let seen = Arc::new(Mutex::new(Vec::new()));
952        let recorder = Arc::clone(&seen);
953        let observer = observe_camera_stills(move |still| {
954            recorder
955                .lock()
956                .unwrap_or_else(|error| error.into_inner())
957                .push(still)
958        });
959        start_camera().expect("the session starts");
960        request_camera_still().expect("a still is asked for");
961        assert_eq!(backend.stills.load(Ordering::Relaxed), 1);
962        assert_eq!(
963            *seen.lock().unwrap_or_else(|error| error.into_inner()),
964            vec![Ok(CameraStill {
965                jpeg: vec![0xff, 0xd8]
966            })]
967        );
968        drop(observer);
969        clear_platform_camera();
970    }
971
972    #[test]
973    fn a_backend_that_lists_no_lens_and_no_flash_says_so() {
974        let _guard = crate::registry::test_service_guard();
975        clear_platform_camera();
976        set_platform_camera(FakeCamera::new());
977        let backend = camera().expect("registered");
978        assert!(backend.lenses().is_empty());
979        assert_eq!(backend.lens(), None);
980        assert!(!backend.use_lens("0"));
981        assert!(!backend.has_flash());
982        assert!(!backend.set_flash(FlashMode::On));
983        assert!(!backend.set_torch(true));
984        clear_platform_camera();
985    }
986
987    #[test]
988    fn a_backend_that_lists_two_lenses_hands_them_over_in_order() {
989        let _guard = crate::registry::test_service_guard();
990        clear_platform_camera();
991        struct TwoLenses;
992        impl Camera for TwoLenses {
993            fn start(&self) -> Result<(), CameraError> {
994                Ok(())
995            }
996            fn stop(&self) {}
997            fn lenses(&self) -> Vec<CameraLens> {
998                vec![
999                    CameraLens {
1000                        id: "u".into(),
1001                        name: "Ultra wide".into(),
1002                    },
1003                    CameraLens {
1004                        id: "w".into(),
1005                        name: "Wide".into(),
1006                    },
1007                ]
1008            }
1009            fn lens(&self) -> Option<String> {
1010                Some("w".into())
1011            }
1012            fn use_lens(&self, id: &str) -> bool {
1013                id == "u" || id == "w"
1014            }
1015        }
1016        set_platform_camera(Arc::new(TwoLenses));
1017        let backend = camera().expect("registered");
1018        let lenses = backend.lenses();
1019        assert_eq!(lenses.len(), 2);
1020        assert_eq!(lenses[0].name, "Ultra wide");
1021        assert_eq!(backend.lens().as_deref(), Some("w"));
1022        assert!(backend.use_lens("u"));
1023        assert!(!backend.use_lens("tele"));
1024        clear_platform_camera();
1025    }
1026}