pryty_rustbrowser/
camera.rs1use 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}