yew_scanner/
lib.rs

1use bardecoder::default_decoder;
2use gloo::timers::callback::Interval;
3use gloo::utils::errors::JsError;
4use gloo::utils::window;
5use js_sys::Uint8Array;
6use std::sync::Arc;
7use wasm_bindgen::closure::Closure;
8use wasm_bindgen::JsCast;
9use web_sys::{
10    Blob, CanvasRenderingContext2d, HtmlCanvasElement, HtmlVideoElement, MediaStream,
11    MediaStreamConstraints, MediaTrackConstraints, VideoFacingModeEnum,
12};
13use yew::prelude::*;
14
15pub struct Scanner {
16    video_ref: NodeRef,
17    canvas_ref: NodeRef,
18    stream: Option<MediaStream>,
19    scanner_interval: Option<Interval>,
20    canvas_closure: Option<Arc<Closure<dyn Fn(Blob) -> ()>>>,
21}
22
23pub enum ScannerMessage {
24    ReceivedStream(MediaStream),
25    CapturedImage(Blob),
26    Error(JsError),
27    ImageDone,
28}
29
30#[derive(Properties, PartialEq, Clone)]
31pub struct ScannerProps {
32    #[prop_or_default]
33    pub onscan: Callback<String>,
34    #[prop_or_default]
35    pub onerror: Callback<JsError>,
36}
37
38impl Component for Scanner {
39    type Message = ScannerMessage;
40    type Properties = ScannerProps;
41
42    fn create(ctx: &Context<Self>) -> Self {
43        ctx.link().send_future(async {
44            let mut constraints = MediaStreamConstraints::new();
45            let mut video_constraints = MediaTrackConstraints::new();
46            video_constraints
47                .facing_mode(&VideoFacingModeEnum::Environment.into())
48                .frame_rate(&4.into());
49            constraints.video(&video_constraints);
50            match window().navigator().media_devices() {
51                Ok(devs) => match devs.get_user_media_with_constraints(&constraints) {
52                    Ok(promise) => match wasm_bindgen_futures::JsFuture::from(promise).await {
53                        Ok(stream) => ScannerMessage::ReceivedStream(stream.unchecked_into()),
54                        Err(e) => ScannerMessage::Error(JsError::try_from(e).unwrap()),
55                    },
56                    Err(e) => ScannerMessage::Error(JsError::try_from(e).unwrap()),
57                },
58                Err(e) => ScannerMessage::Error(JsError::try_from(e).unwrap()),
59            }
60        });
61        Self {
62            video_ref: NodeRef::default(),
63            canvas_ref: NodeRef::default(),
64            stream: None,
65            scanner_interval: None,
66            canvas_closure: None,
67        }
68    }
69
70    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
71        match msg {
72            ScannerMessage::ReceivedStream(stream) => {
73                self.stream = Some(stream);
74                true
75            }
76            ScannerMessage::CapturedImage(image_src) => {
77                let decoder = default_decoder();
78                let callback = ctx.props().onscan.clone();
79                let link = ctx.link().clone();
80                ctx.link().send_future(async move {
81                    match wasm_bindgen_futures::JsFuture::from(image_src.array_buffer()).await {
82                        Ok(array_buffer) => {
83                            let array = Uint8Array::new(&array_buffer);
84                            let bytes: Vec<u8> = array.to_vec();
85                            match image::load_from_memory(&bytes) {
86                                Ok(image) => {
87                                    for decode_result in decoder.decode(&image).iter() {
88                                        match decode_result {
89                                            Ok(s) => callback.emit(s.clone()),
90                                            Err(e) => link.send_message(ScannerMessage::Error(
91                                                JsError::from(js_sys::Error::new(
92                                                    e.to_string().as_str(),
93                                                )),
94                                            )),
95                                        }
96                                    }
97                                    ScannerMessage::ImageDone
98                                }
99                                Err(e) => {
100                                    let error = js_sys::Error::new(e.to_string().as_str());
101                                    ScannerMessage::Error(JsError::from(error))
102                                }
103                            }
104                        }
105                        Err(e) => ScannerMessage::Error(JsError::try_from(e).unwrap()),
106                    }
107                });
108                false
109            }
110            ScannerMessage::Error(e) => {
111                ctx.props().onerror.emit(e);
112                false
113            }
114            ScannerMessage::ImageDone => false,
115        }
116    }
117
118    fn view(&self, _ctx: &Context<Self>) -> Html {
119        let html = html! {
120            <>
121            <video ref={&self.video_ref} autoPlay="true" style="width:300px;height:300px;" />
122            <canvas ref={&self.canvas_ref} width="1920" height="1080" style="display: none;"></canvas>
123            </>
124        };
125        html
126    }
127
128    fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
129        let video = self
130            .video_ref
131            .cast::<HtmlVideoElement>()
132            .expect("video should be an HtmlVideoElement");
133        video.set_src_object(self.stream.as_ref().clone());
134        let canvas = self
135            .canvas_ref
136            .cast::<HtmlCanvasElement>()
137            .expect("canvas should be an HtmlCanvasElement");
138        let context = canvas
139            .get_context("2d")
140            .expect("context should be available")
141            .unwrap()
142            .unchecked_into::<CanvasRenderingContext2d>();
143
144        let link = ctx.link().clone();
145        let link_callback = link.clone();
146        self.canvas_closure = Some(Arc::new(Closure::wrap(Box::new(move |blob: Blob| {
147            link_callback.send_message(ScannerMessage::CapturedImage(blob));
148        }) as Box<dyn Fn(Blob)>)));
149        let canvas_closure_ref = self.canvas_closure.as_ref().unwrap().clone();
150        self.scanner_interval = Some(Interval::new(500, move || {
151            context
152                .draw_image_with_html_video_element_and_dw_and_dh(&video, 0.0, 0.0, 1920.0, 1080.0)
153                .expect("rendering to canvas should work");
154            canvas
155                .to_blob(canvas_closure_ref.as_ref().as_ref().unchecked_ref())
156                .expect("getting blob failed");
157        }));
158    }
159}