euv_ui/component/camera/hook/
impl.rs1use super::*;
2
3impl UseEuvCamera {
5 pub fn use_camera_state() -> UseEuvCamera {
11 UseEuvCamera::new(
12 App::use_signal(|| false),
13 App::use_signal(|| false),
14 App::use_signal(String::new),
15 App::use_signal(EuvCameraFacing::default),
16 App::use_signal(String::new),
17 App::use_signal(|| None),
18 )
19 }
20
21 pub(crate) fn open(video_selector: &str, facing: EuvCameraFacing) -> Result<(), String> {
38 let window_value: Window = window().expect("no global window exists");
39 let navigator: Navigator = window_value.navigator();
40 let media_devices: MediaDevices = navigator
41 .media_devices()
42 .map_err(|error: JsValue| format!("{error:?}"))?;
43 let constraints: MediaStreamConstraints = MediaStreamConstraints::new();
44 let facing_mode: &str = match facing {
45 EuvCameraFacing::User => CAMERA_FACING_MODE_USER,
46 EuvCameraFacing::Environment => CAMERA_FACING_MODE_ENVIRONMENT,
47 };
48 let video_constraint: Object = Object::new();
49 let _: Result<bool, JsValue> = Reflect::set(
50 &video_constraint,
51 &JsValue::from_str("facingMode"),
52 &JsValue::from_str(facing_mode),
53 );
54 constraints.set_video(&video_constraint);
55 constraints.set_audio(&JsValue::from_bool(false));
56 let promise: Promise = media_devices
57 .get_user_media_with_constraints(&constraints)
58 .map_err(|error: JsValue| format!("{error:?}"))?;
59 let selector: String = video_selector.to_string();
60 let on_fulfilled: Closure<dyn FnMut(JsValue)> =
61 Closure::wrap(Box::new(move |stream_value: JsValue| {
62 let stream: MediaStream = stream_value.unchecked_into();
63 let document: Document = window()
64 .expect("no global window exists")
65 .document()
66 .expect("should have a document");
67 if let Some(element) = document.query_selector(&selector).ok().flatten() {
68 let video_element: HtmlVideoElement = element.unchecked_into();
69 video_element.set_src_object(Some(&stream));
70 let _: Result<Promise, JsValue> = video_element.play();
71 }
72 }));
73 let on_rejected: Closure<dyn FnMut(JsValue)> =
74 Closure::wrap(Box::new(move |error: JsValue| {
75 web_sys::console::log_2(&wasm_bindgen::JsValue::from_str("[euv-camera]"), &error);
76 }));
77 let _: Promise = promise.then(&on_fulfilled).catch(&on_rejected);
78 on_fulfilled.forget();
79 on_rejected.forget();
80 Ok(())
81 }
82
83 pub(crate) fn close(video_selector: &str) {
93 let window_value: Window = window().expect("no global window exists");
94 let document: Document = window_value.document().expect("should have a document");
95 if let Some(element) = document.query_selector(video_selector).ok().flatten() {
96 let video_element: HtmlVideoElement = element.unchecked_into();
97 if let Some(stream) = video_element.src_object() {
98 let stream: MediaStream = stream.unchecked_into();
99 let tracks: Array = stream.get_tracks();
100 for track_value in tracks.iter() {
101 let track: MediaStreamTrack = track_value.unchecked_into();
102 track.stop();
103 }
104 }
105 video_element.set_src_object(None);
106 }
107 }
108
109 pub(crate) fn open_and_scan(self, config: Option<&EuvCameraConfig>) {
118 let cfg: EuvCameraConfig =
119 config.map_or_else(EuvCameraConfig::default, |c: &EuvCameraConfig| c.clone());
120 self.get_camera_loading().set(true);
121 self.get_error_message().set(String::new());
122 self.get_scan_result().set(String::new());
123 let facing: EuvCameraFacing = self.get_facing().get();
124 let result: Result<(), String> = Self::open(cfg.video_selector, facing);
125 match result {
126 Ok(()) => {
127 self.get_camera_open().set(true);
128 self.get_camera_loading().set(false);
129 if cfg.auto_scan {
130 self.start_qr_scan(config);
131 }
132 }
133 Err(error) => {
134 self.get_error_message().set(error);
135 self.get_camera_loading().set(false);
136 if let Some(ref on_error) = cfg.on_error {
137 on_error(self.get_error_message().get());
138 }
139 }
140 }
141 }
142
143 pub(crate) fn switch(self, config: Option<&EuvCameraConfig>) {
153 let cfg: EuvCameraConfig =
154 config.map_or_else(EuvCameraConfig::default, |c: &EuvCameraConfig| c.clone());
155 self.stop_qr_scan();
156 Self::close(cfg.video_selector);
157 self.get_camera_open().set(false);
158 let new_facing: EuvCameraFacing = match self.get_facing().get() {
159 EuvCameraFacing::User => EuvCameraFacing::Environment,
160 EuvCameraFacing::Environment => EuvCameraFacing::User,
161 };
162 self.get_facing().set(new_facing);
163 self.get_camera_loading().set(true);
164 self.get_error_message().set(String::new());
165 let result: Result<(), String> = Self::open(cfg.video_selector, new_facing);
166 match result {
167 Ok(()) => {
168 self.get_camera_open().set(true);
169 self.get_camera_loading().set(false);
170 if cfg.auto_scan {
171 self.start_qr_scan(config);
172 }
173 }
174 Err(error) => {
175 self.get_error_message().set(error);
176 self.get_camera_loading().set(false);
177 }
178 }
179 }
180
181 pub(crate) fn start_qr_scan(self, config: Option<&EuvCameraConfig>) {
193 let cfg: EuvCameraConfig =
194 config.map_or_else(EuvCameraConfig::default, |c: &EuvCameraConfig| c.clone());
195 let window_value: Window = window().expect("no global window exists");
196 let barcode_detector_key: JsValue = JsValue::from_str("BarcodeDetector");
197 let barcode_detector_constructor: Function =
198 match Reflect::get(&window_value, &barcode_detector_key) {
199 Ok(value) if !value.is_undefined() && !value.is_null() => value.unchecked_into(),
200 _ => {
201 self.get_error_message()
202 .set("BarcodeDetector API is not supported in this browser".to_string());
203 return;
204 }
205 };
206 let formats_array: Array = Array::new();
207 formats_array.push(&JsValue::from_str("qr_code"));
208 let init_object: Object = Object::new();
209 let _: Result<bool, JsValue> =
210 Reflect::set(&init_object, &JsValue::from_str("formats"), &formats_array);
211 let args_array: Array = Array::new();
212 args_array.push(&init_object.into());
213 let detector: JsValue = match Reflect::construct(&barcode_detector_constructor, &args_array)
214 {
215 Ok(value) => value,
216 Err(error) => {
217 self.get_error_message()
218 .set(format!("Failed to create BarcodeDetector: {error:?}"));
219 return;
220 }
221 };
222 let video_selector: Rc<String> = Rc::new(cfg.video_selector.to_string());
223 let on_qr_detected: Option<QrDetectedCallback> = cfg.on_qr_detected.clone();
224 let handle: IntervalHandle = App::use_interval(cfg.scan_interval_millis, move || {
225 let document: Document = window()
226 .expect("no global window exists")
227 .document()
228 .expect("should have a document");
229 let Some(element) = document.query_selector(&video_selector).ok().flatten() else {
230 return;
231 };
232 let video_element: HtmlVideoElement = element.unchecked_into();
233 if video_element.ready_state() != HtmlMediaElement::HAVE_ENOUGH_DATA {
234 return;
235 }
236 let detect_fn: Function = Reflect::get(&detector, &JsValue::from_str("detect"))
237 .ok()
238 .and_then(|value: JsValue| value.dyn_into::<Function>().ok())
239 .unwrap_or_else(|| Function::new_no_args("return Promise.resolve([])"));
240 let promise: Promise = match detect_fn.call1(&detector, &video_element) {
241 Ok(result) => result.into(),
242 Err(_) => return,
243 };
244 let on_qr_detected_clone: Option<QrDetectedCallback> = on_qr_detected.clone();
245 let video_selector_clone: Rc<String> = video_selector.clone();
246 let on_detected: Closure<dyn FnMut(JsValue)> =
247 Closure::wrap(Box::new(move |barcodes_value: JsValue| {
248 let barcodes: Array = match barcodes_value.dyn_into::<Array>() {
249 Ok(array) => array,
250 Err(_) => return,
251 };
252 if barcodes.length() == 0 {
253 return;
254 }
255 let text: Option<String> = barcodes.get(0).as_string().or_else(|| {
256 Reflect::get(&barcodes.get(0), &JsValue::from_str("rawValue"))
257 .ok()
258 .and_then(|v: JsValue| v.as_string())
259 });
260 if let Some(text) = text {
261 self.get_scan_result().set(text.clone());
262 if let Some(ref callback) = on_qr_detected_clone {
263 callback(&text);
264 }
265 if Self::is_valid_qr_url(&text) {
266 self.stop_qr_scan();
267 Self::close(&video_selector_clone);
268 self.get_camera_open().set(false);
269 Self::navigate_qr_url(&text);
270 }
271 }
272 }));
273 let on_scan_error: Closure<dyn FnMut(JsValue)> =
274 Closure::wrap(Box::new(move |_error: JsValue| {}));
275 let _: Promise = promise.then(&on_detected).catch(&on_scan_error);
276 on_detected.forget();
277 on_scan_error.forget();
278 });
279 self.get_scan_handle().set(Some(handle));
280 }
281
282 pub(crate) fn stop_qr_scan(self) {
284 if let Some(handle) = self.get_scan_handle().get() {
285 handle.clear();
286 self.get_scan_handle().set(None);
287 }
288 }
289
290 pub(crate) fn is_valid_qr_url(text: &str) -> bool {
303 text.starts_with(CAMERA_URL_PREFIX_HTTP) || text.starts_with(CAMERA_URL_PREFIX_HTTPS)
304 }
305
306 pub(crate) fn extract_hostname(url: &str) -> String {
321 let rest: &str = if let Some(stripped) = url.strip_prefix(CAMERA_URL_PREFIX_HTTPS) {
322 stripped
323 } else if let Some(stripped) = url.strip_prefix(CAMERA_URL_PREFIX_HTTP) {
324 stripped
325 } else {
326 return String::new();
327 };
328 let authority: &str = rest.split('/').next().unwrap_or("");
329 let host_with_brackets: &str = authority.split(':').next().unwrap_or("");
330 if let Some(stripped) = host_with_brackets.strip_prefix('[')
331 && let Some(inner) = stripped.strip_suffix(']')
332 {
333 return inner.to_string();
334 }
335 host_with_brackets.to_string()
336 }
337
338 pub(crate) fn is_private_host(hostname: &str) -> bool {
353 if hostname.is_empty() {
354 return false;
355 }
356 if hostname.eq_ignore_ascii_case(CAMERA_LOCALHOST_HOSTNAME) {
357 return true;
358 }
359 let octets: Vec<&str> = hostname.split('.').collect();
360 if octets.len() != 4 {
361 return false;
362 }
363 let Ok(first) = octets[0].parse::<u8>() else {
364 return false;
365 };
366 let Ok(second) = octets[1].parse::<u8>() else {
367 return false;
368 };
369 if first == 127 {
370 return true;
371 }
372 if first == 10 {
373 return true;
374 }
375 if first == 172 && (16..=31).contains(&second) {
376 return true;
377 }
378 if first == 192 && second == 168 {
379 return true;
380 }
381 if first == 169 && second == 254 {
382 return true;
383 }
384 false
385 }
386
387 pub(crate) fn navigate_qr_url(url: &str) {
400 let window_value: Window = window().expect("no global window exists");
401 let location: Location = window_value.location();
402 let current_hostname: String = location.hostname().unwrap_or_default();
403 let url_hostname: String = Self::extract_hostname(url);
404 if url_hostname == current_hostname
405 && let Some(fragment) = url.split('#').nth(1)
406 {
407 let route: &str = if fragment.is_empty() { "/" } else { fragment };
408 Router::navigate(route);
409 return;
410 }
411 if Self::is_private_host(&url_hostname) {
412 let _: Result<(), JsValue> = window_value.location().set_href(url);
413 return;
414 }
415 if let Ok(open_fn) = Reflect::get(&window_value, &JsValue::from_str("open"))
416 .and_then(|value: JsValue| value.dyn_into::<Function>())
417 {
418 let _: Result<JsValue, JsValue> = open_fn.call2(
419 &window_value,
420 &JsValue::from_str(url),
421 &JsValue::from_str(SYSTEM_BROWSER_TARGET),
422 );
423 }
424 }
425
426 pub fn on_close(self, config: Option<&EuvCameraConfig>) -> Option<Rc<dyn Fn(Event)>> {
437 let cfg: EuvCameraConfig =
438 config.map_or_else(EuvCameraConfig::default, |c: &EuvCameraConfig| c.clone());
439 Some(Rc::new(move |_: Event| {
440 self.stop_qr_scan();
441 Self::close(cfg.video_selector);
442 self.get_camera_open().set(false);
443 self.get_scan_result().set(String::new());
444 }))
445 }
446
447 pub fn on_switch(self, config: Option<&EuvCameraConfig>) -> Option<Rc<dyn Fn(Event)>> {
457 let cfg: EuvCameraConfig =
458 config.map_or_else(EuvCameraConfig::default, |c: &EuvCameraConfig| c.clone());
459 Some(Rc::new(move |_: Event| {
460 self.switch(Some(&cfg));
461 }))
462 }
463
464 pub fn on_open(self, config: Option<&EuvCameraConfig>) -> Option<Rc<dyn Fn(Event)>> {
474 let cfg: EuvCameraConfig =
475 config.map_or_else(EuvCameraConfig::default, |c: &EuvCameraConfig| c.clone());
476 Some(Rc::new(move |_: Event| {
477 self.open_and_scan(Some(&cfg));
478 }))
479 }
480
481 pub fn cleanup(self, config: Option<&EuvCameraConfig>) {
489 let cfg: EuvCameraConfig =
490 config.map_or_else(EuvCameraConfig::default, |c: &EuvCameraConfig| c.clone());
491 App::use_cleanup(move || {
492 self.stop_qr_scan();
493 Self::close(cfg.video_selector);
494 self.get_camera_open().set(false);
495 self.get_camera_loading().set(false);
496 self.get_error_message().set(String::new());
497 self.get_scan_result().set(String::new());
498 });
499 }
500}
501
502impl Default for EuvCameraConfig {
504 fn default() -> Self {
505 EuvCameraConfig {
506 video_selector: CAMERA_VIDEO_SELECTOR,
507 scan_interval_millis: CAMERA_SCAN_INTERVAL_MILLIS,
508 auto_scan: true,
509 on_qr_detected: None,
510 on_error: None,
511 }
512 }
513}