use std::sync::{
Arc, Mutex, OnceLock,
atomic::{AtomicU64, Ordering},
};
use cranpose_core::{EventStream, State, rememberEventStream};
use crate::registry::ServiceRegistry;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FrameFormat {
#[default]
Rgba8,
Rgb8,
Nv12,
}
impl FrameFormat {
pub fn byte_len(self, width: u32, height: u32) -> Option<usize> {
let pixels = (width as usize).checked_mul(height as usize)?;
match self {
FrameFormat::Rgba8 => pixels.checked_mul(4),
FrameFormat::Rgb8 => pixels.checked_mul(3),
FrameFormat::Nv12 => {
if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
return None;
}
pixels.checked_add(pixels / 2)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CameraFrame {
pub width: u32,
pub height: u32,
pub format: FrameFormat,
pub rotation_degrees: u16,
pub sequence: u64,
pub bytes: Vec<u8>,
}
impl CameraFrame {
pub fn new(
width: u32,
height: u32,
format: FrameFormat,
rotation_degrees: u16,
sequence: u64,
bytes: Vec<u8>,
) -> Option<Self> {
if format.byte_len(width, height)? != bytes.len() {
return None;
}
Some(Self {
width,
height,
format,
rotation_degrees: rotation_degrees % 360,
sequence,
bytes,
})
}
pub fn to_rgba8(&self) -> Vec<u8> {
match self.format {
FrameFormat::Rgba8 => self.bytes.clone(),
FrameFormat::Rgb8 => rgb8_to_rgba8(&self.bytes),
FrameFormat::Nv12 => nv12_to_rgba8(self.width, self.height, &self.bytes),
}
}
pub fn upright_rgba8(&self) -> UprightRgba {
let rotation = self.rotation_degrees;
if !matches!(rotation, 90 | 180 | 270) {
return UprightRgba {
width: self.width,
height: self.height,
rgba: self.to_rgba8(),
};
}
let (width, height) = (self.width as usize, self.height as usize);
let (out_width, out_height) = match rotation {
90 | 270 => (self.height, self.width),
_ => (self.width, self.height),
};
let mut rgba = vec![0u8; width * height * 4];
match self.format {
FrameFormat::Rgba8 => {
for y in 0..height {
let row = &self.bytes[y * width * 4..(y + 1) * width * 4];
for x in 0..width {
let src = &row[x * 4..x * 4 + 4];
let dst = turned_index(rotation, width, height, x, y) * 4;
rgba[dst..dst + 4].copy_from_slice(src);
}
}
}
FrameFormat::Rgb8 => {
for y in 0..height {
let row = &self.bytes[y * width * 3..(y + 1) * width * 3];
for x in 0..width {
let src = &row[x * 3..x * 3 + 3];
let dst = turned_index(rotation, width, height, x, y) * 4;
rgba[dst..dst + 3].copy_from_slice(src);
rgba[dst + 3] = 255;
}
}
}
FrameFormat::Nv12 => {
let pixels = width * height;
if self.bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
return UprightRgba {
width: out_width,
height: out_height,
rgba,
};
}
let (luma, chroma) = self.bytes.split_at(pixels);
for y in 0..height {
let luma_row = &luma[y * width..(y + 1) * width];
let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
for x in 0..width {
let luminance = luma_row[x] as i32;
let blue_difference = chroma_row[x & !1] as i32 - 128;
let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
let dst = turned_index(rotation, width, height, x, y) * 4;
rgba[dst] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
rgba[dst + 1] = clamp_byte(
luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16),
);
rgba[dst + 2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
rgba[dst + 3] = 255;
}
}
}
}
UprightRgba {
width: out_width,
height: out_height,
rgba,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UprightRgba {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
#[inline]
fn turned_index(rotation: u16, width: usize, height: usize, x: usize, y: usize) -> usize {
match rotation {
90 => x * height + (height - 1 - y),
180 => (height - 1 - y) * width + (width - 1 - x),
270 => (width - 1 - x) * height + y,
_ => y * width + x,
}
}
fn rgb8_to_rgba8(bytes: &[u8]) -> Vec<u8> {
let mut rgba = Vec::with_capacity(bytes.len() / 3 * 4);
for [red, green, blue] in bytes.as_chunks::<3>().0 {
rgba.extend_from_slice(&[*red, *green, *blue, 255]);
}
rgba
}
fn nv12_to_rgba8(width: u32, height: u32, bytes: &[u8]) -> Vec<u8> {
let (width, height) = (width as usize, height as usize);
let pixels = width * height;
let mut rgba = vec![0u8; pixels * 4];
if bytes.len() < pixels + pixels / 2 || width == 0 || height == 0 {
return rgba;
}
let (luma, chroma) = bytes.split_at(pixels);
for y in 0..height {
let luma_row = &luma[y * width..(y + 1) * width];
let chroma_row = &chroma[(y / 2) * width..(y / 2 + 1) * width];
let out_row = &mut rgba[y * width * 4..(y + 1) * width * 4];
for x in 0..width {
let luminance = luma_row[x] as i32;
let blue_difference = chroma_row[x & !1] as i32 - 128;
let red_difference = chroma_row[(x & !1) + 1] as i32 - 128;
let out = &mut out_row[x * 4..x * 4 + 4];
out[0] = clamp_byte(luminance + ((91881 * red_difference) >> 16));
out[1] =
clamp_byte(luminance - ((22554 * blue_difference + 46802 * red_difference) >> 16));
out[2] = clamp_byte(luminance + ((116130 * blue_difference) >> 16));
out[3] = 255;
}
}
rgba
}
fn clamp_byte(value: i32) -> u8 {
value.clamp(0, 255) as u8
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CameraStill {
pub jpeg: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CameraLens {
pub id: String,
pub name: String,
pub facing: LensFacing,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum LensFacing {
#[default]
Back,
Front,
External,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CameraLenses {
pub lenses: Vec<CameraLens>,
pub active: Option<String>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FlashMode {
#[default]
Off,
Auto,
On,
}
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum CameraError {
#[error("live camera capture is not supported here")]
Unsupported,
#[error("camera permission denied")]
PermissionDenied,
#[error("the camera is not running")]
NotRunning,
#[error("{0}")]
Failed(String),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum CameraState {
#[default]
Idle,
Starting,
Running {
device: String,
},
Stopped,
Failed(CameraError),
}
impl CameraState {
pub fn is_running(&self) -> bool {
matches!(self, CameraState::Running { .. })
}
pub fn is_active(&self) -> bool {
matches!(self, CameraState::Starting | CameraState::Running { .. })
}
pub fn failure(&self) -> Option<&CameraError> {
match self {
CameraState::Failed(error) => Some(error),
_ => None,
}
}
}
pub trait Camera: Send + Sync {
fn start(&self) -> Result<(), CameraError>;
fn stop(&self);
fn request_still(&self) -> Result<(), CameraError> {
Err(CameraError::Unsupported)
}
fn set_torch(&self, _on: bool) -> bool {
false
}
fn lenses(&self) -> Vec<CameraLens> {
Vec::new()
}
fn lens(&self) -> Option<String> {
None
}
fn use_lens(&self, _id: &str) -> bool {
false
}
fn has_flash(&self) -> bool {
false
}
fn set_flash(&self, _mode: FlashMode) -> bool {
false
}
}
pub type CameraRef = Arc<dyn Camera>;
static PLATFORM_CAMERA: ServiceRegistry<dyn Camera> = ServiceRegistry::new();
pub fn set_platform_camera(camera: CameraRef) {
PLATFORM_CAMERA.set(camera);
}
pub fn clear_platform_camera() {
PLATFORM_CAMERA.clear();
if let Ok(mut observers) = frame_observers().lock() {
observers.clear();
}
if let Ok(mut observers) = state_observers().lock() {
observers.clear();
}
if let Ok(mut observers) = still_observers().lock() {
observers.clear();
}
if let Ok(mut observers) = lens_observers().lock() {
observers.clear();
}
if let Ok(mut latest) = latest_frame_slot().lock() {
*latest = None;
}
if let Ok(mut state) = state_slot().lock() {
*state = CameraState::Idle;
}
if let Ok(mut lenses) = lenses_slot().lock() {
*lenses = CameraLenses::default();
}
DROPPED_FRAMES.store(0, Ordering::Release);
}
pub fn camera() -> Option<CameraRef> {
PLATFORM_CAMERA.get()
}
pub fn camera_supported() -> bool {
PLATFORM_CAMERA.get().is_some()
}
fn state_slot() -> &'static Mutex<CameraState> {
static SLOT: OnceLock<Mutex<CameraState>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(CameraState::Idle))
}
fn latest_frame_slot() -> &'static Mutex<Option<CameraFrame>> {
static SLOT: OnceLock<Mutex<Option<CameraFrame>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
fn lenses_slot() -> &'static Mutex<CameraLenses> {
static SLOT: OnceLock<Mutex<CameraLenses>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(CameraLenses::default()))
}
static DROPPED_FRAMES: AtomicU64 = AtomicU64::new(0);
type FrameObserver = Arc<dyn Fn(CameraFrame) + Send + Sync>;
type StateObserver = Arc<dyn Fn(CameraState) + Send + Sync>;
type StillObserver = Arc<dyn Fn(Result<CameraStill, CameraError>) + Send + Sync>;
type LensObserver = Arc<dyn Fn(CameraLenses) + Send + Sync>;
fn frame_observers() -> &'static Mutex<Vec<(u64, FrameObserver)>> {
static SLOT: OnceLock<Mutex<Vec<(u64, FrameObserver)>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(Vec::new()))
}
fn state_observers() -> &'static Mutex<Vec<(u64, StateObserver)>> {
static SLOT: OnceLock<Mutex<Vec<(u64, StateObserver)>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(Vec::new()))
}
fn still_observers() -> &'static Mutex<Vec<(u64, StillObserver)>> {
static SLOT: OnceLock<Mutex<Vec<(u64, StillObserver)>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(Vec::new()))
}
fn lens_observers() -> &'static Mutex<Vec<(u64, LensObserver)>> {
static SLOT: OnceLock<Mutex<Vec<(u64, LensObserver)>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(Vec::new()))
}
static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
pub struct CameraObserver {
id: u64,
kind: ObserverKind,
}
#[derive(Clone, Copy)]
enum ObserverKind {
Frame,
State,
Still,
Lenses,
}
impl Drop for CameraObserver {
fn drop(&mut self) {
match self.kind {
ObserverKind::Frame => retain_without(frame_observers(), self.id),
ObserverKind::State => retain_without(state_observers(), self.id),
ObserverKind::Still => retain_without(still_observers(), self.id),
ObserverKind::Lenses => retain_without(lens_observers(), self.id),
}
}
}
fn retain_without<T>(slot: &'static Mutex<Vec<(u64, T)>>, id: u64) {
if let Ok(mut observers) = slot.lock() {
observers.retain(|(observer, _)| *observer != id);
}
}
fn snapshot<T: Clone>(slot: &'static Mutex<Vec<(u64, T)>>) -> Vec<T> {
slot.lock()
.map(|observers| {
observers
.iter()
.map(|(_, observer)| observer.clone())
.collect()
})
.unwrap_or_default()
}
pub fn latest_camera_frame() -> Option<CameraFrame> {
latest_frame_slot()
.lock()
.map_or(None, |frame| frame.clone())
}
pub fn camera_state() -> CameraState {
state_slot()
.lock()
.map(|state| state.clone())
.unwrap_or_default()
}
pub fn dropped_camera_frames() -> u64 {
DROPPED_FRAMES.load(Ordering::Acquire)
}
pub fn publish_camera_frame(frame: CameraFrame) {
if let Ok(mut latest) = latest_frame_slot().lock() {
*latest = Some(frame.clone());
}
let observers = snapshot(frame_observers());
if observers.is_empty() {
return;
}
for observer in observers {
observer(frame.clone());
}
}
pub fn record_dropped_camera_frame() {
DROPPED_FRAMES.fetch_add(1, Ordering::AcqRel);
}
pub fn publish_camera_state(state: CameraState) {
{
let Ok(mut current) = state_slot().lock() else {
return;
};
if *current == state {
return;
}
*current = state.clone();
}
if matches!(state, CameraState::Idle | CameraState::Starting) {
DROPPED_FRAMES.store(0, Ordering::Release);
if let Ok(mut latest) = latest_frame_slot().lock() {
*latest = None;
}
}
for observer in snapshot(state_observers()) {
observer(state.clone());
}
}
pub fn publish_camera_still(still: Result<CameraStill, CameraError>) {
for observer in snapshot(still_observers()) {
observer(still.clone());
}
}
pub fn publish_camera_lenses(lenses: CameraLenses) {
{
let Ok(mut current) = lenses_slot().lock() else {
return;
};
if *current == lenses {
return;
}
*current = lenses.clone();
}
for observer in snapshot(lens_observers()) {
observer(lenses.clone());
}
}
pub fn camera_lenses() -> CameraLenses {
lenses_slot()
.lock()
.map(|lenses| lenses.clone())
.unwrap_or_default()
}
pub fn observe_camera_frames(
observer: impl Fn(CameraFrame) + Send + Sync + 'static,
) -> CameraObserver {
let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
if let Ok(mut observers) = frame_observers().lock() {
observers.push((id, Arc::new(observer)));
}
CameraObserver {
id,
kind: ObserverKind::Frame,
}
}
pub fn observe_camera_state(
observer: impl Fn(CameraState) + Send + Sync + 'static,
) -> CameraObserver {
let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
let observer: StateObserver = Arc::new(observer);
if let Ok(mut observers) = state_observers().lock() {
observers.push((id, Arc::clone(&observer)));
}
observer(camera_state());
CameraObserver {
id,
kind: ObserverKind::State,
}
}
pub fn observe_camera_stills(
observer: impl Fn(Result<CameraStill, CameraError>) + Send + Sync + 'static,
) -> CameraObserver {
let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
if let Ok(mut observers) = still_observers().lock() {
observers.push((id, Arc::new(observer)));
}
CameraObserver {
id,
kind: ObserverKind::Still,
}
}
pub fn observe_camera_lenses(
observer: impl Fn(CameraLenses) + Send + Sync + 'static,
) -> CameraObserver {
let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
let observer: LensObserver = Arc::new(observer);
if let Ok(mut observers) = lens_observers().lock() {
observers.push((id, Arc::clone(&observer)));
}
observer(camera_lenses());
CameraObserver {
id,
kind: ObserverKind::Lenses,
}
}
#[expect(non_snake_case)]
#[track_caller]
pub fn rememberCameraState() -> State<CameraState> {
let updates = rememberEventStream((), |sender| {
observe_camera_state(move |state| sender.send(state))
});
cranpose_core::collectAsState(updates, (), camera_state())
}
#[expect(non_snake_case)]
#[track_caller]
pub fn rememberCameraFrames() -> EventStream<CameraFrame> {
rememberEventStream((), |sender| {
observe_camera_frames(move |frame| sender.send(frame))
})
}
#[expect(non_snake_case)]
#[track_caller]
pub fn rememberCameraStills() -> EventStream<Result<CameraStill, CameraError>> {
rememberEventStream((), |sender| {
observe_camera_stills(move |still| sender.send(still))
})
}
#[expect(non_snake_case)]
#[track_caller]
pub fn rememberCameraLenses() -> State<CameraLenses> {
let updates = rememberEventStream((), |sender| {
observe_camera_lenses(move |lenses| sender.send(lenses))
});
cranpose_core::collectAsState(updates, (), camera_lenses())
}
pub fn start_camera() -> Result<(), CameraError> {
let Some(camera) = camera() else {
publish_camera_state(CameraState::Failed(CameraError::Unsupported));
return Err(CameraError::Unsupported);
};
publish_camera_state(CameraState::Starting);
camera.start().inspect_err(|error| {
publish_camera_state(CameraState::Failed(error.clone()));
})
}
pub fn stop_camera() {
if let Some(camera) = camera() {
camera.stop();
}
publish_camera_state(CameraState::Stopped);
}
pub fn request_camera_still() -> Result<(), CameraError> {
let Some(camera) = camera() else {
return Err(CameraError::Unsupported);
};
if !camera_state().is_running() {
return Err(CameraError::NotRunning);
}
camera.request_still()
}
pub async fn capture_camera_still() -> Result<CameraStill, CameraError> {
let signal = crate::async_io::Signal::new();
let deliver = signal.clone();
let observer = observe_camera_stills(move |result| deliver.set(result));
request_camera_still()?;
let arrived = signal.wait().await;
drop(observer);
arrived.unwrap_or(Err(CameraError::NotRunning))
}
#[cfg(test)]
#[path = "tests/camera_tests.rs"]
mod tests;