pryty-rustbrowser 0.1.2

One-line browser API hooks for Rust Front-end development
Documentation
use dioxus::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{MediaStream, MediaStreamConstraints, MediaTrackConstraints, MediaStreamTrack};

#[derive(Debug, Clone, PartialEq)]
pub enum CameraState {
    Idle,
    Starting,
    Active,
    Stopping,
    Error(String),
}

#[derive(Debug, Clone)]
pub enum CameraError {
    WindowUnavailable,
    MediaDevicesUnavailable,
    GetUserMediaFailed(String),
    CastMediaStreamFailed,
}

impl core::fmt::Display for CameraError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            CameraError::WindowUnavailable => write!(f, "window unavailable"),
            CameraError::MediaDevicesUnavailable => write!(f, "media devices unavailable"),
            CameraError::GetUserMediaFailed(e) => write!(f, "getUserMedia failed: {e}"),
            CameraError::CastMediaStreamFailed => write!(f, "failed to cast to MediaStream"),
        }
    }
}

pub struct CameraQualityConfig {
    pub name: &'static str,
    pub width: u32,
    pub height: u32,
    pub frame_rate: f64,
}

impl CameraQualityConfig {
    pub fn low() -> Self {
        Self {
            name: "Low (480p)",
            width: 640,
            height: 480,
            frame_rate: 15.0,
        }
    }

    pub fn hd() -> Self {
        Self {
            name: "HD (720p)",
            width: 1280,
            height: 720,
            frame_rate: 30.0,
        }
    }

    pub fn full_hd() -> Self {
        Self {
            name: "Full HD (1080p)",
            width: 1920,
            height: 1080,
            frame_rate: 60.0,
        }
    }
}

pub struct Camera {
    pub start: Callback<()>,
    pub start_with_quality: Callback<CameraQualityConfig>,
    pub stop: Callback<()>,
    pub stream: Signal<Option<MediaStream>>,
    pub state: Signal<CameraState>,
    pub last_error: Signal<Option<String>>,
}

impl Camera {
    pub fn is_active(&self) -> bool {
        matches!(*self.state.read(), CameraState::Active)
    }

    pub fn is_busy(&self) -> bool {
        matches!(*self.state.read(), CameraState::Starting | CameraState::Stopping)
    }
}

pub fn use_camera() -> Camera {
    let stream = use_signal(|| None::<MediaStream>);
    let state = use_signal(|| CameraState::Idle);
    let last_error = use_signal(|| None::<String>);

    let start = {
        let stream = stream.clone();
        let state = state.clone();
        let last_error = last_error.clone();

        use_callback(move |_| {
            let mut stream = stream.clone();
            let mut state = state.clone();
            let mut last_error = last_error.clone();

            spawn(async move {
                if let Err(e) = start_camera(&mut stream, &mut state, &mut last_error).await {
                    let msg = e.to_string();
                    state.set(CameraState::Error(msg.clone()));
                    last_error.set(Some(msg));
                }
            });
        })
    };

    let start_with_quality = {
        let stream = stream.clone();
        let state = state.clone();
        let last_error = last_error.clone();

        use_callback(move |quality: CameraQualityConfig| {
            let mut stream = stream.clone();
            let mut state = state.clone();
            let mut last_error = last_error.clone();

            spawn(async move {
                if let Err(e) = start_camera_with_quality(
                    &mut stream,
                    &mut state,
                    &mut last_error,
                    Some(quality),
                )
                .await
                {
                    let msg = e.to_string();
                    state.set(CameraState::Error(msg.clone()));
                    last_error.set(Some(msg));
                }
            });
        })
    };

    let stop = {
        let mut stream = stream.clone();
        let mut state = state.clone();

        use_callback(move |_| {
            stop_camera(&mut stream, &mut state);
        })
    };

    Camera {
        start,
        start_with_quality,
        stop,
        stream,
        state,
        last_error,
    }
}

async fn start_camera(
    stream: &mut Signal<Option<MediaStream>>,
    state: &mut Signal<CameraState>,
    last_error: &mut Signal<Option<String>>,
) -> Result<(), CameraError> {
    start_camera_with_quality(stream, state, last_error, None).await
}

pub async fn start_camera_with_quality(
    stream: &mut Signal<Option<MediaStream>>,
    state: &mut Signal<CameraState>,
    last_error: &mut Signal<Option<String>>,
    quality: Option<CameraQualityConfig>,
) -> Result<(), CameraError> {

    state.set(CameraState::Starting);
    
    if let Some(s) = stream.read().as_ref() {
        let tracks = s.get_tracks();
        for i in 0..tracks.length() {
            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
                track.stop();
            }
        }
    }
    last_error.set(None);

    let window = web_sys::window().ok_or(CameraError::WindowUnavailable)?;
    let devices = window
        .navigator()
        .media_devices()
        .map_err(|_| CameraError::MediaDevicesUnavailable)?;

    let constraints = MediaStreamConstraints::new();

    if let Some(q) = quality {
        let video = MediaTrackConstraints::new();
        video.set_width(&q.width.into());
        video.set_height(&q.height.into());
        video.set_frame_rate(&q.frame_rate.into());
        constraints.set_video(&video.into());
    } else {
        constraints.set_video(&true.into());
    }

    let promise = devices
        .get_user_media_with_constraints(&constraints)
        .map_err(|e| CameraError::GetUserMediaFailed(format!("{e:?}")))?;

    let js_val = JsFuture::from(promise)
        .await
        .map_err(|e| CameraError::GetUserMediaFailed(format!("{e:?}")))?;

    let s: MediaStream = js_val
        .dyn_into()
        .map_err(|_| CameraError::CastMediaStreamFailed)?;

    stream.set(Some(s));
    state.set(CameraState::Active);
    Ok(())
}

fn stop_camera(stream: &mut Signal<Option<MediaStream>>, state: &mut Signal<CameraState>) {
    state.set(CameraState::Stopping);

    if let Some(s) = stream.read().as_ref() {
        let tracks = s.get_tracks();
        for i in 0..tracks.length() {
            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
                track.stop();
            }
        }
    }

    stream.set(None);
    state.set(CameraState::Idle);
}