fission-shell-winit 0.8.0

Shared winit shell runtime for desktop and mobile Fission hosts
Documentation
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};

/// Host-side camera and flashlight provider.
pub trait CameraHost: Send + Sync + 'static {
    /// Returns camera permission state and host-visible camera devices.
    fn availability(&self) -> Result<CameraAvailability, CameraError>;
    /// Requests camera permission and returns the resulting permission state.
    fn request_permission(
        &self,
        request: CameraPermissionRequest,
    ) -> Result<CameraPermission, CameraError>;
    /// Captures a still image according to the selected camera, format, flash, and quality request.
    fn capture_photo(
        &self,
        request: CameraCaptureRequest,
        ctx: &CapabilityCtx,
    ) -> Result<CameraCapture, CameraError>;
    /// Enables, disables, or adjusts the selected camera flashlight where available.
    fn set_flashlight(&self, request: CameraFlashlightRequest) -> Result<(), CameraError>;
    /// Cancels an active camera capture flow.
    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]);
    }
}