fix(shopfloor): jsQR loop — full-res frame + canvas blank-pixel check + last-result trace
271 attempts at 720x1280 with no detection means either downsampling
killed the finder patterns or drawImage is silently painting blank
pixels (a known iOS WebKit failure mode for some video-stream sources).
- Drop the 600px scaling cap. Feed the full native video frame to
jsQR. Per-frame cost goes up but is still fine; the win is jsQR
sees finder patterns at full sharpness.
- Add a one-time sanity check that walks the first ImageData buffer
looking for a non-zero pixel. If everything is 0,0,0 the canvas is
blank (drawImage failed) and we surface that as 'px:BLANK' in the
status line — telling us instantly to switch decoder strategies
rather than chasing tuning.
- Status line now also shows the last jsQR call outcome:
r:found / r:no_code / r:empty / r:error: ...
So we can confirm whether jsQR is even being invoked successfully.
Status format compacted to fit one line on phone:
jsqr · f1234 a567 720x1280 rs4 px:ok r:no_code
Version: shopfloor 19.0.20 -> 19.0.21.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'version': '19.0.20.0.0',
|
||||
'version': '19.0.21.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||
'first-piece inspection gates.',
|
||||
|
||||
@@ -211,8 +211,10 @@ export class QrScanner extends Component {
|
||||
let attempts = 0;
|
||||
let lastDecode = 0;
|
||||
let lastStatus = 0;
|
||||
let lastResult = "—"; // "found" | "no_code" | "empty" | error msg
|
||||
let firstNonZeroPixel = -1; // sanity check that drawImage works
|
||||
const MIN_INTERVAL_MS = 100;
|
||||
const STATUS_INTERVAL_MS = 400;
|
||||
const STATUS_INTERVAL_MS = 500;
|
||||
const tick = (now) => {
|
||||
if (!this.decodeLoopActive || !this.state.open) return;
|
||||
frames++;
|
||||
@@ -226,38 +228,54 @@ export class QrScanner extends Component {
|
||||
try {
|
||||
const w = v.videoWidth;
|
||||
const h = v.videoHeight;
|
||||
// Cap the working frame at 600px on the long side —
|
||||
// enough resolution for jsQR to find an ~33-module
|
||||
// QR, while keeping per-frame cost reasonable.
|
||||
const scale = Math.min(1, 600 / Math.max(w, h));
|
||||
const cw = Math.round(w * scale);
|
||||
const ch = Math.round(h * scale);
|
||||
if (this._canvas.width !== cw) this._canvas.width = cw;
|
||||
if (this._canvas.height !== ch) this._canvas.height = ch;
|
||||
this._ctx.drawImage(v, 0, 0, cw, ch);
|
||||
const imageData = this._ctx.getImageData(0, 0, cw, ch);
|
||||
// attemptBoth tries the image as-is AND inverted —
|
||||
// covers cases where a camera's auto-exposure makes
|
||||
// the QR look light-on-dark to the decoder.
|
||||
const code = window.jsQR(imageData.data, cw, ch, {
|
||||
// Use the native video resolution directly — no
|
||||
// downscaling. jsQR's runtime cost is acceptable
|
||||
// even at 1080p, and downsampling can blur the
|
||||
// finder patterns just enough to defeat detection
|
||||
// when the QR is small in the frame.
|
||||
if (this._canvas.width !== w) this._canvas.width = w;
|
||||
if (this._canvas.height !== h) this._canvas.height = h;
|
||||
this._ctx.drawImage(v, 0, 0, w, h);
|
||||
const imageData = this._ctx.getImageData(0, 0, w, h);
|
||||
if (firstNonZeroPixel < 0) {
|
||||
// One-time sanity check: confirm drawImage is
|
||||
// actually painting the video onto the canvas.
|
||||
// If every pixel is 0,0,0 we'd never decode
|
||||
// anything regardless of jsQR settings (this
|
||||
// can happen with tainted canvases on some
|
||||
// older WebKit builds).
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
if (imageData.data[i] | imageData.data[i + 1] | imageData.data[i + 2]) {
|
||||
firstNonZeroPixel = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstNonZeroPixel < 0) firstNonZeroPixel = -2;
|
||||
}
|
||||
const code = window.jsQR(imageData.data, w, h, {
|
||||
inversionAttempts: "attemptBoth",
|
||||
});
|
||||
if (code && code.data) {
|
||||
lastResult = "found";
|
||||
this._handleCode(code.data);
|
||||
return;
|
||||
}
|
||||
lastResult = code ? "empty" : "no_code";
|
||||
} catch (e) {
|
||||
this.state.statusLine = "Decoder: jsqr — error: " +
|
||||
(e.message || String(e)).slice(0, 80);
|
||||
lastResult = "error: " +
|
||||
(e.message || String(e)).slice(0, 60);
|
||||
}
|
||||
}
|
||||
if (now - lastStatus > STATUS_INTERVAL_MS) {
|
||||
lastStatus = now;
|
||||
this.state.statusLine =
|
||||
"Decoder: jsqr · frames " + frames +
|
||||
" · attempts " + attempts +
|
||||
" · video " + (v.videoWidth || 0) + "x" + (v.videoHeight || 0) +
|
||||
" rs" + v.readyState;
|
||||
"jsqr · f" + frames +
|
||||
" a" + attempts +
|
||||
" " + (v.videoWidth || 0) + "x" + (v.videoHeight || 0) +
|
||||
" rs" + v.readyState +
|
||||
" px:" + (firstNonZeroPixel === -2
|
||||
? "BLANK" : firstNonZeroPixel < 0 ? "?" : "ok") +
|
||||
" r:" + lastResult;
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user