Skip to main content

cranpose_services/
camera.rs

1//! The live camera: what it is doing, the frames it is producing, and the
2//! stills it takes.
3//!
4//! Three things about a camera are easy to get wrong and expensive to get wrong
5//! twice, so the framework owns all three.
6//!
7//! A camera is **observable, not polled**. A viewfinder that asks "is there a
8//! new frame yet?" every frame does that work whether or not one arrived, and
9//! learns about a failure only by noticing that frames stopped. Here the
10//! session publishes what it is doing and the frames it produces, and a screen
11//! reacts.
12//!
13//! Frames arrive in the **format the sensor produced**. Encoding a preview
14//! frame as JPEG to hand it across a language boundary and decoding it back
15//! costs several milliseconds per frame, every frame, to arrive at the pixels
16//! the camera already had. [`FrameFormat::Nv12`] is what a phone camera
17//! actually produces, and it converts to RGBA in one pass over the bytes.
18//!
19//! Analysis is **bounded and latest-wins**. A detector that takes longer than a
20//! frame interval must fall behind rather than accumulate: a queue of stale
21//! frames costs memory to hold and produces answers about a scene that has
22//! already moved.
23
24use std::sync::{
25    Arc, Mutex, OnceLock,
26    atomic::{AtomicU64, Ordering},
27};
28
29use cranpose_core::{EventStream, State, rememberEventStream};
30
31use crate::registry::ServiceRegistry;
32
33/// How a frame's pixels are laid out.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum FrameFormat {
36    /// Tightly packed RGBA8, row-major, no padding.
37    #[default]
38    Rgba8,
39    /// Tightly packed RGB8, row-major, no padding.
40    ///
41    /// What a USB webcam decodes to. Carried as-is so a desktop viewfinder does
42    /// not pay a widening copy per frame for an alpha channel a camera never
43    /// has.
44    Rgb8,
45    /// Full-range NV12: a `width * height` luma plane followed by an
46    /// interleaved `width * height / 2` chroma plane at half resolution in both
47    /// directions.
48    ///
49    /// What a phone camera produces. Carrying it as-is is what removes the
50    /// encode-and-decode round trip a JPEG preview pays on every frame.
51    Nv12,
52}
53
54impl FrameFormat {
55    /// How many bytes a frame of this size occupies, or `None` when the size
56    /// cannot be represented — which is a frame nobody can allocate anyway.
57    pub fn byte_len(self, width: u32, height: u32) -> Option<usize> {
58        let pixels = (width as usize).checked_mul(height as usize)?;
59        match self {
60            FrameFormat::Rgba8 => pixels.checked_mul(4),
61            FrameFormat::Rgb8 => pixels.checked_mul(3),
62            // 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    /// The frame as tightly packed RGBA8 with
129    /// [`rotation_degrees`](Self::rotation_degrees) applied, so the pixels are
130    /// the right way up whatever the sensor's mounting was.
131    ///
132    /// The turn happens in the same pass as the format conversion, because this
133    /// runs on every previewed frame: converting and then turning would walk
134    /// the pixels twice. A rotation that is not a quarter turn is left alone —
135    /// no camera produces one.
136    pub fn upright_rgba8(&self) -> UprightRgba {
137        let rotation = self.rotation_degrees;
138        if !matches!(rotation, 90 | 180 | 270) {
139            return UprightRgba {
140                width: self.width,
141                height: self.height,
142                rgba: self.to_rgba8(),
143            };
144        }
145        let (width, height) = (self.width as usize, self.height as usize);
146        let (out_width, out_height) = match rotation {
147            90 | 270 => (self.height, self.width),
148            _ => (self.width, self.height),
149        };
150        let mut rgba = vec![0u8; width * height * 4];
151        match self.format {
152            FrameFormat::Rgba8 => {
153                for y in 0..height {
154                    let row = &self.bytes[y * width * 4..(y + 1) * width * 4];
155                    for x in 0..width {
156                        let src = &row[x * 4..x * 4 + 4];
157                        let dst = turned_index(rotation, width, height, x, y) * 4;
158                        rgba[dst..dst + 4].copy_from_slice(src);
159                    }
160                }
161            }
162            FrameFormat::Rgb8 => {
163                for y in 0..height {
164                    let row = &self.bytes[y * width * 3..(y + 1) * width * 3];
165                    for x in 0..width {
166                        let src = &row[x * 3..x * 3 + 3];
167                        let dst = turned_index(rotation, width, height, x, y) * 4;
168                        rgba[dst..dst + 3].copy_from_slice(src);
169                        rgba[dst + 3] = 255;
170                    }
171                }
172            }
173            FrameFormat::Nv12 => {
174                let pixels = width * height;
175                if self.bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
176                    return UprightRgba {
177                        width: out_width,
178                        height: out_height,
179                        rgba,
180                    };
181                }
182                let (luma, chroma) = self.bytes.split_at(pixels);
183                for y in 0..height {
184                    let luma_row = &luma[y * width..(y + 1) * width];
185                    let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
186                    for x in 0..width {
187                        let luminance = luma_row[x] as i32;
188                        let blue_difference = chroma_row[x & !1] as i32 - 128;
189                        let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
190                        let dst = turned_index(rotation, width, height, x, y) * 4;
191                        rgba[dst] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
192                        rgba[dst + 1] = clamp_byte(
193                            luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16),
194                        );
195                        rgba[dst + 2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
196                        rgba[dst + 3] = 255;
197                    }
198                }
199            }
200        }
201        UprightRgba {
202            width: out_width,
203            height: out_height,
204            rgba,
205        }
206    }
207}
208
209/// A frame's pixels as tightly packed RGBA8, already the right way up.
210///
211/// `width` and `height` describe the turned image, so a 90° or 270° turn swaps
212/// them relative to the frame that produced this.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub struct UprightRgba {
215    pub width: u32,
216    pub height: u32,
217    pub rgba: Vec<u8>,
218}
219
220/// Where source pixel `(x, y)` lands in the image turned clockwise by
221/// `rotation`, as a pixel index into the turned image.
222#[inline]
223fn turned_index(rotation: u16, width: usize, height: usize, x: usize, y: usize) -> usize {
224    match rotation {
225        90 => x * height + (height - 1 - y),
226        180 => (height - 1 - y) * width + (width - 1 - x),
227        270 => (width - 1 - x) * height + y,
228        _ => y * width + x,
229    }
230}
231
232/// Widens tightly packed RGB8 to RGBA8, opaque throughout.
233fn rgb8_to_rgba8(bytes: &[u8]) -> Vec<u8> {
234    let mut rgba = Vec::with_capacity(bytes.len() / 3 * 4);
235    // Fixed-size chunks rather than a runtime length: the three reads below
236    // then carry no bounds check, which is what lets this vectorise.
237    for [red, green, blue] in bytes.as_chunks::<3>().0 {
238        rgba.extend_from_slice(&[*red, *green, *blue, 255]);
239    }
240    rgba
241}
242
243/// Converts a full-range NV12 frame to tightly packed RGBA8.
244///
245/// The BT.601 full-range matrix, which is what `ImageFormat.YUV_420_888` and
246/// the equivalent capture formats deliver. Written over whole rows so the
247/// bounds checks fall out of the inner loop and the compiler can vectorise it,
248/// which matters because this runs on every previewed frame.
249fn nv12_to_rgba8(width: u32, height: u32, bytes: &[u8]) -> Vec<u8> {
250    let (width, height) = (width as usize, height as usize);
251    let pixels = width * height;
252    let mut rgba = vec![0u8; pixels * 4];
253    if bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
254        return rgba;
255    }
256    let (luma, chroma) = bytes.split_at(pixels);
257
258    for y in 0..height {
259        let luma_row = &luma[y * width..(y + 1) * width];
260        let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
261        let out_row = &mut rgba[y * width * 4..(y + 1) * width * 4];
262        for x in 0..width {
263            let luminance = luma_row[x] as i32;
264            let blue_difference = chroma_row[x & !1] as i32 - 128;
265            let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
266            let out = &mut out_row[x * 4..x * 4 + 4];
267            out[0] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
268            out[1] =
269                clamp_byte(luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16));
270            out[2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
271            out[3] = 255;
272        }
273    }
274    rgba
275}
276
277fn clamp_byte(value: i32) -> u8 {
278    value.clamp(0, 255) as u8
279}
280
281/// A full-resolution still photograph as an encoded image.
282///
283/// Unlike a viewfinder frame, a still comes through the platform's dedicated
284/// photo pipeline at full sensor resolution. The bytes are an encoded JPEG
285/// whose EXIF orientation tag reflects the device rotation; decode with an
286/// orientation-aware decoder.
287#[derive(Clone, Debug, PartialEq, Eq)]
288pub struct CameraStill {
289    pub jpeg: Vec<u8>,
290}
291
292/// One capture device the application may pick.
293///
294/// `id` is the platform's own handle for the device (an `AVCaptureDevice`
295/// uniqueID on iOS, a camera2 id on Android, a device index on desktop); pass
296/// it back to [`Camera::use_lens`]. `name` is for a button label: "Ultra
297/// wide", "Wide", "Tele".
298#[derive(Clone, Debug, PartialEq, Eq)]
299pub struct CameraLens {
300    pub id: String,
301    pub name: String,
302    /// Which way the device points, so an application can offer back lenses
303    /// and the front lens as different controls.
304    pub facing: LensFacing,
305}
306
307/// Which way a capture device points.
308#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
309pub enum LensFacing {
310    /// Away from the screen: the main photography cameras on a phone.
311    #[default]
312    Back,
313    /// At the person holding the device.
314    Front,
315    /// Not fixed to a screen at all: a webcam or another attached device.
316    External,
317}
318
319/// The devices the application may pick between, and the one in use.
320///
321/// Published by the backend when a session opens and when the device changes.
322/// A screen showing a lens control observes this instead of asking the
323/// platform, because both phone lens lists are blocking platform calls — a
324/// JNI round trip on Android, a fresh discovery session on iOS — and a
325/// recomposition must not pay that.
326#[derive(Clone, Debug, Default, PartialEq, Eq)]
327pub struct CameraLenses {
328    /// Back lenses first in field-of-view order, widest first, then the rest.
329    pub lenses: Vec<CameraLens>,
330    /// The id of the device the open session uses, or `None` while nothing
331    /// runs.
332    pub active: Option<String>,
333}
334
335/// What the light does when a still is captured.
336///
337/// `Auto` leaves the choice to the device's exposure metering. A backend with
338/// no flash reports `false` from [`Camera::set_flash`] and the application
339/// hides the control.
340#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
341pub enum FlashMode {
342    #[default]
343    Off,
344    Auto,
345    On,
346}
347
348#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
349pub enum CameraError {
350    /// No live camera backend on this platform.
351    #[error("live camera capture is not supported here")]
352    Unsupported,
353    /// The user denied camera access.
354    #[error("camera permission denied")]
355    PermissionDenied,
356    /// A still was asked for while nothing was running.
357    #[error("the camera is not running")]
358    NotRunning,
359    /// Any other failure — no device, a configuration the hardware refused.
360    #[error("{0}")]
361    Failed(String),
362}
363
364/// What the camera session is doing.
365#[derive(Clone, Debug, Default, PartialEq, Eq)]
366pub enum CameraState {
367    /// Nothing has been asked of it.
368    #[default]
369    Idle,
370    /// A session is opening. Phones take a noticeable moment over this, which
371    /// is why it is a state a screen can show rather than a gap before frames.
372    Starting,
373    /// Frames are being produced.
374    Running {
375        /// The device the session opened, for a label.
376        device: String,
377    },
378    /// The session was stopped and the device released.
379    Stopped,
380    /// The session could not start, or ended on its own.
381    Failed(CameraError),
382}
383
384impl CameraState {
385    /// Whether frames are being produced now.
386    pub fn is_running(&self) -> bool {
387        matches!(self, CameraState::Running { .. })
388    }
389
390    /// Whether the session is opening or open — which is when a screen keeps
391    /// the viewfinder on screen rather than showing a placeholder.
392    pub fn is_active(&self) -> bool {
393        matches!(self, CameraState::Starting | CameraState::Running { .. })
394    }
395
396    /// The failure, if the session ended in one.
397    pub fn failure(&self) -> Option<&CameraError> {
398        match self {
399            CameraState::Failed(error) => Some(error),
400            _ => None,
401        }
402    }
403}
404
405/// A platform capture session.
406///
407/// A backend starts and stops the device and publishes what it produces through
408/// [`publish_camera_frame`] and [`publish_camera_state`]; nothing here is
409/// polled, and no method blocks for the length of a capture.
410pub trait Camera: Send + Sync {
411    /// Opens the session.
412    ///
413    /// Returns as soon as the request is accepted. The session's progress
414    /// arrives as [`CameraState`], because opening a camera takes long enough
415    /// on a phone that a screen has to show something in the meantime.
416    fn start(&self) -> Result<(), CameraError>;
417
418    /// Stops the session and releases the device.
419    fn stop(&self);
420
421    /// Asks for a full-resolution still.
422    ///
423    /// The picture arrives through [`publish_camera_still`], because the device
424    /// takes as long as it takes to expose and encode, and a call that blocked
425    /// for it would block whatever asked.
426    fn request_still(&self) -> Result<(), CameraError> {
427        Err(CameraError::Unsupported)
428    }
429
430    /// Turns the torch on or off while the session runs.
431    ///
432    /// Scanner-style applications light a dim scene rather than analysing
433    /// photon-starved frames. Returns `false` where the device has no torch;
434    /// the torch dies with the session.
435    fn set_torch(&self, _on: bool) -> bool {
436        false
437    }
438
439    /// The devices the application may pick between, back cameras first and in
440    /// field-of-view order, widest first, then the rest.
441    ///
442    /// An empty list means the application shows no lens control: either the
443    /// platform has one camera or the backend does not list them. Backends
444    /// also publish this through [`publish_camera_lenses`] when a session
445    /// opens, so a screen observes [`rememberCameraLenses`] rather than paying
446    /// this blocking platform call per recomposition.
447    fn lenses(&self) -> Vec<CameraLens> {
448        Vec::new()
449    }
450
451    /// The device the session is using, or `None` when nothing is open and the
452    /// backend has no stored choice.
453    fn lens(&self) -> Option<String> {
454        None
455    }
456
457    /// Opens `id` instead of the current device, keeping the session running.
458    /// Returns `false` when the id is unknown or the backend cannot switch.
459    fn use_lens(&self, _id: &str) -> bool {
460        false
461    }
462
463    /// Whether the current device has a flash for stills.
464    fn has_flash(&self) -> bool {
465        false
466    }
467
468    /// What the flash does on the next still. Returns `false` where the device
469    /// has no flash; the mode dies with the session.
470    fn set_flash(&self, _mode: FlashMode) -> bool {
471        false
472    }
473}
474
475/// Shared handle to the platform camera.
476pub type CameraRef = Arc<dyn Camera>;
477
478static PLATFORM_CAMERA: ServiceRegistry<dyn Camera> = ServiceRegistry::new();
479
480/// Installs the platform camera, replacing any previous one.
481pub fn set_platform_camera(camera: CameraRef) {
482    PLATFORM_CAMERA.set(camera);
483}
484
485/// Removes the platform camera and forgets everything the last session
486/// published.
487pub fn clear_platform_camera() {
488    PLATFORM_CAMERA.clear();
489    if let Ok(mut observers) = frame_observers().lock() {
490        observers.clear();
491    }
492    if let Ok(mut observers) = state_observers().lock() {
493        observers.clear();
494    }
495    if let Ok(mut observers) = still_observers().lock() {
496        observers.clear();
497    }
498    if let Ok(mut observers) = lens_observers().lock() {
499        observers.clear();
500    }
501    if let Ok(mut latest) = latest_frame_slot().lock() {
502        *latest = None;
503    }
504    if let Ok(mut state) = state_slot().lock() {
505        *state = CameraState::Idle;
506    }
507    if let Ok(mut lenses) = lenses_slot().lock() {
508        *lenses = CameraLenses::default();
509    }
510    DROPPED_FRAMES.store(0, Ordering::Release);
511}
512
513/// The installed camera, or `None` where live capture is unsupported — which is
514/// when an application falls back to the image picker.
515pub fn camera() -> Option<CameraRef> {
516    PLATFORM_CAMERA.get()
517}
518
519/// Whether this platform has a live camera at all.
520pub fn camera_supported() -> bool {
521    PLATFORM_CAMERA.get().is_some()
522}
523
524fn state_slot() -> &'static Mutex<CameraState> {
525    static SLOT: OnceLock<Mutex<CameraState>> = OnceLock::new();
526    SLOT.get_or_init(|| Mutex::new(CameraState::Idle))
527}
528
529fn latest_frame_slot() -> &'static Mutex<Option<CameraFrame>> {
530    static SLOT: OnceLock<Mutex<Option<CameraFrame>>> = OnceLock::new();
531    SLOT.get_or_init(|| Mutex::new(None))
532}
533
534fn lenses_slot() -> &'static Mutex<CameraLenses> {
535    static SLOT: OnceLock<Mutex<CameraLenses>> = OnceLock::new();
536    SLOT.get_or_init(|| Mutex::new(CameraLenses::default()))
537}
538
539/// Frames produced while every observer was still busy with an earlier one.
540///
541/// Counted rather than queued: a detector that falls behind should see the
542/// scene as it is now and know how much it missed, not work through a backlog
543/// describing a scene that has moved.
544static DROPPED_FRAMES: AtomicU64 = AtomicU64::new(0);
545
546type FrameObserver = Arc<dyn Fn(CameraFrame) + Send + Sync>;
547type StateObserver = Arc<dyn Fn(CameraState) + Send + Sync>;
548type StillObserver = Arc<dyn Fn(Result<CameraStill, CameraError>) + Send + Sync>;
549type LensObserver = Arc<dyn Fn(CameraLenses) + Send + Sync>;
550
551fn frame_observers() -> &'static Mutex<Vec<(u64, FrameObserver)>> {
552    static SLOT: OnceLock<Mutex<Vec<(u64, FrameObserver)>>> = OnceLock::new();
553    SLOT.get_or_init(|| Mutex::new(Vec::new()))
554}
555
556fn state_observers() -> &'static Mutex<Vec<(u64, StateObserver)>> {
557    static SLOT: OnceLock<Mutex<Vec<(u64, StateObserver)>>> = OnceLock::new();
558    SLOT.get_or_init(|| Mutex::new(Vec::new()))
559}
560
561fn still_observers() -> &'static Mutex<Vec<(u64, StillObserver)>> {
562    static SLOT: OnceLock<Mutex<Vec<(u64, StillObserver)>>> = OnceLock::new();
563    SLOT.get_or_init(|| Mutex::new(Vec::new()))
564}
565
566fn lens_observers() -> &'static Mutex<Vec<(u64, LensObserver)>> {
567    static SLOT: OnceLock<Mutex<Vec<(u64, LensObserver)>>> = OnceLock::new();
568    SLOT.get_or_init(|| Mutex::new(Vec::new()))
569}
570
571static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
572
573/// Keeps a camera observer registered until it is dropped.
574pub struct CameraObserver {
575    id: u64,
576    kind: ObserverKind,
577}
578
579#[derive(Clone, Copy)]
580enum ObserverKind {
581    Frame,
582    State,
583    Still,
584    Lenses,
585}
586
587impl Drop for CameraObserver {
588    fn drop(&mut self) {
589        match self.kind {
590            ObserverKind::Frame => retain_without(frame_observers(), self.id),
591            ObserverKind::State => retain_without(state_observers(), self.id),
592            ObserverKind::Still => retain_without(still_observers(), self.id),
593            ObserverKind::Lenses => retain_without(lens_observers(), self.id),
594        }
595    }
596}
597
598fn retain_without<T>(slot: &'static Mutex<Vec<(u64, T)>>, id: u64) {
599    if let Ok(mut observers) = slot.lock() {
600        observers.retain(|(observer, _)| *observer != id);
601    }
602}
603
604fn snapshot<T: Clone>(slot: &'static Mutex<Vec<(u64, T)>>) -> Vec<T> {
605    slot.lock()
606        .map(|observers| {
607            observers
608                .iter()
609                .map(|(_, observer)| observer.clone())
610                .collect()
611        })
612        .unwrap_or_default()
613}
614
615/// The last frame the session produced, or `None` before the first one.
616///
617/// Read outside composition — during draw — so a viewfinder shows the newest
618/// frame without a recomposition per frame.
619pub fn latest_camera_frame() -> Option<CameraFrame> {
620    latest_frame_slot()
621        .lock()
622        .map(|frame| frame.clone())
623        .unwrap_or(None)
624}
625
626/// What the session is doing.
627pub fn camera_state() -> CameraState {
628    state_slot()
629        .lock()
630        .map(|state| state.clone())
631        .unwrap_or_default()
632}
633
634/// How many frames were produced while every observer was still busy.
635pub fn dropped_camera_frames() -> u64 {
636    DROPPED_FRAMES.load(Ordering::Acquire)
637}
638
639/// Publishes a frame. Backends call this from whichever thread the platform
640/// delivers frames on.
641///
642/// The newest frame always replaces the stored one, so a viewfinder never draws
643/// a stale frame; observers that are keeping up see every frame, and one that
644/// is not is counted in [`dropped_camera_frames`] rather than queued behind.
645pub fn publish_camera_frame(frame: CameraFrame) {
646    if let Ok(mut latest) = latest_frame_slot().lock() {
647        *latest = Some(frame.clone());
648    }
649    let observers = snapshot(frame_observers());
650    if observers.is_empty() {
651        return;
652    }
653    for observer in observers {
654        observer(frame.clone());
655    }
656}
657
658/// Records that the platform produced a frame nobody could take.
659///
660/// A backend running a bounded analysis queue calls this when it drops one, so
661/// the count reflects what the device produced rather than what got through.
662pub fn record_dropped_camera_frame() {
663    DROPPED_FRAMES.fetch_add(1, Ordering::AcqRel);
664}
665
666/// Publishes what the session is doing.
667pub fn publish_camera_state(state: CameraState) {
668    {
669        let Ok(mut current) = state_slot().lock() else {
670            return;
671        };
672        if *current == state {
673            return;
674        }
675        *current = state.clone();
676    }
677    if matches!(state, CameraState::Idle | CameraState::Starting) {
678        DROPPED_FRAMES.store(0, Ordering::Release);
679        if let Ok(mut latest) = latest_frame_slot().lock() {
680            *latest = None;
681        }
682    }
683    for observer in snapshot(state_observers()) {
684        observer(state.clone());
685    }
686}
687
688/// Publishes the answer to a still request.
689pub fn publish_camera_still(still: Result<CameraStill, CameraError>) {
690    for observer in snapshot(still_observers()) {
691        observer(still.clone());
692    }
693}
694
695/// Publishes the lens list and the device in use. Backends call this when a
696/// session opens and when the device changes.
697pub fn publish_camera_lenses(lenses: CameraLenses) {
698    {
699        let Ok(mut current) = lenses_slot().lock() else {
700            return;
701        };
702        if *current == lenses {
703            return;
704        }
705        *current = lenses.clone();
706    }
707    for observer in snapshot(lens_observers()) {
708        observer(lenses.clone());
709    }
710}
711
712/// The devices the application may pick between, as the backend last published
713/// them.
714pub fn camera_lenses() -> CameraLenses {
715    lenses_slot()
716        .lock()
717        .map(|lenses| lenses.clone())
718        .unwrap_or_default()
719}
720
721/// Registers `observer` for frames. Applications collect
722/// [`rememberCameraFrames`] instead of calling this.
723pub fn observe_camera_frames(
724    observer: impl Fn(CameraFrame) + Send + Sync + 'static,
725) -> CameraObserver {
726    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
727    if let Ok(mut observers) = frame_observers().lock() {
728        observers.push((id, Arc::new(observer)));
729    }
730    CameraObserver {
731        id,
732        kind: ObserverKind::Frame,
733    }
734}
735
736/// Registers `observer` for session state. The current state is delivered at
737/// once, so a screen composed mid-session shows what is happening rather than
738/// waiting for the next change.
739pub fn observe_camera_state(
740    observer: impl Fn(CameraState) + Send + Sync + 'static,
741) -> CameraObserver {
742    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
743    let observer: StateObserver = Arc::new(observer);
744    if let Ok(mut observers) = state_observers().lock() {
745        observers.push((id, Arc::clone(&observer)));
746    }
747    observer(camera_state());
748    CameraObserver {
749        id,
750        kind: ObserverKind::State,
751    }
752}
753
754/// Registers `observer` for stills.
755pub fn observe_camera_stills(
756    observer: impl Fn(Result<CameraStill, CameraError>) + Send + Sync + 'static,
757) -> CameraObserver {
758    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
759    if let Ok(mut observers) = still_observers().lock() {
760        observers.push((id, Arc::new(observer)));
761    }
762    CameraObserver {
763        id,
764        kind: ObserverKind::Still,
765    }
766}
767
768/// Registers `observer` for the lens list. The current list is delivered at
769/// once, so a screen composed mid-session shows the devices rather than
770/// waiting for the next change.
771pub fn observe_camera_lenses(
772    observer: impl Fn(CameraLenses) + Send + Sync + 'static,
773) -> CameraObserver {
774    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
775    let observer: LensObserver = Arc::new(observer);
776    if let Ok(mut observers) = lens_observers().lock() {
777        observers.push((id, Arc::clone(&observer)));
778    }
779    observer(camera_lenses());
780    CameraObserver {
781        id,
782        kind: ObserverKind::Lenses,
783    }
784}
785
786/// What the camera session is doing, observed for as long as this call stays in
787/// the composition.
788#[allow(non_snake_case)]
789#[track_caller]
790pub fn rememberCameraState() -> State<CameraState> {
791    let updates = rememberEventStream((), |sender| {
792        observe_camera_state(move |state| sender.send(state))
793    });
794    cranpose_core::collectAsState(updates, (), camera_state())
795}
796
797/// The frames the session produces, as a stream this composition collects.
798///
799/// This is the analysis path: a detector collects it, and one that cannot keep
800/// up falls behind by frames rather than by memory — see
801/// [`dropped_camera_frames`]. A viewfinder draws [`latest_camera_frame`]
802/// instead, which costs no recomposition at all.
803#[allow(non_snake_case)]
804#[track_caller]
805pub fn rememberCameraFrames() -> EventStream<CameraFrame> {
806    rememberEventStream((), |sender| {
807        observe_camera_frames(move |frame| sender.send(frame))
808    })
809}
810
811/// The stills the session produces, as a stream this composition collects.
812#[allow(non_snake_case)]
813#[track_caller]
814pub fn rememberCameraStills() -> EventStream<Result<CameraStill, CameraError>> {
815    rememberEventStream((), |sender| {
816        observe_camera_stills(move |still| sender.send(still))
817    })
818}
819
820/// The lens list and the device in use, observed for as long as this call
821/// stays in the composition.
822#[allow(non_snake_case)]
823#[track_caller]
824pub fn rememberCameraLenses() -> State<CameraLenses> {
825    let updates = rememberEventStream((), |sender| {
826        observe_camera_lenses(move |lenses| sender.send(lenses))
827    });
828    cranpose_core::collectAsState(updates, (), camera_lenses())
829}
830
831/// Starts the camera, publishing [`CameraState::Starting`] before the backend
832/// is asked so a screen shows the wait rather than a gap.
833pub fn start_camera() -> Result<(), CameraError> {
834    let Some(camera) = camera() else {
835        publish_camera_state(CameraState::Failed(CameraError::Unsupported));
836        return Err(CameraError::Unsupported);
837    };
838    publish_camera_state(CameraState::Starting);
839    camera.start().inspect_err(|error| {
840        publish_camera_state(CameraState::Failed(error.clone()));
841    })
842}
843
844/// Stops the camera and releases the device.
845pub fn stop_camera() {
846    if let Some(camera) = camera() {
847        camera.stop();
848    }
849    publish_camera_state(CameraState::Stopped);
850}
851
852/// Asks for a full-resolution still, which arrives through
853/// [`rememberCameraStills`].
854pub fn request_camera_still() -> Result<(), CameraError> {
855    let Some(camera) = camera() else {
856        return Err(CameraError::Unsupported);
857    };
858    if !camera_state().is_running() {
859        return Err(CameraError::NotRunning);
860    }
861    camera.request_still()
862}
863
864/// Takes one full-resolution still and resolves with it.
865///
866/// [`request_camera_still`] asks and [`observe_camera_stills`] answers, which is
867/// the right shape for a screen that keeps a shutter open. A caller that wants
868/// one photograph wants the two joined, and joining them by hand means holding
869/// an observer alive across an await in every application that takes a picture.
870///
871/// The observer is dropped as soon as a still arrives, so a second capture is a
872/// second call rather than a subscription to unregister.
873pub async fn capture_camera_still() -> Result<CameraStill, CameraError> {
874    let signal = crate::async_io::Signal::new();
875    let deliver = signal.clone();
876    let observer = observe_camera_stills(move |result| deliver.set(result));
877    request_camera_still()?;
878    let arrived = signal.wait().await;
879    drop(observer);
880    // The signal only closes empty if the backend went away mid-capture, which
881    // is a camera that stopped rather than a still that failed.
882    arrived.unwrap_or(Err(CameraError::NotRunning))
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888
889    /// A backend that records what it was asked and publishes what a real one
890    /// would.
891    struct FakeCamera {
892        started: AtomicU64,
893        stopped: AtomicU64,
894        stills: AtomicU64,
895        fails: bool,
896    }
897
898    impl FakeCamera {
899        fn new() -> Arc<Self> {
900            Arc::new(Self {
901                started: AtomicU64::new(0),
902                stopped: AtomicU64::new(0),
903                stills: AtomicU64::new(0),
904                fails: false,
905            })
906        }
907
908        fn failing() -> Arc<Self> {
909            Arc::new(Self {
910                started: AtomicU64::new(0),
911                stopped: AtomicU64::new(0),
912                stills: AtomicU64::new(0),
913                fails: true,
914            })
915        }
916    }
917
918    impl Camera for FakeCamera {
919        fn start(&self) -> Result<(), CameraError> {
920            self.started.fetch_add(1, Ordering::Relaxed);
921            if self.fails {
922                return Err(CameraError::PermissionDenied);
923            }
924            publish_camera_state(CameraState::Running {
925                device: "fake".to_string(),
926            });
927            Ok(())
928        }
929
930        fn stop(&self) {
931            self.stopped.fetch_add(1, Ordering::Relaxed);
932        }
933
934        fn request_still(&self) -> Result<(), CameraError> {
935            self.stills.fetch_add(1, Ordering::Relaxed);
936            publish_camera_still(Ok(CameraStill {
937                jpeg: vec![0xff, 0xd8],
938            }));
939            Ok(())
940        }
941    }
942
943    fn rgba_frame(sequence: u64) -> CameraFrame {
944        CameraFrame::new(2, 2, FrameFormat::Rgba8, 90, sequence, vec![7; 16])
945            .expect("a well-formed frame")
946    }
947
948    #[test]
949    fn a_frame_size_is_the_one_its_format_implies() {
950        assert_eq!(FrameFormat::Rgba8.byte_len(4, 2), Some(32));
951        assert_eq!(FrameFormat::Nv12.byte_len(4, 2), Some(12));
952        assert_eq!(
953            FrameFormat::Nv12.byte_len(3, 2),
954            None,
955            "NV12 has no half column for an odd width"
956        );
957        assert_eq!(FrameFormat::Nv12.byte_len(4, 3), None);
958    }
959
960    /// A frame whose bytes do not match its size reads as corrupted video
961    /// rather than as an error, so it is refused where it enters.
962    #[test]
963    fn a_frame_that_does_not_match_its_size_is_refused() {
964        assert!(CameraFrame::new(2, 2, FrameFormat::Rgba8, 0, 0, vec![0; 15]).is_none());
965        assert!(CameraFrame::new(2, 2, FrameFormat::Nv12, 0, 0, vec![0; 5]).is_none());
966        assert!(CameraFrame::new(2, 2, FrameFormat::Rgba8, 0, 0, vec![0; 16]).is_some());
967    }
968
969    #[test]
970    fn a_rotation_is_kept_inside_one_turn() {
971        let frame = CameraFrame::new(2, 2, FrameFormat::Rgba8, 450, 0, vec![0; 16])
972            .expect("a well-formed frame");
973        assert_eq!(frame.rotation_degrees, 90);
974    }
975
976    #[test]
977    fn an_rgba_frame_is_handed_over_unchanged() {
978        let frame = rgba_frame(0);
979        assert_eq!(frame.to_rgba8(), frame.bytes);
980    }
981
982    /// The conversion has to agree with what every other BT.601 full-range
983    /// implementation produces, or the viewfinder is a different colour from
984    /// the photo the same camera takes.
985    #[test]
986    fn nv12_black_white_and_primaries_convert_to_the_expected_colours() {
987        fn convert(luma: u8, blue: u8, red: u8) -> [u8; 4] {
988            let bytes = vec![luma, luma, luma, luma, blue, red];
989            let frame = CameraFrame::new(2, 2, FrameFormat::Nv12, 0, 0, bytes)
990                .expect("a well-formed frame");
991            let rgba = frame.to_rgba8();
992            [rgba[0], rgba[1], rgba[2], rgba[3]]
993        }
994
995        assert_eq!(convert(0, 128, 128), [0, 0, 0, 255], "black");
996        assert_eq!(convert(255, 128, 128), [255, 255, 255, 255], "white");
997
998        let red = convert(76, 84, 255);
999        assert!(
1000            red[0] > 240 && red[1] < 20 && red[2] < 20,
1001            "red, got {red:?}"
1002        );
1003        let blue = convert(29, 255, 107);
1004        assert!(
1005            blue[2] > 240 && blue[0] < 20 && blue[1] < 20,
1006            "blue, got {blue:?}"
1007        );
1008        assert!(
1009            convert(128, 128, 128).iter().all(|value| *value > 0),
1010            "a grey frame must not clamp to black"
1011        );
1012    }
1013
1014    #[test]
1015    fn a_short_nv12_frame_converts_to_black_rather_than_reading_past_its_end() {
1016        let rgba = nv12_to_rgba8(4, 4, &[0u8; 3]);
1017        assert_eq!(rgba.len(), 4 * 4 * 4);
1018        assert!(rgba.iter().all(|value| *value == 0));
1019    }
1020
1021    #[test]
1022    fn a_platform_without_a_camera_says_so_rather_than_pretending() {
1023        let _guard = crate::registry::test_service_guard();
1024        clear_platform_camera();
1025        assert!(!camera_supported());
1026        assert_eq!(start_camera(), Err(CameraError::Unsupported));
1027        assert_eq!(
1028            camera_state(),
1029            CameraState::Failed(CameraError::Unsupported)
1030        );
1031        assert_eq!(request_camera_still(), Err(CameraError::Unsupported));
1032        clear_platform_camera();
1033    }
1034
1035    /// Opening a camera takes long enough on a phone that a screen has to show
1036    /// something in the meantime, so the wait is a state and not a gap.
1037    #[test]
1038    fn the_session_reports_starting_before_it_reports_running() {
1039        let _guard = crate::registry::test_service_guard();
1040        clear_platform_camera();
1041        let seen = Arc::new(Mutex::new(Vec::new()));
1042        let recorder = Arc::clone(&seen);
1043        let observer = observe_camera_state(move |state| {
1044            recorder
1045                .lock()
1046                .unwrap_or_else(|error| error.into_inner())
1047                .push(state)
1048        });
1049        set_platform_camera(FakeCamera::new());
1050        start_camera().expect("the session starts");
1051
1052        assert_eq!(
1053            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1054            vec![
1055                CameraState::Idle,
1056                CameraState::Starting,
1057                CameraState::Running {
1058                    device: "fake".to_string()
1059                }
1060            ]
1061        );
1062        assert!(camera_state().is_running());
1063        assert!(camera_state().is_active());
1064        drop(observer);
1065        clear_platform_camera();
1066    }
1067
1068    #[test]
1069    fn a_session_that_cannot_open_reports_why() {
1070        let _guard = crate::registry::test_service_guard();
1071        clear_platform_camera();
1072        set_platform_camera(FakeCamera::failing());
1073        assert_eq!(start_camera(), Err(CameraError::PermissionDenied));
1074        assert_eq!(
1075            camera_state().failure(),
1076            Some(&CameraError::PermissionDenied)
1077        );
1078        assert!(!camera_state().is_active());
1079        clear_platform_camera();
1080    }
1081
1082    #[test]
1083    fn stopping_releases_the_device_and_says_so() {
1084        let _guard = crate::registry::test_service_guard();
1085        clear_platform_camera();
1086        let backend = FakeCamera::new();
1087        set_platform_camera(backend.clone());
1088        start_camera().expect("the session starts");
1089        stop_camera();
1090        assert_eq!(backend.stopped.load(Ordering::Relaxed), 1);
1091        assert_eq!(camera_state(), CameraState::Stopped);
1092        clear_platform_camera();
1093    }
1094
1095    /// A viewfinder draws the newest frame, so the stored one is always the
1096    /// newest — never a queue a slow drawer works through.
1097    #[test]
1098    fn the_stored_frame_is_always_the_newest_one() {
1099        let _guard = crate::registry::test_service_guard();
1100        clear_platform_camera();
1101        assert_eq!(latest_camera_frame(), None);
1102        publish_camera_frame(rgba_frame(1));
1103        publish_camera_frame(rgba_frame(2));
1104        publish_camera_frame(rgba_frame(3));
1105        assert_eq!(latest_camera_frame().map(|frame| frame.sequence), Some(3));
1106        clear_platform_camera();
1107    }
1108
1109    #[test]
1110    fn frame_observers_see_frames_and_stop_when_dropped() {
1111        let _guard = crate::registry::test_service_guard();
1112        clear_platform_camera();
1113        let seen = Arc::new(Mutex::new(Vec::new()));
1114        let recorder = Arc::clone(&seen);
1115        let observer = observe_camera_frames(move |frame| {
1116            recorder
1117                .lock()
1118                .unwrap_or_else(|error| error.into_inner())
1119                .push(frame.sequence)
1120        });
1121        publish_camera_frame(rgba_frame(1));
1122        publish_camera_frame(rgba_frame(2));
1123        drop(observer);
1124        publish_camera_frame(rgba_frame(3));
1125        assert_eq!(
1126            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1127            vec![1, 2]
1128        );
1129        clear_platform_camera();
1130    }
1131
1132    /// A detector that cannot keep up must fall behind by frames, and the
1133    /// framework has to be able to say how far.
1134    #[test]
1135    fn frames_the_platform_could_not_deliver_are_counted_rather_than_queued() {
1136        let _guard = crate::registry::test_service_guard();
1137        clear_platform_camera();
1138        assert_eq!(dropped_camera_frames(), 0);
1139        record_dropped_camera_frame();
1140        record_dropped_camera_frame();
1141        assert_eq!(dropped_camera_frames(), 2);
1142        // A new session starts the count and the stored frame over: what the
1143        // last one missed says nothing about this one.
1144        publish_camera_state(CameraState::Starting);
1145        assert_eq!(dropped_camera_frames(), 0);
1146        assert_eq!(latest_camera_frame(), None);
1147        clear_platform_camera();
1148    }
1149
1150    /// A still takes as long as the device takes; asking must not block
1151    /// whatever asked.
1152    #[test]
1153    fn a_still_is_asked_for_and_arrives_separately() {
1154        let _guard = crate::registry::test_service_guard();
1155        clear_platform_camera();
1156        let backend = FakeCamera::new();
1157        set_platform_camera(backend.clone());
1158
1159        assert_eq!(
1160            request_camera_still(),
1161            Err(CameraError::NotRunning),
1162            "nothing is running, so there is nothing to photograph"
1163        );
1164
1165        let seen = Arc::new(Mutex::new(Vec::new()));
1166        let recorder = Arc::clone(&seen);
1167        let observer = observe_camera_stills(move |still| {
1168            recorder
1169                .lock()
1170                .unwrap_or_else(|error| error.into_inner())
1171                .push(still)
1172        });
1173        start_camera().expect("the session starts");
1174        request_camera_still().expect("a still is asked for");
1175        assert_eq!(backend.stills.load(Ordering::Relaxed), 1);
1176        assert_eq!(
1177            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1178            vec![Ok(CameraStill {
1179                jpeg: vec![0xff, 0xd8]
1180            })]
1181        );
1182        drop(observer);
1183        clear_platform_camera();
1184    }
1185
1186    #[test]
1187    fn a_backend_that_lists_no_lens_and_no_flash_says_so() {
1188        let _guard = crate::registry::test_service_guard();
1189        clear_platform_camera();
1190        set_platform_camera(FakeCamera::new());
1191        let backend = camera().expect("registered");
1192        assert!(backend.lenses().is_empty());
1193        assert_eq!(backend.lens(), None);
1194        assert!(!backend.use_lens("0"));
1195        assert!(!backend.has_flash());
1196        assert!(!backend.set_flash(FlashMode::On));
1197        assert!(!backend.set_torch(true));
1198        clear_platform_camera();
1199    }
1200
1201    #[test]
1202    fn a_backend_that_lists_two_lenses_hands_them_over_in_order() {
1203        let _guard = crate::registry::test_service_guard();
1204        clear_platform_camera();
1205        struct TwoLenses;
1206        impl Camera for TwoLenses {
1207            fn start(&self) -> Result<(), CameraError> {
1208                Ok(())
1209            }
1210            fn stop(&self) {}
1211            fn lenses(&self) -> Vec<CameraLens> {
1212                vec![
1213                    CameraLens {
1214                        id: "u".into(),
1215                        name: "Ultra wide".into(),
1216                        facing: LensFacing::Back,
1217                    },
1218                    CameraLens {
1219                        id: "w".into(),
1220                        name: "Wide".into(),
1221                        facing: LensFacing::Back,
1222                    },
1223                ]
1224            }
1225            fn lens(&self) -> Option<String> {
1226                Some("w".into())
1227            }
1228            fn use_lens(&self, id: &str) -> bool {
1229                id == "u" || id == "w"
1230            }
1231        }
1232        set_platform_camera(Arc::new(TwoLenses));
1233        let backend = camera().expect("registered");
1234        let lenses = backend.lenses();
1235        assert_eq!(lenses.len(), 2);
1236        assert_eq!(lenses[0].name, "Ultra wide");
1237        assert_eq!(backend.lens().as_deref(), Some("w"));
1238        assert!(backend.use_lens("u"));
1239        assert!(!backend.use_lens("tele"));
1240        clear_platform_camera();
1241    }
1242
1243    fn two_pixel_frame(rotation: u16) -> CameraFrame {
1244        let mut bytes = vec![10u8, 10, 10, 255];
1245        bytes.extend_from_slice(&[20, 20, 20, 255]);
1246        CameraFrame::new(2, 1, FrameFormat::Rgba8, rotation, 0, bytes).expect("a well-formed frame")
1247    }
1248
1249    fn pixel_values(image: &UprightRgba) -> Vec<u8> {
1250        image.rgba.iter().step_by(4).copied().collect()
1251    }
1252
1253    /// A quarter turn must land every pixel where a clockwise turn of the
1254    /// picture puts it, and swap the sides for the odd quarters.
1255    #[test]
1256    fn a_frame_turns_upright_by_its_rotation() {
1257        let unturned = two_pixel_frame(0).upright_rgba8();
1258        assert_eq!((unturned.width, unturned.height), (2, 1));
1259        assert_eq!(pixel_values(&unturned), vec![10, 20]);
1260
1261        let quarter = two_pixel_frame(90).upright_rgba8();
1262        assert_eq!((quarter.width, quarter.height), (1, 2));
1263        assert_eq!(pixel_values(&quarter), vec![10, 20]);
1264
1265        let half = two_pixel_frame(180).upright_rgba8();
1266        assert_eq!((half.width, half.height), (2, 1));
1267        assert_eq!(pixel_values(&half), vec![20, 10]);
1268
1269        let three_quarters = two_pixel_frame(270).upright_rgba8();
1270        assert_eq!((three_quarters.width, three_quarters.height), (1, 2));
1271        assert_eq!(pixel_values(&three_quarters), vec![20, 10]);
1272    }
1273
1274    /// The turn happens in the same pass as the NV12 conversion, so the two
1275    /// paths must agree pixel for pixel.
1276    #[test]
1277    fn an_nv12_frame_turns_and_converts_in_one_pass() {
1278        let bytes = vec![0, 255, 0, 0, 128, 128];
1279        let frame =
1280            CameraFrame::new(2, 2, FrameFormat::Nv12, 90, 0, bytes).expect("a well-formed frame");
1281        let upright = frame.upright_rgba8();
1282        assert_eq!((upright.width, upright.height), (2, 2));
1283        let values = pixel_values(&upright);
1284        assert_eq!(values[0], 0, "top-left stays dark");
1285        assert_eq!(
1286            values[3], 255,
1287            "the bright top-right pixel lands bottom-right"
1288        );
1289    }
1290
1291    #[test]
1292    fn an_rgb8_frame_turns_upright_with_a_full_alpha() {
1293        let frame = CameraFrame::new(
1294            2,
1295            1,
1296            FrameFormat::Rgb8,
1297            180,
1298            0,
1299            vec![10, 10, 10, 20, 20, 20],
1300        )
1301        .expect("a well-formed frame");
1302        let upright = frame.upright_rgba8();
1303        assert_eq!(pixel_values(&upright), vec![20, 10]);
1304        assert!(upright.rgba.iter().skip(3).step_by(4).all(|a| *a == 255));
1305    }
1306
1307    /// No camera produces a turn that is not a quarter, so anything else is
1308    /// left alone rather than guessed at.
1309    #[test]
1310    fn a_turn_that_is_not_a_quarter_is_left_alone() {
1311        let frame = CameraFrame::new(2, 1, FrameFormat::Rgba8, 45, 0, vec![7; 8])
1312            .expect("a well-formed frame");
1313        let upright = frame.upright_rgba8();
1314        assert_eq!((upright.width, upright.height), (2, 1));
1315        assert_eq!(upright.rgba, frame.to_rgba8());
1316    }
1317
1318    #[test]
1319    fn the_lens_list_is_published_and_observed() {
1320        let _guard = crate::registry::test_service_guard();
1321        clear_platform_camera();
1322        assert_eq!(camera_lenses(), CameraLenses::default());
1323
1324        let seen = Arc::new(Mutex::new(Vec::new()));
1325        let recorder = Arc::clone(&seen);
1326        let observer = observe_camera_lenses(move |lenses| {
1327            recorder
1328                .lock()
1329                .unwrap_or_else(|error| error.into_inner())
1330                .push(lenses)
1331        });
1332
1333        let published = CameraLenses {
1334            lenses: vec![CameraLens {
1335                id: "0".into(),
1336                name: "Back".into(),
1337                facing: LensFacing::Back,
1338            }],
1339            active: Some("0".into()),
1340        };
1341        publish_camera_lenses(published.clone());
1342        publish_camera_lenses(published.clone());
1343        assert_eq!(camera_lenses(), published);
1344        assert_eq!(
1345            *seen.lock().unwrap_or_else(|error| error.into_inner()),
1346            vec![CameraLenses::default(), published.clone()],
1347            "the current list arrives at once, and a repeat is not re-delivered"
1348        );
1349
1350        drop(observer);
1351        publish_camera_lenses(CameraLenses::default());
1352        assert_eq!(
1353            seen.lock().unwrap_or_else(|error| error.into_inner()).len(),
1354            2,
1355            "a dropped observer hears nothing more"
1356        );
1357        clear_platform_camera();
1358        assert_eq!(camera_lenses(), CameraLenses::default());
1359    }
1360}