use crate::*;
pub(crate) fn use_camera() -> UseCamera {
UseCamera {
camera_open: use_signal(|| false),
camera_loading: use_signal(|| false),
error_message: use_signal(String::new),
facing: use_signal(|| CameraFacing::Environment),
scan_result: use_signal(String::new),
scan_handle: use_signal(|| None),
}
}
pub(crate) fn open_camera(video_selector: &str, facing: CameraFacing) -> Result<(), String> {
let window_value: Window = window().expect("no global window exists");
let navigator: Navigator = window_value.navigator();
let media_devices: MediaDevices = navigator
.media_devices()
.map_err(|error: JsValue| format!("{error:?}"))?;
let constraints: MediaStreamConstraints = MediaStreamConstraints::new();
let facing_mode: &str = match facing {
CameraFacing::User => CAMERA_FACING_MODE_USER,
CameraFacing::Environment => CAMERA_FACING_MODE_ENVIRONMENT,
};
let video_constraint: js_sys::Object = js_sys::Object::new();
let _ = Reflect::set(
&video_constraint,
&JsValue::from_str("facingMode"),
&JsValue::from_str(facing_mode),
);
constraints.set_video(&video_constraint);
constraints.set_audio(&JsValue::from_bool(false));
let promise: js_sys::Promise = media_devices
.get_user_media_with_constraints(&constraints)
.map_err(|error: JsValue| format!("{error:?}"))?;
let selector: String = video_selector.to_string();
let on_fulfilled: Closure<dyn FnMut(JsValue)> =
Closure::wrap(Box::new(move |stream_value: JsValue| {
let stream: MediaStream = stream_value.unchecked_into();
let document: Document = window()
.expect("no global window exists")
.document()
.expect("should have a document");
if let Some(element) = document.query_selector(&selector).ok().flatten() {
let video_element: HtmlVideoElement = element.unchecked_into();
video_element.set_src_object(Some(&stream));
let _ = video_element.play();
}
}));
let on_rejected: Closure<dyn FnMut(JsValue)> =
Closure::wrap(Box::new(move |error: JsValue| {
web_sys::console::log_2(&wasm_bindgen::JsValue::from_str("[euv-camera]"), &error);
}));
let _ = promise.then(&on_fulfilled).catch(&on_rejected);
on_fulfilled.forget();
on_rejected.forget();
Ok(())
}
pub(crate) fn close_camera(video_selector: &str) {
let window_value: Window = window().expect("no global window exists");
let document: Document = window_value.document().expect("should have a document");
if let Some(element) = document.query_selector(video_selector).ok().flatten() {
let video_element: HtmlVideoElement = element.unchecked_into();
if let Some(stream) = video_element.src_object() {
let stream: MediaStream = stream.unchecked_into();
let tracks: js_sys::Array = stream.get_tracks();
for track_value in tracks.iter() {
let track: MediaStreamTrack = track_value.unchecked_into();
track.stop();
}
}
video_element.set_src_object(None);
}
}
pub(crate) fn switch_camera(state: UseCamera) {
close_camera(CAMERA_VIDEO_SELECTOR);
state.camera_open.set(false);
let new_facing: CameraFacing = match state.facing.get() {
CameraFacing::User => CameraFacing::Environment,
CameraFacing::Environment => CameraFacing::User,
};
state.facing.set(new_facing);
state.camera_loading.set(true);
state.error_message.set(String::new());
let result: Result<(), String> = open_camera(CAMERA_VIDEO_SELECTOR, new_facing);
match result {
Ok(()) => {
state.camera_open.set(true);
state.camera_loading.set(false);
}
Err(error) => {
state.error_message.set(error);
state.camera_loading.set(false);
}
}
}
pub(crate) fn start_qr_scan(state: UseCamera) {
let window_value: Window = window().expect("no global window exists");
let barcode_detector_key: JsValue = JsValue::from_str("BarcodeDetector");
if Reflect::get(&window_value, &barcode_detector_key).is_err() {
state
.error_message
.set("BarcodeDetector API is not supported in this browser".to_string());
return;
}
let detector_result: Result<JsValue, JsValue> =
js_sys::Function::new_no_args("return new BarcodeDetector({ formats: ['qr_code'] })")
.call0(&JsValue::NULL);
let detector: JsValue = match detector_result {
Ok(value) => value,
Err(error) => {
state
.error_message
.set(format!("Failed to create BarcodeDetector: {error:?}"));
return;
}
};
let handle: IntervalHandle = use_interval(CAMERA_SCAN_INTERVAL_MILLIS, move || {
let document: Document = window()
.expect("no global window exists")
.document()
.expect("should have a document");
let Some(element) = document
.query_selector(CAMERA_VIDEO_SELECTOR)
.ok()
.flatten()
else {
return;
};
let video_element: HtmlVideoElement = element.unchecked_into();
if video_element.ready_state() != HtmlMediaElement::HAVE_ENOUGH_DATA {
return;
}
let detect_fn: js_sys::Function = Reflect::get(&detector, &JsValue::from_str("detect"))
.ok()
.and_then(|value: JsValue| value.dyn_into::<js_sys::Function>().ok())
.unwrap_or_else(|| js_sys::Function::new_no_args("return Promise.resolve([])"));
let promise: js_sys::Promise = match detect_fn.call1(&detector, &video_element) {
Ok(result) => result.into(),
Err(_) => return,
};
let scan_state: UseCamera = state;
let on_detected: Closure<dyn FnMut(JsValue)> =
Closure::wrap(Box::new(move |barcodes_value: JsValue| {
let barcodes: js_sys::Array = match barcodes_value.dyn_into::<js_sys::Array>() {
Ok(array) => array,
Err(_) => return,
};
if barcodes.length() == 0 {
return;
}
if let Some(first) = barcodes.get(0).as_string() {
scan_state.scan_result.set(first.clone());
if is_url(&first) {
stop_qr_scan(scan_state);
close_camera(CAMERA_VIDEO_SELECTOR);
scan_state.camera_open.set(false);
let window_value: Window = window().expect("no global window exists");
let _ = window_value.location().set_href(&first);
}
} else if let Ok(raw_value) =
Reflect::get(&barcodes.get(0), &JsValue::from_str("rawValue"))
&& let Some(text) = raw_value.as_string()
{
scan_state.scan_result.set(text.clone());
if is_url(&text) {
stop_qr_scan(scan_state);
close_camera(CAMERA_VIDEO_SELECTOR);
scan_state.camera_open.set(false);
let window_value: Window = window().expect("no global window exists");
let _ = window_value.location().set_href(&text);
}
}
}));
let on_scan_error: Closure<dyn FnMut(JsValue)> =
Closure::wrap(Box::new(move |_error: JsValue| {}));
let _ = promise.then(&on_detected).catch(&on_scan_error);
on_detected.forget();
on_scan_error.forget();
});
state.scan_handle.set(Some(handle));
}
pub(crate) fn stop_qr_scan(state: UseCamera) {
if let Some(handle) = state.scan_handle.get() {
handle.clear();
state.scan_handle.set(None);
}
}
pub(crate) fn is_url(text: &str) -> bool {
text.starts_with(CAMERA_URL_PREFIX_HTTP) || text.starts_with(CAMERA_URL_PREFIX_HTTPS)
}
pub(crate) fn camera_cleanup(state: UseCamera) {
use_cleanup(move || {
stop_qr_scan(state);
close_camera(CAMERA_VIDEO_SELECTOR);
state.camera_open.set(false);
state.camera_loading.set(false);
state.error_message.set(String::new());
state.scan_result.set(String::new());
});
}