var debug_taps = 0; var debug_logs = []; var track; var frame_width; var frame_height; var scanguide_anime; var busy = false; const max_inflight = 5; var should_check_auto_torch = true; var done_checking_auto_torch = false; var camera_sensitivity = 1.0; var camera_capabilities = null; var Module = { onRuntimeInitialized: start, }; function get_query(key) { const qs = window.location.search; const params = new URLSearchParams(qs); return params.get(key) || ""; } function show_debug() { const debug_div = document.getElementById("debug_div"); debug_div.classList.remove("hidden"); } function hide_debug() { const debug_div = document.getElementById("debug_div"); debug_div.classList.add("hidden"); } function debug_countdown() { debug_taps += 1; if (debug_taps > 10) { show_debug(); } } function hide_loading() { const loading = document.getElementById("loading"); loading.classList.add("hidden"); } function handle_stream(stream) { const video = document.querySelector('video'); const videoTracks = stream.getVideoTracks(); debug_log(`total video tracks: ${videoTracks.length} Using video device: ${videoTracks[0].label}`); window.stream = stream; // make variable available to browser console video.srcObject = stream; video.play(); track = videoTracks[0]; camera_capabilities = track.getCapabilities(); const caps = document.getElementById("caps"); caps.innerHTML = JSON.stringify(camera_capabilities); const settings = track.getSettings(); console.log(settings); const canvas = document.getElementById("original"); frame_width = settings.width; frame_height = settings.height; video.width = frame_width; video.height = frame_height; canvas.width = frame_width; canvas.height = frame_height; } function handleError(error) { debug_log(error); console.log(error); } function debug_log(msg) { debug_logs.push(Date.now() / 1000 + ": " + msg); const nentries = 20; while (debug_logs.length > nentries) { debug_logs.shift(); } var output = ""; for (var l of debug_logs) { output = `
${l}
` + output; } console.log(msg); const di = document.getElementById("logs"); di.innerHTML = output; } async function start_camera(e) { try { const constraints = window.constraints = { audio: false, video: { facingMode: "environment", focusDistance: 0.12, focusMode: "manual", width: { ideal: 2000 }, }, }; const supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); const cons = document.getElementById("cons"); cons.innerHTML = JSON.stringify(supportedConstraints); debug_log(navigator.userAgent); const stream = await navigator.mediaDevices.getUserMedia(constraints); handle_stream(stream); const zoom = get_query("zoom"); if (zoom) { set_zoom(zoom); } } catch (e) { handleError(e); } } function is_emblem_qr_pattern(p) { if (p.search(/code=[0-9a-zA-Z]+/) >= 0) return true; if (p.search(/c=[0-9a-zA-Z]+/) >= 0) return true; if (p.search(/https:\/\/xy.ltd\/v\/[0-9a-zA-Z]+/) >= 0) return true; return false; } function make_hint_text(r) { var qr_is_valid = false; if (r.qrcode && r.qrcode.length > 0) { qr_is_valid = is_emblem_qr_pattern(r.qrcode); if (!qr_is_valid) { return "无效编码"; } } if (qr_is_valid) { var err = r.err || ""; if (err.includes("margin too small")) { return "对齐定位点"; } else if (err.includes("energy check failed") || err.includes("cannot detect angle")) { return "移近一点"; } } return "对齐定位点"; } function handle_frame() { try { do_handle_frame(); } catch (e) { debug_log("handle frame exception: " + e); } setTimeout(handle_frame, 20); } image_data_urls = []; function do_handle_frame() { if (busy) return; const canvas = document.getElementById("original"); const video = document.getElementById("video"); canvas.width = video.width; canvas.height = video.height; canvas.style.width = video.width / 4 + "px"; canvas.style.height = video.height / 4 + "px"; const ctx = canvas.getContext("2d"); ctx.drawImage(video, 0, 0); const id = ctx.getImageData(0, 0, canvas.width, canvas.height); console.log(Module); var buf = Module._malloc(id.data.length * id.data.BYTES_PER_ELEMENT); Module.HEAPU8.set(id.data, buf); var r = Module.ccall('qrtool_angle', 'string', ['number', 'number', 'number', 'number', 'number'], [buf, id.width, id.height, 0, camera_sensitivity]); Module._free(buf); debug_log(r); const res = JSON.parse(r); const is_valid_pattern = res.qrcode && res.qrcode.length && is_emblem_qr_pattern(res.qrcode); if (res.qrcode && should_check_auto_torch) { start_check_auto_torch(res.qrcode); } if (done_checking_auto_torch && is_valid_pattern && res.ok) { var data_url = canvas.toDataURL("image/jpeg", 1.0); image_data_urls.push(data_url); if (image_data_urls.length >= 3) { submit_image(res.qrcode, res.angle, image_data_urls); image_data_urls = []; } else { pending_hint = make_hint_text(res); } } else { pending_hint = make_hint_text(res); } } async function start_check_auto_torch(qrcode) { should_check_auto_torch = false; var r = await fetch("https://themblem.com/api/v1/check-auto-torch/?qrcode=" + encodeURIComponent(qrcode), { method: "GET", }); var d = await r.json(); debug_log(JSON.stringify(d)); if (d.enable_auto_torch && !torch) { toggle_torch(); } camera_sensitivity = d.camera_sensitivity || 1.0; setTimeout(() => { done_checking_auto_torch = true; }, 200); } function set_zoom(zoom) { if (camera_capabilities.zoom) { debug_log("set zoom by applying constraints"); track.applyConstraints({advanced: [ {zoom} ]}); } else { debug_log("set zoom by scaling video element"); add_style_by_query("video.preview", "transform", `scale(${zoom})`); } } let torch = false; function toggle_torch() { torch = !torch; track.applyConstraints({advanced: [ {torch: torch} ]}); if (torch) { add_class_by_query(".bottomfixed .torch.action", "highlight"); } else { remove_class_by_query(".bottomfixed .torch.action", "highlight"); } } function show_spinner() { remove_class_by_query('.verifyspin', 'hidden'); remove_class_by_query('.spin-image', 'spin-only'); remove_class_by_query('.spin-image', 'spin-and-shrink'); setTimeout(() => { add_class_by_query('.spin-image', 'spin-and-shrink'); setTimeout(() => { remove_class_by_query('.spin-image', 'spin-and-shrink'); add_class_by_query('.spin-image', 'spin-only'); }, 3000); }, 0); } function hide_spinner() { add_class_by_query('.verifyspin', 'hidden'); } async function submit_image(qrcode, angle, image_data_urls) { busy = true; var begin = Date.now(); show_spinner(); debug_log(`submit: qrcode: ${qrcode} angle: ${angle}`); try { // TODO: pass these parameters from query string var emblem_id = get_query("emblem_id"); var nick_name = get_query("nick_name"); var realip = get_query("realip"); var phonemodel = get_query("phonemodel"); var data = { emblem_id, nick_name, realip, qrcode, angle, phonemodel, image_data_urls, log: debug_logs.join("\n"), }; var r = await fetch("https://themblem.com/api/v1/qr-verify/", { method: "POST", body: JSON.stringify(data), headers: { 'Content-Type': 'application/json' }, }); var d = await r.json(); debug_log(JSON.stringify(d)); if (d.serial_code) { if (wx) { var delay = 3000 - (Date.now() - begin); setTimeout(() => { if (get_query("ai_chat_mode")) { wx.miniProgram.redirectTo({ url: '/pages/chat/chat?serial_code=' + d.serial_code, }); } else { wx.miniProgram.redirectTo({ url: '/pages/productinfo/productinfo?serial_code=' + d.serial_code, }); } }, delay > 0 ? delay : 0); } } else { this.show_modal('verifyfailed'); } } catch (e) { debug_log(`submission error: ${e}`); this.show_modal('verifyfailed'); } } var pending_hint = null; function update_hint() { var now = Date.now(); if (pending_hint) { const hint_dev = document.getElementById("hint"); hint_dev.innerHTML = pending_hint; pending_hint = null; } } function start() { console.log("start"); setTimeout(handle_frame, 100); hide_loading(); if (get_query("debug")) { show_debug(); } setInterval(update_hint, 1000); setTimeout(() => { show_tooltip(); }, 15000); } function show_tooltip() { remove_class_by_query(".tooltip", "hidden"); add_class_by_query(".bottomfixed .action", "highlight"); } function hide_tooltip() { add_class_by_query(".tooltip", "hidden"); remove_class_by_query(".bottomfixed .action", "highlight"); } async function init_scanguide() { var r = await fetch('https://emblem-resources.oss-cn-guangzhou.aliyuncs.com/scan-guide-1080x1920-3.json'); var d = await r.json(); console.log("start scanguide", d); const elem = document.getElementById("scanguide"); scanguide_anime = bodymovin.loadAnimation({ container: elem, animationData: d, // path: 'data.json', renderer: 'svg', loop: true, autoplay: true, name: "Scan guide", }) } function add_style_by_query(query, property, style) { var list = document.querySelectorAll(query); for (var i = 0; i < list.length; ++i) { list[i].style[property] = style; } } function add_class_by_query(query, to_add) { var list = document.querySelectorAll(query); for (var i = 0; i < list.length; ++i) { list[i].classList.add(to_add); } } function remove_class_by_query(query, to_remove) { var list = document.querySelectorAll(query); for (var i = 0; i < list.length; ++i) { list[i].classList.remove(to_remove); } } var modals = ['serviceqr', 'scanguide', 'verifyfailed', 'verifyspin']; function hide_modal() { busy = false; scanguide_anime.stop(); add_class_by_query(".modal", "hidden"); hide_tooltip(); } function show_modal(which) { busy = true; hide_tooltip(); hide_spinner(); add_class_by_query(".verifyspin", "hidden"); remove_class_by_query(".modal", "hidden"); remove_class_by_query('.actions', 'hidden'); for (var m of modals) { add_class_by_query('.' + m, 'hidden'); } if (which == 'scanguide') { scanguide_anime.goToAndPlay(0); remove_class_by_query('.scanguide', 'hidden'); } if (which == 'serviceqr') { remove_class_by_query('.serviceqr', 'hidden'); add_class_by_query('.actions', 'hidden'); } if (which == 'verifyfailed') { remove_class_by_query('.verifyfailed', 'hidden'); } } function set_service_qr_img() { var tid = get_query('tenant'); var url = '/api/v1/service-qr/?tenant=' + tid; const elem = document.getElementById("service_img"); elem.src = url; } (function() { start_camera(); init_scanguide(); set_service_qr_img(); })();