use crate::*;
pub(crate) fn use_camera() -> UseCamera {
UseCamera::new(
use_signal(|| false),
use_signal(|| false),
use_signal(String::new),
use_signal(|| CameraFacing::Environment),
use_signal(String::new),
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: Object = 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: 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: 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 open_camera_and_scan(state: UseCamera) {
state.get_camera_loading().set(true);
state.get_error_message().set(String::new());
state.get_scan_result().set(String::new());
let facing: CameraFacing = state.get_facing().get();
let result: Result<(), String> = open_camera(CAMERA_VIDEO_SELECTOR, facing);
match result {
Ok(()) => {
state.get_camera_open().set(true);
state.get_camera_loading().set(false);
start_qr_scan(state);
}
Err(error) => {
state.get_error_message().set(error);
state.get_camera_loading().set(false);
}
}
}
pub(crate) fn switch_camera(state: UseCamera) {
stop_qr_scan(state);
close_camera(CAMERA_VIDEO_SELECTOR);
state.get_camera_open().set(false);
let new_facing: CameraFacing = match state.get_facing().get() {
CameraFacing::User => CameraFacing::Environment,
CameraFacing::Environment => CameraFacing::User,
};
state.get_facing().set(new_facing);
state.get_camera_loading().set(true);
state.get_error_message().set(String::new());
let result: Result<(), String> = open_camera(CAMERA_VIDEO_SELECTOR, new_facing);
match result {
Ok(()) => {
state.get_camera_open().set(true);
state.get_camera_loading().set(false);
start_qr_scan(state);
}
Err(error) => {
state.get_error_message().set(error);
state.get_camera_loading().set(false);
}
}
}
pub(crate) fn navigate_qr_url(url: &str) {
let window_value: Window = window().expect("no global window exists");
let location: Location = window_value.location();
let current_hostname: String = location.hostname().unwrap_or_default();
let url_hostname: String = extract_hostname(url);
if url_hostname == current_hostname
&& let Some(fragment) = url.split('#').nth(1)
{
let route: &str = if fragment.is_empty() { "/" } else { fragment };
navigate(route);
return;
}
if is_private_host(&url_hostname) {
let _ = window_value.location().set_href(url);
return;
}
if let Ok(open_fn) = Reflect::get(&window_value, &JsValue::from_str("open"))
.and_then(|value: JsValue| value.dyn_into::<Function>())
{
let _ = open_fn.call2(
&window_value,
&JsValue::from_str(url),
&JsValue::from_str(CAMERA_SYSTEM_BROWSER_TARGET),
);
}
}
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
.get_error_message()
.set("BarcodeDetector API is not supported in this browser".to_string());
return;
}
let detector_result: Result<JsValue, JsValue> =
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
.get_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: Function = Reflect::get(&detector, &JsValue::from_str("detect"))
.ok()
.and_then(|value: JsValue| value.dyn_into::<Function>().ok())
.unwrap_or_else(|| Function::new_no_args("return Promise.resolve([])"));
let promise: Promise = match detect_fn.call1(&detector, &video_element) {
Ok(result) => result.into(),
Err(_) => return,
};
let on_detected: Closure<dyn FnMut(JsValue)> =
Closure::wrap(Box::new(move |barcodes_value: JsValue| {
let barcodes: Array = match barcodes_value.dyn_into::<Array>() {
Ok(array) => array,
Err(_) => return,
};
if barcodes.length() == 0 {
return;
}
if let Some(first) = barcodes.get(0).as_string() {
state.get_scan_result().set(first.clone());
if is_valid_qr_url(&first) {
stop_qr_scan(state);
close_camera(CAMERA_VIDEO_SELECTOR);
state.get_camera_open().set(false);
navigate_qr_url(&first);
}
} else if let Ok(raw_value) =
Reflect::get(&barcodes.get(0), &JsValue::from_str("rawValue"))
&& let Some(text) = raw_value.as_string()
{
state.get_scan_result().set(text.clone());
if is_valid_qr_url(&text) {
stop_qr_scan(state);
close_camera(CAMERA_VIDEO_SELECTOR);
state.get_camera_open().set(false);
navigate_qr_url(&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.get_scan_handle().set(Some(handle));
}
pub(crate) fn stop_qr_scan(state: UseCamera) {
if let Some(handle) = state.get_scan_handle().get() {
handle.clear();
state.get_scan_handle().set(None);
}
}
pub(crate) fn is_valid_qr_url(text: &str) -> bool {
text.starts_with(CAMERA_URL_PREFIX_HTTP) || text.starts_with(CAMERA_URL_PREFIX_HTTPS)
}
fn extract_hostname(url: &str) -> String {
let rest: &str = if let Some(stripped) = url.strip_prefix(CAMERA_URL_PREFIX_HTTPS) {
stripped
} else if let Some(stripped) = url.strip_prefix(CAMERA_URL_PREFIX_HTTP) {
stripped
} else {
return String::new();
};
let authority: &str = rest.split('/').next().unwrap_or("");
let host_with_brackets: &str = authority.split(':').next().unwrap_or("");
if let Some(stripped) = host_with_brackets.strip_prefix('[')
&& let Some(inner) = stripped.strip_suffix(']')
{
return inner.to_string();
}
host_with_brackets.to_string()
}
fn is_private_host(hostname: &str) -> bool {
if hostname.is_empty() {
return false;
}
if hostname.eq_ignore_ascii_case(CAMERA_LOCALHOST_HOSTNAME) {
return true;
}
let octets: Vec<&str> = hostname.split('.').collect();
if octets.len() != 4 {
return false;
}
let Ok(first) = octets[0].parse::<u8>() else {
return false;
};
let Ok(second) = octets[1].parse::<u8>() else {
return false;
};
if first == 127 {
return true;
}
if first == 10 {
return true;
}
if first == 172 && (16..=31).contains(&second) {
return true;
}
if first == 192 && second == 168 {
return true;
}
if first == 169 && second == 254 {
return true;
}
false
}
pub(crate) fn camera_cleanup(state: UseCamera) {
use_cleanup(move || {
stop_qr_scan(state);
close_camera(CAMERA_VIDEO_SELECTOR);
state.get_camera_open().set(false);
state.get_camera_loading().set(false);
state.get_error_message().set(String::new());
state.get_scan_result().set(String::new());
});
}