use fission_core::{
single_chunk_data_stream, CameraAvailability, CameraCapture, CameraCaptureRequest,
CameraDevice, CameraError, CameraFacing, CameraFlashlightRequest, CameraPermission,
CameraPermissionRequest, CapabilityCtx, CANCEL_CAMERA_CAPTURE, CAPTURE_PHOTO,
GET_CAMERA_AVAILABILITY, REQUEST_CAMERA_PERMISSION, SET_CAMERA_FLASHLIGHT,
};
use fission_shell::async_host::AsyncRegistry;
use std::io::Cursor;
use std::sync::{Arc, Mutex};
pub trait CameraHost: Send + Sync + 'static {
fn availability(&self) -> Result<CameraAvailability, CameraError>;
fn request_permission(
&self,
request: CameraPermissionRequest,
) -> Result<CameraPermission, CameraError>;
fn capture_photo(
&self,
request: CameraCaptureRequest,
ctx: &CapabilityCtx,
) -> Result<CameraCapture, CameraError>;
fn set_flashlight(&self, request: CameraFlashlightRequest) -> Result<(), CameraError>;
fn cancel_capture(&self) -> Result<(), CameraError>;
}
#[derive(Debug, Default)]
pub struct UnsupportedCameraHost;
impl CameraHost for UnsupportedCameraHost {
fn availability(&self) -> Result<CameraAvailability, CameraError> {
Ok(CameraAvailability {
permission: CameraPermission::Denied,
devices: Vec::new(),
})
}
fn request_permission(
&self,
_request: CameraPermissionRequest,
) -> Result<CameraPermission, CameraError> {
Err(CameraError::unsupported("request_permission"))
}
fn capture_photo(
&self,
_request: CameraCaptureRequest,
_ctx: &CapabilityCtx,
) -> Result<CameraCapture, CameraError> {
Err(CameraError::unsupported("capture_photo"))
}
fn set_flashlight(&self, _request: CameraFlashlightRequest) -> Result<(), CameraError> {
Err(CameraError::unsupported("set_flashlight"))
}
fn cancel_capture(&self) -> Result<(), CameraError> {
Err(CameraError::unsupported("cancel_capture"))
}
}
#[derive(Debug)]
pub struct MemoryCameraHost {
availability: CameraAvailability,
capture_bytes: Arc<Vec<u8>>,
content_type: String,
width: u32,
height: u32,
camera_id: Option<String>,
flashlight_calls: Arc<Mutex<Vec<CameraFlashlightRequest>>>,
}
impl MemoryCameraHost {
pub fn new(
availability: CameraAvailability,
capture_bytes: Vec<u8>,
content_type: impl Into<String>,
width: u32,
height: u32,
camera_id: Option<String>,
) -> Self {
Self {
availability,
capture_bytes: Arc::new(capture_bytes),
content_type: content_type.into(),
width,
height,
camera_id,
flashlight_calls: Arc::new(Mutex::new(Vec::new())),
}
}
pub fn flashlight_calls(&self) -> Vec<CameraFlashlightRequest> {
self.flashlight_calls
.lock()
.map(|calls| calls.clone())
.unwrap_or_default()
}
}
impl Default for MemoryCameraHost {
fn default() -> Self {
Self::new(
CameraAvailability {
permission: CameraPermission::Granted,
devices: vec![CameraDevice {
id: "memory-camera".into(),
label: Some("Memory camera".into()),
facing: CameraFacing::Back,
has_flashlight: true,
}],
},
demo_capture_png(96, 72),
"image/png",
96,
72,
Some("memory-camera".into()),
)
}
}
fn demo_capture_png(width: u32, height: u32) -> Vec<u8> {
let mut image = image::RgbaImage::new(width, height);
for (x, y, pixel) in image.enumerate_pixels_mut() {
let red = 24 + ((x * 140) / width.max(1)) as u8;
let green = 120 + ((y * 90) / height.max(1)) as u8;
let blue = 180 + (((x + y) * 50) / (width + height).max(1)) as u8;
*pixel = image::Rgba([red, green, blue, 255]);
}
let mut bytes = Cursor::new(Vec::new());
image::DynamicImage::ImageRgba8(image)
.write_to(&mut bytes, image::ImageFormat::Png)
.expect("encode in-memory camera capture");
bytes.into_inner()
}
impl CameraHost for MemoryCameraHost {
fn availability(&self) -> Result<CameraAvailability, CameraError> {
Ok(self.availability.clone())
}
fn request_permission(
&self,
_request: CameraPermissionRequest,
) -> Result<CameraPermission, CameraError> {
Ok(self.availability.permission)
}
fn capture_photo(
&self,
_request: CameraCaptureRequest,
ctx: &CapabilityCtx,
) -> Result<CameraCapture, CameraError> {
let stream =
ctx.register_data_stream(single_chunk_data_stream((*self.capture_bytes).clone()));
Ok(CameraCapture {
stream,
byte_len: Some(self.capture_bytes.len() as u64),
content_type: self.content_type.clone(),
width: self.width,
height: self.height,
camera_id: self.camera_id.clone(),
})
}
fn set_flashlight(&self, request: CameraFlashlightRequest) -> Result<(), CameraError> {
self.flashlight_calls.lock().unwrap().push(request);
Ok(())
}
fn cancel_capture(&self) -> Result<(), CameraError> {
Ok(())
}
}
pub(crate) fn register_camera_capabilities(
async_registry: &mut AsyncRegistry,
host: Arc<dyn CameraHost>,
) {
let availability_host = host.clone();
async_registry.register_operation_capability(GET_CAMERA_AVAILABILITY, move |(), _| {
let host = availability_host.clone();
async move { host.availability() }
});
let permission_host = host.clone();
async_registry.register_operation_capability(REQUEST_CAMERA_PERMISSION, move |request, _| {
let host = permission_host.clone();
async move { host.request_permission(request) }
});
let capture_host = host.clone();
async_registry.register_operation_capability(CAPTURE_PHOTO, move |request, ctx| {
let host = capture_host.clone();
async move { host.capture_photo(request, &ctx) }
});
let flashlight_host = host.clone();
async_registry.register_operation_capability(SET_CAMERA_FLASHLIGHT, move |request, _| {
let host = flashlight_host.clone();
async move { host.set_flashlight(request) }
});
async_registry.register_operation_capability(CANCEL_CAMERA_CAPTURE, move |(), _| {
let host = host.clone();
async move { host.cancel_capture() }
});
}
#[cfg(test)]
mod tests {
use super::*;
use fission_core::{CameraImageFormat, DataStreamRegistry};
#[test]
fn unsupported_host_reports_errors() {
let host = UnsupportedCameraHost;
let ctx = CapabilityCtx::new_runtime(1, DataStreamRegistry::new());
assert!(host
.capture_photo(
CameraCaptureRequest {
format: CameraImageFormat::Jpeg,
..Default::default()
},
&ctx,
)
.is_err());
assert!(host
.set_flashlight(CameraFlashlightRequest {
enabled: true,
..Default::default()
})
.is_err());
}
#[test]
fn memory_host_returns_capture_and_records_flashlight() {
let host = MemoryCameraHost::default();
let availability = host.availability().unwrap();
assert_eq!(availability.permission, CameraPermission::Granted);
let ctx = CapabilityCtx::new_runtime(1, DataStreamRegistry::new());
let capture = host
.capture_photo(CameraCaptureRequest::default(), &ctx)
.unwrap();
assert_eq!(capture.width, 96);
assert_eq!(capture.height, 72);
assert_eq!(capture.content_type, "image/png");
assert!(capture.stream.0 > 0);
let request = CameraFlashlightRequest {
enabled: true,
intensity: Some(128),
..Default::default()
};
host.set_flashlight(request.clone()).unwrap();
assert_eq!(host.flashlight_calls(), vec![request]);
}
}