Skip to main content

pryty_rustbrowser/
camera.rs

1use dioxus::prelude::*;
2use wasm_bindgen::JsCast;
3use wasm_bindgen_futures::JsFuture;
4use web_sys::{MediaStream, MediaStreamConstraints, MediaTrackConstraints, MediaStreamTrack};
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum CameraState {
8    Idle,
9    Starting,
10    Active,
11    Stopping,
12    Error(String),
13}
14
15#[derive(Debug, Clone)]
16pub enum CameraError {
17    WindowUnavailable,
18    MediaDevicesUnavailable,
19    GetUserMediaFailed(String),
20    CastMediaStreamFailed,
21}
22
23impl core::fmt::Display for CameraError {
24    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
25        match self {
26            CameraError::WindowUnavailable => write!(f, "window unavailable"),
27            CameraError::MediaDevicesUnavailable => write!(f, "media devices unavailable"),
28            CameraError::GetUserMediaFailed(e) => write!(f, "getUserMedia failed: {e}"),
29            CameraError::CastMediaStreamFailed => write!(f, "failed to cast to MediaStream"),
30        }
31    }
32}
33
34pub struct CameraQualityConfig {
35    pub name: &'static str,
36    pub width: u32,
37    pub height: u32,
38    pub frame_rate: f64,
39}
40
41impl CameraQualityConfig {
42    pub fn low() -> Self {
43        Self {
44            name: "Low (480p)",
45            width: 640,
46            height: 480,
47            frame_rate: 15.0,
48        }
49    }
50
51    pub fn hd() -> Self {
52        Self {
53            name: "HD (720p)",
54            width: 1280,
55            height: 720,
56            frame_rate: 30.0,
57        }
58    }
59
60    pub fn full_hd() -> Self {
61        Self {
62            name: "Full HD (1080p)",
63            width: 1920,
64            height: 1080,
65            frame_rate: 60.0,
66        }
67    }
68}
69
70pub struct Camera {
71    pub start: Callback<()>,
72    pub start_with_quality: Callback<CameraQualityConfig>,
73    pub stop: Callback<()>,
74    pub stream: Signal<Option<MediaStream>>,
75    pub state: Signal<CameraState>,
76    pub last_error: Signal<Option<String>>,
77}
78
79impl Camera {
80    pub fn is_active(&self) -> bool {
81        matches!(*self.state.read(), CameraState::Active)
82    }
83
84    pub fn is_busy(&self) -> bool {
85        matches!(*self.state.read(), CameraState::Starting | CameraState::Stopping)
86    }
87}
88
89pub fn use_camera() -> Camera {
90    let stream = use_signal(|| None::<MediaStream>);
91    let state = use_signal(|| CameraState::Idle);
92    let last_error = use_signal(|| None::<String>);
93
94    let start = {
95        let stream = stream.clone();
96        let state = state.clone();
97        let last_error = last_error.clone();
98
99        use_callback(move |_| {
100            let mut stream = stream.clone();
101            let mut state = state.clone();
102            let mut last_error = last_error.clone();
103
104            spawn(async move {
105                if let Err(e) = start_camera(&mut stream, &mut state, &mut last_error).await {
106                    let msg = e.to_string();
107                    state.set(CameraState::Error(msg.clone()));
108                    last_error.set(Some(msg));
109                }
110            });
111        })
112    };
113
114    let start_with_quality = {
115        let stream = stream.clone();
116        let state = state.clone();
117        let last_error = last_error.clone();
118
119        use_callback(move |quality: CameraQualityConfig| {
120            let mut stream = stream.clone();
121            let mut state = state.clone();
122            let mut last_error = last_error.clone();
123
124            spawn(async move {
125                if let Err(e) = start_camera_with_quality(
126                    &mut stream,
127                    &mut state,
128                    &mut last_error,
129                    Some(quality),
130                )
131                .await
132                {
133                    let msg = e.to_string();
134                    state.set(CameraState::Error(msg.clone()));
135                    last_error.set(Some(msg));
136                }
137            });
138        })
139    };
140
141    let stop = {
142        let mut stream = stream.clone();
143        let mut state = state.clone();
144
145        use_callback(move |_| {
146            stop_camera(&mut stream, &mut state);
147        })
148    };
149
150    Camera {
151        start,
152        start_with_quality,
153        stop,
154        stream,
155        state,
156        last_error,
157    }
158}
159
160async fn start_camera(
161    stream: &mut Signal<Option<MediaStream>>,
162    state: &mut Signal<CameraState>,
163    last_error: &mut Signal<Option<String>>,
164) -> Result<(), CameraError> {
165    start_camera_with_quality(stream, state, last_error, None).await
166}
167
168pub async fn start_camera_with_quality(
169    stream: &mut Signal<Option<MediaStream>>,
170    state: &mut Signal<CameraState>,
171    last_error: &mut Signal<Option<String>>,
172    quality: Option<CameraQualityConfig>,
173) -> Result<(), CameraError> {
174
175    state.set(CameraState::Starting);
176    
177    if let Some(s) = stream.read().as_ref() {
178        let tracks = s.get_tracks();
179        for i in 0..tracks.length() {
180            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
181                track.stop();
182            }
183        }
184    }
185    last_error.set(None);
186
187    let window = web_sys::window().ok_or(CameraError::WindowUnavailable)?;
188    let devices = window
189        .navigator()
190        .media_devices()
191        .map_err(|_| CameraError::MediaDevicesUnavailable)?;
192
193    let constraints = MediaStreamConstraints::new();
194
195    if let Some(q) = quality {
196        let video = MediaTrackConstraints::new();
197        video.set_width(&q.width.into());
198        video.set_height(&q.height.into());
199        video.set_frame_rate(&q.frame_rate.into());
200        constraints.set_video(&video.into());
201    } else {
202        constraints.set_video(&true.into());
203    }
204
205    let promise = devices
206        .get_user_media_with_constraints(&constraints)
207        .map_err(|e| CameraError::GetUserMediaFailed(format!("{e:?}")))?;
208
209    let js_val = JsFuture::from(promise)
210        .await
211        .map_err(|e| CameraError::GetUserMediaFailed(format!("{e:?}")))?;
212
213    let s: MediaStream = js_val
214        .dyn_into()
215        .map_err(|_| CameraError::CastMediaStreamFailed)?;
216
217    stream.set(Some(s));
218    state.set(CameraState::Active);
219    Ok(())
220}
221
222fn stop_camera(stream: &mut Signal<Option<MediaStream>>, state: &mut Signal<CameraState>) {
223    state.set(CameraState::Stopping);
224
225    if let Some(s) = stream.read().as_ref() {
226        let tracks = s.get_tracks();
227        for i in 0..tracks.length() {
228            if let Ok(track) = tracks.get(i).dyn_into::<MediaStreamTrack>() {
229                track.stop();
230            }
231        }
232    }
233
234    stream.set(None);
235    state.set(CameraState::Idle);
236}