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