emblemscanner: worker is okay

This commit is contained in:
Fam Zheng 2025-09-14 22:58:01 +01:00
parent 780acc13ce
commit 7d0e30656e
10 changed files with 5702 additions and 97 deletions

View File

@ -102,8 +102,8 @@ qrtool.wx.wasm.br: qrtool.wx.js
brotli -kf qrtool.wx.wasm brotli -kf qrtool.wx.wasm
install-scanner: qrtool.wx.wasm.br install-scanner: qrtool.wx.wasm.br
@cp -v qrtool.wx.js qrtool.wx.wasm.br ../scanner/assets @cp -v qrtool.wx.js qrtool.wx.wasm.br ../scanner/pages/emblemscanner/
@cp -v qrtool.wx.js ../scanner/worker @cp -v qrtool.wx.js ../scanner/pages/emblemscanner/worker/
install-web: qrtool.web.wasm install-web: qrtool.web.wasm
@cp -v qrtool.web.js qrtool.web.wasm ../web/public/camera-5.0/js/ @cp -v qrtool.web.js qrtool.web.wasm ../web/public/camera-5.0/js/

View File

@ -7,7 +7,7 @@ var performance = {
}; };
Module['instantiateWasm'] = (info, receiveInstance) => { Module['instantiateWasm'] = (info, receiveInstance) => {
console.log("loading wasm...", info); console.log("loading wasm...", info);
WebAssembly.instantiate("assets/qrtool.wx.wasm.br", info).then((result) => { WebAssembly.instantiate("pages/emblemscanner/qrtool.wx.wasm.br", info).then((result) => {
console.log("result:", result); console.log("result:", result);
var inst = result['instance']; var inst = result['instance'];
receiveInstance(inst); receiveInstance(inst);

View File

@ -21,5 +21,5 @@
"lazyCodeLoading": "requiredComponents", "lazyCodeLoading": "requiredComponents",
"sitemapLocation": "sitemap.json", "sitemapLocation": "sitemap.json",
"useExtendedLib": {}, "useExtendedLib": {},
"workers": "worker" "workers": "pages/emblemscanner/worker"
} }

View File

@ -68,8 +68,6 @@ Page({
should_check_auto_torch: true, should_check_auto_torch: true,
done_checking_auto_torch: false, done_checking_auto_torch: false,
camera_sensitivity: 1, camera_sensitivity: 1,
frame_uploaded: 0,
frame_upload_time_cost: 0,
qrarc_class: 'sm', qrarc_class: 'sm',
qrmarkers_class: 'hidden', qrmarkers_class: 'hidden',
frame_upload_interval_ms: 2000, frame_upload_interval_ms: 2000,
@ -98,7 +96,8 @@ Page({
// State machine: loading_rules -> loading_qrtool -> init_camera -> scanning -> verifying -> result // State machine: loading_rules -> loading_qrtool -> init_camera -> scanning -> verifying -> result
app_state: 'loading_rules', // 'loading_rules', 'loading_qrtool', 'init_camera', 'scanning', 'webview_scanning', 'verifying', 'result' app_state: 'loading_rules', // 'loading_rules', 'loading_qrtool', 'init_camera', 'scanning', 'webview_scanning', 'verifying', 'result'
scan_mode: 'unknown', // 'camera', 'webview' scan_mode: 'unknown', // 'camera', 'webview'
no_web_view: false // Override web-view rule, force native camera no_web_view: false, // Override web-view rule, force native camera
worker_processing: false // Track if worker is currently processing a frame
}, },
onLoad(options) { onLoad(options) {
@ -180,15 +179,25 @@ Page({
*/ */
setupWorker() { setupWorker() {
// Create worker with local worker file // Create worker with local worker file
this.worker = wx.createWorker('/pages/emblemscanner/worker/index.js'); this.worker = wx.createWorker('/pages/emblemscanner/worker/index.js', {
useExperimentalWorker: true,
});
this.worker.onMessage((msg) => { this.worker.onMessage((msg) => {
console.log('Worker message:', msg.type); console.log('Worker message:', msg.type);
if (msg.type === "result") { if (msg.type === "result") {
// Clear processing flag when we get a result
this.setData({ worker_processing: false });
const result = msg.res; const result = msg.res;
const processingTime = msg.processing_time; const processingTime = msg.processing_time;
// Add WASM response to debug messages
if (this.data.enable_debug) {
this.addDebugMessage(`WASM response: ${JSON.stringify(result)}`);
}
// Update statistics // Update statistics
const newFramesProcessed = this.data.frames_processed + 1; const newFramesProcessed = this.data.frames_processed + 1;
const newTotalTime = this.data.total_processing_time + processingTime; const newTotalTime = this.data.total_processing_time + processingTime;
@ -198,13 +207,12 @@ Page({
frames_processed: newFramesProcessed, frames_processed: newFramesProcessed,
total_processing_time: newTotalTime, total_processing_time: newTotalTime,
avg_processing_time_ms: newAvgTime, avg_processing_time_ms: newAvgTime,
last_frame_time_ms: Math.round(processingTime), last_frame_time_ms: Math.round(processingTime)
debug_last_result: result
}); });
if (result) { if (result) {
// For worker, we need to trigger image collection when we find a good QR // For worker, we need to trigger image collection when we find a good QR
if (result.qrcode && result.valid_pattern && is_emblem_qr_pattern(result.qrcode)) { if (result.qrcode && is_emblem_qr_pattern(result.qrcode)) {
this.addDebugMessage(`Worker QR detected: ${result.qrcode}`); this.addDebugMessage(`Worker QR detected: ${result.qrcode}`);
// Trigger zoom-in if function is set up // Trigger zoom-in if function is set up
@ -225,14 +233,18 @@ Page({
const result = msg.res; const result = msg.res;
const imageData = msg.image_data; const imageData = msg.image_data;
if (imageData) { if (imageData) {
// Worker now sends data URL directly, no need to convert console.log(`worker submitted image data: ${imageData.data}`);
this.image_data_urls.push(imageData.data); const uca = new Uint8ClampedArray(imageData.data);
console.log(`first 4 bytes of image data in message: ${uca[0]}, ${uca[1]}, ${uca[2]}, ${uca[3]}`);
const dataUrl = data_url_from_frame(imageData.width, imageData.height, uca);
this.updateDebugFrameUrls(dataUrl);
this.image_data_urls.push(dataUrl);
// Update ok frames counter // Update ok frames counter
this.setData({ this.setData({
ok_frames: this.image_data_urls.length ok_frames: this.image_data_urls.length
}); });
if (this.image_data_urls.length >= 3) { if (this.image_data_urls.length >= 3) {
this.addDebugMessage('3 good images collected via worker, starting verification'); this.addDebugMessage('3 good images collected via worker, starting verification');
this.startVerifying(); this.startVerifying();
@ -446,9 +458,23 @@ Page({
this.listener = this.camera_context.onCameraFrame((frame) => { this.listener = this.camera_context.onCameraFrame((frame) => {
this.onCameraFrame(frame); this.onCameraFrame(frame);
}); });
// Start the listener // Start the listener with worker if using worker mode
this.listener.start(); if (this.data.use_worker) {
if (!this.worker) {
this.addDebugMessage('Worker not found, cannot start listener');
this.setData({
show_modal: 'verifyfailed',
hint_text: '工作线程初始化失败'
});
return;
}
this.listener.start({
worker: this.worker
});
} else {
this.listener.start();
}
}, },
@ -473,24 +499,34 @@ Page({
// Use worker for iPhone, direct processing for other devices // Use worker for iPhone, direct processing for other devices
if (this.data.use_worker && this.worker) { if (this.data.use_worker && this.worker) {
// Skip if worker is already processing a frame
if (this.data.worker_processing) {
this.setData({
frames_skipped: this.data.frames_skipped + 1
});
return;
}
// Worker processing (iPhone) // Worker processing (iPhone)
this.lastWorkerFrame = frame; // Store for handleQRResult this.lastWorkerFrame = frame; // Store for handleQRResult
// Copy frame data to avoid TOCTOU race condition (like camera.js) // Copy frame data to avoid TOCTOU race condition (like camera.js)
var uca1 = new Uint8ClampedArray(frame.data); var uca1 = new Uint8ClampedArray(frame.data);
var uca = new Uint8ClampedArray(uca1); var uca = new Uint8ClampedArray(uca1);
// Generate debug frame data URL if debug is enabled // Generate debug frame data URL if debug is enabled
if (this.data.enable_debug) { if (this.data.enable_debug) {
const frameDataUrl = data_url_from_frame(frame.width, frame.height, uca); const frameDataUrl = data_url_from_frame(frame.width, frame.height, uca);
this.updateDebugFrameUrls(frameDataUrl); this.updateDebugFrameUrls(frameDataUrl);
} }
// Set processing flag before sending message
this.setData({ worker_processing: true });
this.worker.postMessage({ this.worker.postMessage({
type: 'frame', type: 'frame',
width: frame.width, width: frame.width,
height: frame.height, height: frame.height,
data: uca, // Pass actual frame data
camera_sensitivity: this.data.camera_sensitivity camera_sensitivity: this.data.camera_sensitivity
}); });
} else { } else {
@ -584,7 +620,7 @@ Page({
// Check if we have a valid QR code that's ready for upload (like camera.js) // Check if we have a valid QR code that's ready for upload (like camera.js)
// don't require ok as we only care about the view has a valid qrcode in it // don't require ok as we only care about the view has a valid qrcode in it
// zooming in so that it's more likely to be clear enough for upload // zooming in so that it's more likely to be clear enough for upload
if (result.qrcode && result.valid_pattern && is_emblem_qr_pattern(result.qrcode)) { if (result.qrcode && is_emblem_qr_pattern(result.qrcode)) {
this.addDebugMessage(`QR detected: ${result.qrcode} ok: ${result.ok}: err ${result.err}`); this.addDebugMessage(`QR detected: ${result.qrcode} ok: ${result.ok}: err ${result.err}`);
// Trigger zoom-in if function is set up // Trigger zoom-in if function is set up

View File

@ -70,8 +70,7 @@ function process_frame(width, height, image_data, camera_sensitivity, enable_deb
qrcode: result.qrcode || '', qrcode: result.qrcode || '',
angle: result.angle || 0, angle: result.angle || 0,
ok: result.ok || false, ok: result.ok || false,
err: result.err || '', err: result.err || ''
valid_pattern: is_emblem_qr_pattern(result.qrcode || '')
}; };
if (enable_debug && debug_data_url) { if (enable_debug && debug_data_url) {
@ -137,7 +136,7 @@ function make_hint_text(result) {
} }
if (result.qrcode && result.qrcode.length > 0) { if (result.qrcode && result.qrcode.length > 0) {
if (!result.valid_pattern) { if (!is_emblem_qr_pattern(result.qrcode)) {
return "无效编码"; return "无效编码";
} }

View File

@ -34,7 +34,7 @@ var performance = {
}; };
Module["instantiateWasm"] = (info, receiveInstance) => { Module["instantiateWasm"] = (info, receiveInstance) => {
console.log("loading wasm...", info); console.log("loading wasm...", info);
WebAssembly.instantiate("pages/emblemscanner/assets/qrtool.wx.wasm.br", info).then(result => { WebAssembly.instantiate("pages/emblemscanner/qrtool.wx.wasm.br", info).then(result => {
console.log("result:", result); console.log("result:", result);
var inst = result["instance"]; var inst = result["instance"];
receiveInstance(inst); receiveInstance(inst);
@ -4252,7 +4252,7 @@ var wasmImports = {
/** @export */t: invoke_dii, /** @export */t: invoke_dii,
/** @export */P: invoke_diii, /** @export */P: invoke_diii,
/** @export */va: invoke_diiii, /** @export */va: invoke_diiii,
/** @export */n: invoke_fi, /** @export */m: invoke_fi,
/** @export */G: invoke_fii, /** @export */G: invoke_fii,
/** @export */Ca: invoke_fiii, /** @export */Ca: invoke_fiii,
/** @export */Ob: invoke_fiiii, /** @export */Ob: invoke_fiiii,
@ -4271,7 +4271,7 @@ var wasmImports = {
/** @export */I: invoke_iiiidii, /** @export */I: invoke_iiiidii,
/** @export */da: invoke_iiiidiii, /** @export */da: invoke_iiiidiii,
/** @export */Mb: invoke_iiiiff, /** @export */Mb: invoke_iiiiff,
/** @export */m: invoke_iiiii, /** @export */n: invoke_iiiii,
/** @export */ua: invoke_iiiiid, /** @export */ua: invoke_iiiiid,
/** @export */v: invoke_iiiiii, /** @export */v: invoke_iiiiii,
/** @export */Xa: invoke_iiiiiiffii, /** @export */Xa: invoke_iiiiiiffii,
@ -4523,6 +4523,16 @@ function invoke_viiiiiii(index, a1, a2, a3, a4, a5, a6, a7) {
_setThrew(1, 0); _setThrew(1, 0);
} }
} }
function invoke_viiiiii(index, a1, a2, a3, a4, a5, a6) {
var sp = stackSave();
try {
getWasmTableEntry(index)(a1, a2, a3, a4, a5, a6);
} catch (e) {
stackRestore(sp);
if (e !== e + 0) throw e;
_setThrew(1, 0);
}
}
function invoke_iifii(index, a1, a2, a3, a4) { function invoke_iifii(index, a1, a2, a3, a4) {
var sp = stackSave(); var sp = stackSave();
try { try {
@ -4613,16 +4623,6 @@ function invoke_iid(index, a1, a2) {
_setThrew(1, 0); _setThrew(1, 0);
} }
} }
function invoke_viiiiii(index, a1, a2, a3, a4, a5, a6) {
var sp = stackSave();
try {
getWasmTableEntry(index)(a1, a2, a3, a4, a5, a6);
} catch (e) {
stackRestore(sp);
if (e !== e + 0) throw e;
_setThrew(1, 0);
}
}
function invoke_viiiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9) { function invoke_viiiiiiiii(index, a1, a2, a3, a4, a5, a6, a7, a8, a9) {
var sp = stackSave(); var sp = stackSave();
try { try {

Binary file not shown.

View File

@ -1,34 +1,8 @@
console.log("hello from emblemscanner worker"); console.log("hello from emblemscanner worker");
let qrtool = require('../qrtool.wx.js'); let qrtool = require('./qrtool.wx.js');
// Create offscreen canvas for image generation (self-contained)
let offscreenCanvas = null;
function getOffscreenCanvas() {
if (!offscreenCanvas) {
offscreenCanvas = wx.createOffscreenCanvas({
type: '2d',
width: 100,
height: 100,
});
}
return offscreenCanvas;
}
/**
* Convert raw frame data to data URL for image visualization
*/
function data_url_from_frame(width, height, image_data) {
const canvas = getOffscreenCanvas();
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
var imgd = ctx.createImageData(width, height);
imgd.data.set(image_data);
ctx.putImageData(imgd, 0, 0);
return canvas.toDataURL("image/jpeg", 1.0);
}
var qrtool_ready = false; var qrtool_ready = false;
@ -43,6 +17,34 @@ qrtool.onRuntimeInitialized = () => {
var submit_message = null; var submit_message = null;
function handle_frame(data, width, height, camera_sensitivity) { function handle_frame(data, width, height, camera_sensitivity) {
const begin = Date.now();
console.log(`handling frame ${width}x${height}, data is ${data.byteLength} bytes`);
var uca = new Uint8ClampedArray(data);
console.log(`first 4 bytes of uca: ${uca[0]}, ${uca[1]}, ${uca[2]}, ${uca[3]}`);
var buf = qrtool._malloc(uca.length * uca.BYTES_PER_ELEMENT);
qrtool.HEAPU8.set(uca, buf);
var r = qrtool.ccall('qrtool_angle', 'string', ['number', 'number', 'number', 'number', 'number'], [buf, width, height, 0, camera_sensitivity]);
qrtool._free(buf);
console.log(`image ${width}x${height} processed in ${Date.now() - begin}ms, result: ${r}`);
var res = JSON.parse(r);
worker.postMessage({
type: "result",
res,
processing_time: Date.now() - begin,
});
if (res.ok) {
// Send raw image data back to main thread for data URL conversion
// since we have no access to offscreen canvas in WASM worker
submit_message = {
type: "submit",
res,
image_data: { data, width, height },
};
}
}
worker.onMessage((msg) => {
console.log("emblemscanner worker got message", msg.type);
if (!qrtool_ready) { if (!qrtool_ready) {
console.log("qrtool not ready"); console.log("qrtool not ready");
worker.postMessage({ worker.postMessage({
@ -55,41 +57,29 @@ function handle_frame(data, width, height, camera_sensitivity) {
}); });
return; return;
} }
const begin = Date.now();
var uca = new Uint8ClampedArray(data);
var buf = qrtool._malloc(uca.length * uca.BYTES_PER_ELEMENT);
qrtool.HEAPU8.set(uca, buf);
var r = qrtool.ccall('qrtool_angle', 'string', ['number', 'number', 'number', 'number', 'number'], [buf, width, height, 0, camera_sensitivity]);
qrtool._free(buf);
console.log("emblemscanner worker r:", r);
var res = JSON.parse(r);
worker.postMessage({
type: "result",
res,
processing_time: Date.now() - begin,
});
if (res.ok) {
// Since image_data takes seconds to serialize, send it in a separate
// message to the UI can update with the good news
const dataUrl = data_url_from_frame(width, height, uca);
submit_message = {
type: "submit",
res,
image_data: { data: dataUrl, width, height },
};
}
}
worker.onMessage((msg) => {
console.log("emblemscanner worker got message", msg.type);
switch (msg.type) { switch (msg.type) {
case "frame": case "frame":
try { try {
// Use frame data from message instead of getCameraFrameData() const data = worker.getCameraFrameData();
if (msg.data) { if (data) {
handle_frame(msg.data, msg.width, msg.height, msg.camera_sensitivity); handle_frame(data, msg.width, msg.height, msg.camera_sensitivity);
} else {
worker.postMessage({
type: "result",
res: {
ok: false,
err: "no frame data",
},
});
} }
} catch (e) { } catch (e) {
worker.postMessage({
type: "result",
res: {
ok: false,
err: `failed to handle frame: ${e.message}`,
},
});
console.log(e); console.log(e);
} }
break; break;

File diff suppressed because it is too large Load Diff