quadqr-js 1.4.2 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FORMAT.md +5 -5
- package/README.md +19 -8
- package/SPECIFICATION.md +12 -12
- package/dist/camera-scanner-worker.js +793 -0
- package/dist/esm/camera-scanner-worker.js +793 -0
- package/dist/esm/geometry.js +25 -10
- package/dist/esm/quadqr.js +843 -62
- package/dist/esm/vision.js +887 -122
- package/dist/esm/wasm.js +85 -12
- package/dist/quadqr.js +1803 -198
- package/dist/quadqr.min.js +2032 -274
- package/dist/wasm/quadqr-core.wasm +0 -0
- package/docs/API.md +13 -5
- package/docs/BROWSER_CDN.md +5 -5
- package/docs/CLI.md +167 -167
- package/docs/COMPRESSION.md +170 -170
- package/docs/GETTING_STARTED.md +1 -1
- package/docs/HIGH_DENSITY_MODE.md +100 -100
- package/docs/NODE.md +159 -159
- package/docs/README.md +62 -62
- package/docs/RELIABILITY_LAB.md +64 -64
- package/docs/TRIANGLE16.md +98 -98
- package/docs/WASM.md +30 -4
- package/package.json +5 -3
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CELL_ENCODINGS,
|
|
3
|
+
decodeMatrix,
|
|
4
|
+
scanImageData,
|
|
5
|
+
internals
|
|
6
|
+
} from "./esm/quadqr.js";
|
|
7
|
+
import { autoColorImageData } from "./esm/vision.js";
|
|
8
|
+
import { initWasm } from "./esm/wasm.js";
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
combineFrameObservations,
|
|
12
|
+
cropImageDataInset,
|
|
13
|
+
observationDataAgreement,
|
|
14
|
+
selectBestFrameObservation
|
|
15
|
+
} = internals;
|
|
16
|
+
|
|
17
|
+
let scanOptions = {};
|
|
18
|
+
let missStreak = 0;
|
|
19
|
+
let cameraGeometryHint = null;
|
|
20
|
+
let cameraGeometryHintMisses = 0;
|
|
21
|
+
let observationHistory = new Map();
|
|
22
|
+
let wasmState = null;
|
|
23
|
+
const frameCanvasPool = new Map();
|
|
24
|
+
|
|
25
|
+
function nowMs() {
|
|
26
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
27
|
+
? performance.now()
|
|
28
|
+
: Date.now();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clampNumber(value, min, max) {
|
|
32
|
+
return Math.max(min, Math.min(max, value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function serializeError(error) {
|
|
36
|
+
return {
|
|
37
|
+
name: error?.name || "Error",
|
|
38
|
+
message: error?.message || String(error),
|
|
39
|
+
stack: error?.stack || "",
|
|
40
|
+
debug: error?.debug ?? null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function bestVisionDiagnosticPass(visionDiagnostics) {
|
|
45
|
+
const passes = visionDiagnostics?.passes;
|
|
46
|
+
if (!Array.isArray(passes) || !passes.length) return null;
|
|
47
|
+
return passes.slice().sort((a, b) => {
|
|
48
|
+
const aGeometry = a.geometries?.[0];
|
|
49
|
+
const bGeometry = b.geometries?.[0];
|
|
50
|
+
return (Boolean(bGeometry) - Boolean(aGeometry)) ||
|
|
51
|
+
((b.finderCount ?? 0) - (a.finderCount ?? 0)) ||
|
|
52
|
+
((bGeometry?.score ?? 0) - (aGeometry?.score ?? 0));
|
|
53
|
+
})[0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeFrameDiagnostics(frameDiagnostics, source, width, height, visionDiagnostics) {
|
|
57
|
+
if (!frameDiagnostics || typeof frameDiagnostics !== "object") return;
|
|
58
|
+
frameDiagnostics.scanWidth = width;
|
|
59
|
+
frameDiagnostics.scanHeight = height;
|
|
60
|
+
frameDiagnostics.frameWidth = width;
|
|
61
|
+
frameDiagnostics.frameHeight = height;
|
|
62
|
+
frameDiagnostics.scanRect = { x: 0, y: 0, width, height };
|
|
63
|
+
frameDiagnostics.sourceRect = {
|
|
64
|
+
x: source.x,
|
|
65
|
+
y: source.y,
|
|
66
|
+
width: source.width,
|
|
67
|
+
height: source.height,
|
|
68
|
+
cropped: Boolean(source.cropped)
|
|
69
|
+
};
|
|
70
|
+
frameDiagnostics.vision = visionDiagnostics;
|
|
71
|
+
const bestPass = bestVisionDiagnosticPass(visionDiagnostics);
|
|
72
|
+
frameDiagnostics.bestPass = bestPass;
|
|
73
|
+
frameDiagnostics.finderCount = bestPass?.finderCount ?? 0;
|
|
74
|
+
frameDiagnostics.finders = bestPass?.finders ?? [];
|
|
75
|
+
frameDiagnostics.finderMethod = bestPass?.finderMethod ?? null;
|
|
76
|
+
frameDiagnostics.finderPasses = Array.isArray(visionDiagnostics?.passes)
|
|
77
|
+
? visionDiagnostics.passes.map((pass) => ({
|
|
78
|
+
method: pass.finderMethod ?? pass.label,
|
|
79
|
+
finderCount: pass.finderCount ?? 0,
|
|
80
|
+
threshold: pass.threshold,
|
|
81
|
+
geometryCount: pass.geometries?.length ?? 0
|
|
82
|
+
}))
|
|
83
|
+
: [];
|
|
84
|
+
frameDiagnostics.geometry = bestPass?.geometries?.[0] ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function attachGeometryDiagnostic(frameDiagnostics, result) {
|
|
88
|
+
if (!frameDiagnostics || !result?.geometry || frameDiagnostics.geometry) return;
|
|
89
|
+
const geometry = result.geometry;
|
|
90
|
+
frameDiagnostics.geometry = geometry;
|
|
91
|
+
frameDiagnostics.geometryReused = Boolean(result.geometryReused);
|
|
92
|
+
if (geometry.finders) {
|
|
93
|
+
const reusedFinders = [
|
|
94
|
+
geometry.finders.topLeft,
|
|
95
|
+
geometry.finders.topRight,
|
|
96
|
+
geometry.finders.bottomLeft
|
|
97
|
+
].filter(Boolean);
|
|
98
|
+
frameDiagnostics.finders = reusedFinders;
|
|
99
|
+
frameDiagnostics.finderCount = reusedFinders.length;
|
|
100
|
+
if (result.geometryReused) frameDiagnostics.finderMethod = "geometry-reuse";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scaleGeometryForFrame(geometry, fromFrame, toFrame) {
|
|
105
|
+
if (!geometry?.homography || !fromFrame?.scanWidth || !toFrame?.scanWidth) return null;
|
|
106
|
+
const sx = toFrame.scanWidth / fromFrame.scanWidth;
|
|
107
|
+
const sy = toFrame.scanHeight / fromFrame.scanHeight;
|
|
108
|
+
if (!Number.isFinite(sx) || !Number.isFinite(sy) || sx <= 0 || sy <= 0) return null;
|
|
109
|
+
const moduleScale = Math.sqrt(sx * sy);
|
|
110
|
+
const scalePoint = (point) => point ? { ...point, x: point.x * sx, y: point.y * sy } : point;
|
|
111
|
+
const scaleFinder = (finder) => finder ? {
|
|
112
|
+
...finder,
|
|
113
|
+
x: finder.x * sx,
|
|
114
|
+
y: finder.y * sy,
|
|
115
|
+
moduleSize: finder.moduleSize * moduleScale
|
|
116
|
+
} : finder;
|
|
117
|
+
const h = geometry.homography;
|
|
118
|
+
const scaled = {
|
|
119
|
+
...geometry,
|
|
120
|
+
homography: [
|
|
121
|
+
h[0] * sx, h[1] * sx, h[2] * sx,
|
|
122
|
+
h[3] * sy, h[4] * sy, h[5] * sy,
|
|
123
|
+
h[6], h[7], h[8]
|
|
124
|
+
],
|
|
125
|
+
sourcePoints: Array.isArray(geometry.sourcePoints)
|
|
126
|
+
? geometry.sourcePoints.map(scalePoint)
|
|
127
|
+
: geometry.sourcePoints,
|
|
128
|
+
finders: geometry.finders ? {
|
|
129
|
+
topLeft: scaleFinder(geometry.finders.topLeft),
|
|
130
|
+
topRight: scaleFinder(geometry.finders.topRight),
|
|
131
|
+
bottomLeft: scaleFinder(geometry.finders.bottomLeft)
|
|
132
|
+
} : geometry.finders
|
|
133
|
+
};
|
|
134
|
+
if (geometry.alignment) {
|
|
135
|
+
scaled.alignment = {
|
|
136
|
+
...geometry.alignment,
|
|
137
|
+
center: scalePoint(geometry.alignment.center),
|
|
138
|
+
predicted: scalePoint(geometry.alignment.predicted)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return scaled;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function updateCameraGeometryHint(frameDiagnostics, result = null) {
|
|
145
|
+
if (scanOptions.cameraGeometryReuse === false) {
|
|
146
|
+
cameraGeometryHint = null;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const geometry = result?.geometry ?? frameDiagnostics?.geometry ?? null;
|
|
150
|
+
if (geometry?.homography && Number.isInteger(geometry.version)) {
|
|
151
|
+
cameraGeometryHint = geometry;
|
|
152
|
+
cameraGeometryHintMisses = 0;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (cameraGeometryHint) {
|
|
156
|
+
cameraGeometryHintMisses++;
|
|
157
|
+
const maxMisses = Math.max(1, Math.round(scanOptions.cameraGeometryReuseMaxMisses ?? 5));
|
|
158
|
+
if (cameraGeometryHintMisses >= maxMisses) {
|
|
159
|
+
cameraGeometryHint = null;
|
|
160
|
+
cameraGeometryHintMisses = 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function tryMultiFrameDecode(observations) {
|
|
166
|
+
if (scanOptions.multiFrame === false) return null;
|
|
167
|
+
const multiFrameWindow = Math.max(2, Math.min(8, Math.round(scanOptions.multiFrameWindow ?? 4)));
|
|
168
|
+
const multiFrameMinFrames = Math.max(2, Math.min(multiFrameWindow, Math.round(scanOptions.multiFrameMinFrames ?? 2)));
|
|
169
|
+
const best = selectBestFrameObservation(observations);
|
|
170
|
+
if (!best) return null;
|
|
171
|
+
const trackKey = `${best.version}:${best.cellEncoding ?? "auto"}`;
|
|
172
|
+
let history = observationHistory.get(trackKey) ?? [];
|
|
173
|
+
|
|
174
|
+
if (history.length) {
|
|
175
|
+
const agreement = observationDataAgreement(history[history.length - 1], best);
|
|
176
|
+
const minimumAgreement = best.cellEncoding === CELL_ENCODINGS.TRIANGLE16
|
|
177
|
+
? (scanOptions.multiFrameMinAgreementHighDensity ?? 0.58)
|
|
178
|
+
: (scanOptions.multiFrameMinAgreement ?? 0.62);
|
|
179
|
+
if (agreement > 0 && agreement < minimumAgreement) history = [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
history.push(best);
|
|
183
|
+
while (history.length > multiFrameWindow) history.shift();
|
|
184
|
+
observationHistory.set(trackKey, history);
|
|
185
|
+
if (history.length < multiFrameMinFrames) return null;
|
|
186
|
+
|
|
187
|
+
const combined = combineFrameObservations(history);
|
|
188
|
+
if (!combined) return null;
|
|
189
|
+
try {
|
|
190
|
+
const decoded = decodeMatrix(combined.matrix, {
|
|
191
|
+
structureTolerance: scanOptions.structureTolerance ?? 0.20,
|
|
192
|
+
cellConfidence: combined.confidence,
|
|
193
|
+
cellAlternatives: combined.alternatives,
|
|
194
|
+
cellEncodingHint: best.cellEncoding ?? undefined,
|
|
195
|
+
maxErasureConfidence: scanOptions.maxErasureConfidence,
|
|
196
|
+
softDecoding: scanOptions.softDecoding
|
|
197
|
+
});
|
|
198
|
+
if (decoded.version !== best.version) return null;
|
|
199
|
+
return {
|
|
200
|
+
...decoded,
|
|
201
|
+
perspectiveCorrected: Boolean(best.geometry),
|
|
202
|
+
colorCalibrated: true,
|
|
203
|
+
colorNormalization: "multi-frame-confidence-fusion",
|
|
204
|
+
samplingMode: "multi-frame-confidence-fusion",
|
|
205
|
+
multiFrameCombined: history.length,
|
|
206
|
+
multiFrameAgreement: combined.frameAgreement,
|
|
207
|
+
multiFrameMode: "confidence-fusion",
|
|
208
|
+
geometry: best.geometry,
|
|
209
|
+
observedPalette: best.observedPalette,
|
|
210
|
+
averageCellConfidence: best.averageCellConfidence,
|
|
211
|
+
lowConfidenceCells: best.lowConfidenceCells
|
|
212
|
+
};
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function pooledFrameCanvas(cap, width, height) {
|
|
219
|
+
let entry = frameCanvasPool.get(cap);
|
|
220
|
+
if (!entry) {
|
|
221
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
222
|
+
const ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
|
|
223
|
+
entry = { canvas, ctx };
|
|
224
|
+
frameCanvasPool.set(cap, entry);
|
|
225
|
+
} else if (entry.canvas.width !== width || entry.canvas.height !== height) {
|
|
226
|
+
entry.canvas.width = width;
|
|
227
|
+
entry.canvas.height = height;
|
|
228
|
+
}
|
|
229
|
+
return entry;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function makeCanvasFrameProvider(bitmap, source) {
|
|
233
|
+
// Cache ImageData per resolution for the current camera frame, and reuse the
|
|
234
|
+
// underlying OffscreenCanvas/context across frames. This avoids allocating
|
|
235
|
+
// fresh canvases at camera frame rate without changing any scanner stage.
|
|
236
|
+
const frames = new Map();
|
|
237
|
+
return (maxDimension) => {
|
|
238
|
+
const cap = Math.max(1, Math.round(maxDimension));
|
|
239
|
+
if (frames.has(cap)) return frames.get(cap);
|
|
240
|
+
const scale = Math.min(1, cap / Math.max(source.width, source.height));
|
|
241
|
+
const width = Math.max(1, Math.round(source.width * scale));
|
|
242
|
+
const height = Math.max(1, Math.round(source.height * scale));
|
|
243
|
+
const { ctx } = pooledFrameCanvas(cap, width, height);
|
|
244
|
+
if ("imageSmoothingEnabled" in ctx) ctx.imageSmoothingEnabled = true;
|
|
245
|
+
if ("imageSmoothingQuality" in ctx) ctx.imageSmoothingQuality = "high";
|
|
246
|
+
ctx.drawImage(bitmap, source.x, source.y, source.width, source.height, 0, 0, width, height);
|
|
247
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
248
|
+
const frame = { imageData, scanWidth: width, scanHeight: height, source: { ...source } };
|
|
249
|
+
frames.set(cap, frame);
|
|
250
|
+
return frame;
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function scanCapturedFrame(frame, options, observations, frameDiagnostics) {
|
|
255
|
+
const visionDiagnostics = frameDiagnostics ? { passes: [] } : null;
|
|
256
|
+
try {
|
|
257
|
+
let result = scanImageData(frame.imageData, {
|
|
258
|
+
...options,
|
|
259
|
+
_visionDiagnostics: visionDiagnostics,
|
|
260
|
+
_observationCollector: observations
|
|
261
|
+
});
|
|
262
|
+
normalizeFrameDiagnostics(frameDiagnostics, frame.source, frame.scanWidth, frame.scanHeight, visionDiagnostics);
|
|
263
|
+
attachGeometryDiagnostic(frameDiagnostics, result);
|
|
264
|
+
if (frame.source.cropped) {
|
|
265
|
+
result = {
|
|
266
|
+
...result,
|
|
267
|
+
cameraVisibleCrop: true,
|
|
268
|
+
cameraSourceRect: {
|
|
269
|
+
x: frame.source.x,
|
|
270
|
+
y: frame.source.y,
|
|
271
|
+
width: frame.source.width,
|
|
272
|
+
height: frame.source.height
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
} catch (error) {
|
|
278
|
+
normalizeFrameDiagnostics(frameDiagnostics, frame.source, frame.scanWidth, frame.scanHeight, visionDiagnostics);
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function makeFrameMeta(frame, diagnostic, enhancement = null) {
|
|
284
|
+
return {
|
|
285
|
+
imageData: frame.imageData,
|
|
286
|
+
scanWidth: frame.scanWidth,
|
|
287
|
+
scanHeight: frame.scanHeight,
|
|
288
|
+
sourceRect: frame.source ? { ...frame.source } : null,
|
|
289
|
+
enhancedImageData: enhancement?.imageData ?? null,
|
|
290
|
+
enhancedRect: enhancement?.rect ? { ...enhancement.rect } : null,
|
|
291
|
+
enhancement: enhancement?.meta ? { ...enhancement.meta } : null,
|
|
292
|
+
diagnostic
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function transferableBuffers(frameMeta) {
|
|
297
|
+
const buffers = [];
|
|
298
|
+
const base = frameMeta?.imageData?.data?.buffer;
|
|
299
|
+
const enhanced = frameMeta?.enhancedImageData?.data?.buffer;
|
|
300
|
+
if (base instanceof ArrayBuffer) buffers.push(base);
|
|
301
|
+
if (enhanced instanceof ArrayBuffer && enhanced !== base) buffers.push(enhanced);
|
|
302
|
+
return buffers;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function pushDiagnostic(events, event) {
|
|
306
|
+
events.push(event);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function processFrame(bitmap, source, frameNumber) {
|
|
310
|
+
const events = [];
|
|
311
|
+
const observations = [];
|
|
312
|
+
const frameDiagnostics = {};
|
|
313
|
+
const frameStarted = nowMs();
|
|
314
|
+
const baseCameraMaxDimension = Math.max(480, Math.round(scanOptions.maxDimension ?? 640));
|
|
315
|
+
const cameraHighResolutionMaxDimension = Math.max(
|
|
316
|
+
baseCameraMaxDimension,
|
|
317
|
+
Math.round(scanOptions.cameraHighResolutionMaxDimension ?? 960)
|
|
318
|
+
);
|
|
319
|
+
const cameraAutoColorEvery = Math.max(1, Math.round(scanOptions.cameraAutoColorEvery ?? 1));
|
|
320
|
+
const cameraAutoEnhanceEvery = Math.max(1, Math.round(scanOptions.cameraAutoEnhanceEvery ?? 2));
|
|
321
|
+
const cameraFinderRecoveryEvery = Math.max(1, Math.round(scanOptions.cameraFinderRecoveryEvery ?? 2));
|
|
322
|
+
const cameraHighResolutionEvery = Math.max(1, Math.round(scanOptions.cameraHighResolutionEvery ?? 2));
|
|
323
|
+
const getFrame = makeCanvasFrameProvider(bitmap, source);
|
|
324
|
+
const baseFrame = getFrame(baseCameraMaxDimension);
|
|
325
|
+
|
|
326
|
+
let allowFinderRecovery = false;
|
|
327
|
+
let allowAutoEnhance = false;
|
|
328
|
+
try {
|
|
329
|
+
allowFinderRecovery = scanOptions.finderRecovery !== false &&
|
|
330
|
+
missStreak > 0 && ((missStreak - 1) % cameraFinderRecoveryEvery === 0);
|
|
331
|
+
allowAutoEnhance = scanOptions.autoEnhanceRecovery !== false &&
|
|
332
|
+
missStreak > 0 && ((missStreak - 1) % cameraAutoEnhanceEvery === 0);
|
|
333
|
+
const method = allowAutoEnhance
|
|
334
|
+
? "progressive-color-recovery"
|
|
335
|
+
: (allowFinderRecovery ? "finder-recovery" : "fast-scan");
|
|
336
|
+
|
|
337
|
+
const result = scanCapturedFrame(baseFrame, {
|
|
338
|
+
...scanOptions,
|
|
339
|
+
_diagnosticLabel: method,
|
|
340
|
+
finderRecovery: allowFinderRecovery,
|
|
341
|
+
autoEnhanceRecovery: allowAutoEnhance,
|
|
342
|
+
autoEnhanceWhenNoGeometry: allowAutoEnhance,
|
|
343
|
+
fullFrameAutoEnhanceRecovery: scanOptions.fullFrameAutoEnhanceRecovery ?? false,
|
|
344
|
+
_geometryHints: scanOptions.cameraGeometryReuse === false || !cameraGeometryHint ? undefined : [cameraGeometryHint]
|
|
345
|
+
}, observations, frameDiagnostics);
|
|
346
|
+
|
|
347
|
+
const elapsedMs = nowMs() - frameStarted;
|
|
348
|
+
updateCameraGeometryHint(frameDiagnostics, result);
|
|
349
|
+
pushDiagnostic(events, {
|
|
350
|
+
type: "frame",
|
|
351
|
+
state: "decoded",
|
|
352
|
+
method: result.recoveryMode ?? result.samplingMode ?? method,
|
|
353
|
+
elapsedMs,
|
|
354
|
+
missStreak,
|
|
355
|
+
...frameDiagnostics
|
|
356
|
+
});
|
|
357
|
+
pushDiagnostic(events, {
|
|
358
|
+
type: "success",
|
|
359
|
+
state: "decoded",
|
|
360
|
+
method: result.recoveryMode ?? result.samplingMode ?? method,
|
|
361
|
+
elapsedMs,
|
|
362
|
+
message: `Decoded v${result.version} · ECC ${result.eccLevel} · ${Math.round(elapsedMs)} ms`,
|
|
363
|
+
...frameDiagnostics
|
|
364
|
+
});
|
|
365
|
+
missStreak = 0;
|
|
366
|
+
observationHistory.clear();
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
result,
|
|
370
|
+
frameMeta: makeFrameMeta(baseFrame, frameDiagnostics),
|
|
371
|
+
diagnostics: events,
|
|
372
|
+
elapsedMs,
|
|
373
|
+
missStreak
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
missStreak++;
|
|
377
|
+
updateCameraGeometryHint(frameDiagnostics);
|
|
378
|
+
const fastElapsedMs = nowMs() - frameStarted;
|
|
379
|
+
pushDiagnostic(events, {
|
|
380
|
+
type: "frame",
|
|
381
|
+
state: "miss",
|
|
382
|
+
method: allowAutoEnhance
|
|
383
|
+
? "progressive-color-recovery"
|
|
384
|
+
: (allowFinderRecovery ? "finder-recovery" : "fast-scan"),
|
|
385
|
+
elapsedMs: fastElapsedMs,
|
|
386
|
+
missStreak,
|
|
387
|
+
error: error?.message ?? String(error),
|
|
388
|
+
...frameDiagnostics
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// The continuous camera engine can dedicate one worker to fresh-frame
|
|
392
|
+
// acquisition. In fast mode we intentionally stop here after the normal
|
|
393
|
+
// detector/decode attempt. A second worker runs the complete recovery
|
|
394
|
+
// stack in parallel, so Auto Color, high-resolution perspective recovery,
|
|
395
|
+
// multi-frame fusion, and damaged-code recovery can never block the next
|
|
396
|
+
// fresh camera frame. This changes scheduling only, not scanner capability.
|
|
397
|
+
if (scanOptions.cameraPipelineMode === "fast") {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
error: serializeError(error),
|
|
401
|
+
diagnostics: events,
|
|
402
|
+
elapsedMs: nowMs() - frameStarted,
|
|
403
|
+
missStreak,
|
|
404
|
+
fastPipeline: true
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const shouldTryHighResolution = scanOptions.cameraHighResolutionRecovery !== false &&
|
|
409
|
+
cameraHighResolutionMaxDimension > baseCameraMaxDimension &&
|
|
410
|
+
(frameDiagnostics?.finderCount ?? 0) >= (scanOptions.cameraHighResolutionMinFinders ?? 2) &&
|
|
411
|
+
((missStreak - 1) % cameraHighResolutionEvery === 0);
|
|
412
|
+
if (shouldTryHighResolution) {
|
|
413
|
+
const highResolutionObservations = [];
|
|
414
|
+
const highResolutionDiagnostics = {};
|
|
415
|
+
pushDiagnostic(events, {
|
|
416
|
+
type: "method",
|
|
417
|
+
state: "trying",
|
|
418
|
+
method: "high-resolution-geometry-recovery",
|
|
419
|
+
message: `Finder geometry detected · refining detail at up to ${cameraHighResolutionMaxDimension}px`,
|
|
420
|
+
...frameDiagnostics
|
|
421
|
+
});
|
|
422
|
+
try {
|
|
423
|
+
const recoveryStarted = nowMs();
|
|
424
|
+
const highResolutionFrame = getFrame(cameraHighResolutionMaxDimension);
|
|
425
|
+
const scaledGeometry = scaleGeometryForFrame(frameDiagnostics?.geometry, baseFrame, highResolutionFrame);
|
|
426
|
+
let recovered = null;
|
|
427
|
+
|
|
428
|
+
// If the low-resolution locator already established projective geometry,
|
|
429
|
+
// do not make the detailed frame rediscover the eyes. Scale the
|
|
430
|
+
// homography and sample/decode immediately. If that fast reuse misses,
|
|
431
|
+
// fall through to the complete high-detail detector/recovery path.
|
|
432
|
+
if (scaledGeometry) {
|
|
433
|
+
try {
|
|
434
|
+
recovered = scanCapturedFrame(highResolutionFrame, {
|
|
435
|
+
...scanOptions,
|
|
436
|
+
_diagnosticLabel: "high-resolution-geometry-reuse",
|
|
437
|
+
_geometryHints: [scaledGeometry],
|
|
438
|
+
_geometryHintOnly: true,
|
|
439
|
+
finderRecovery: false,
|
|
440
|
+
autoEnhanceRecovery: false,
|
|
441
|
+
fullFrameAutoEnhanceRecovery: false
|
|
442
|
+
}, highResolutionObservations, highResolutionDiagnostics);
|
|
443
|
+
} catch {
|
|
444
|
+
recovered = null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!recovered) {
|
|
449
|
+
recovered = scanCapturedFrame(highResolutionFrame, {
|
|
450
|
+
...scanOptions,
|
|
451
|
+
_diagnosticLabel: "high-resolution-geometry-recovery",
|
|
452
|
+
finderRecovery: true,
|
|
453
|
+
autoEnhanceRecovery: false,
|
|
454
|
+
fullFrameAutoEnhanceRecovery: false
|
|
455
|
+
}, highResolutionObservations, highResolutionDiagnostics);
|
|
456
|
+
}
|
|
457
|
+
const recoveryElapsedMs = nowMs() - recoveryStarted;
|
|
458
|
+
pushDiagnostic(events, {
|
|
459
|
+
type: "success",
|
|
460
|
+
state: "decoded",
|
|
461
|
+
method: recovered.geometryReused ? "high-resolution-geometry-reuse" : "high-resolution-geometry-recovery",
|
|
462
|
+
elapsedMs: recoveryElapsedMs,
|
|
463
|
+
message: `Detail retry decoded v${recovered.version} · ${Math.round(recoveryElapsedMs)} ms`,
|
|
464
|
+
...highResolutionDiagnostics
|
|
465
|
+
});
|
|
466
|
+
missStreak = 0;
|
|
467
|
+
observationHistory.clear();
|
|
468
|
+
return {
|
|
469
|
+
ok: true,
|
|
470
|
+
result: {
|
|
471
|
+
...recovered,
|
|
472
|
+
cameraHighResolutionRecovery: true,
|
|
473
|
+
cameraProgressiveRecovery: true,
|
|
474
|
+
recoveryMode: recovered.recoveryMode ?? (recovered.geometryReused ? "high-resolution-geometry-reuse" : "high-resolution-geometry-recovery")
|
|
475
|
+
},
|
|
476
|
+
frameMeta: makeFrameMeta(highResolutionFrame, highResolutionDiagnostics),
|
|
477
|
+
diagnostics: events,
|
|
478
|
+
elapsedMs: nowMs() - frameStarted,
|
|
479
|
+
missStreak
|
|
480
|
+
};
|
|
481
|
+
} catch (highResolutionError) {
|
|
482
|
+
observations.push(...highResolutionObservations);
|
|
483
|
+
pushDiagnostic(events, {
|
|
484
|
+
type: "method",
|
|
485
|
+
state: "failed",
|
|
486
|
+
method: "high-resolution-geometry-recovery",
|
|
487
|
+
message: `High-detail geometry retry did not decode${highResolutionDiagnostics?.finderCount != null ? ` · ${highResolutionDiagnostics.finderCount} finder(s)` : ""}`,
|
|
488
|
+
error: highResolutionError?.message ?? String(highResolutionError),
|
|
489
|
+
...(highResolutionDiagnostics ?? frameDiagnostics)
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const shouldTryCameraAutoColor = scanOptions.cameraAutoColorRecovery !== false &&
|
|
495
|
+
baseFrame.imageData &&
|
|
496
|
+
((missStreak - 1) % cameraAutoColorEvery === 0);
|
|
497
|
+
if (shouldTryCameraAutoColor) {
|
|
498
|
+
const requestedCrops = Array.isArray(scanOptions.cameraAutoColorCropInsets)
|
|
499
|
+
? scanOptions.cameraAutoColorCropInsets
|
|
500
|
+
: [0.08, 0.16, 0.22, 0];
|
|
501
|
+
const cropInsets = [];
|
|
502
|
+
for (const value of requestedCrops) {
|
|
503
|
+
const inset = clampNumber(Number(value), 0, 0.30);
|
|
504
|
+
if (!cropInsets.some((item) => Math.abs(item - inset) < 0.001)) cropInsets.push(inset);
|
|
505
|
+
}
|
|
506
|
+
const explicitAnalysisInsets = Array.isArray(scanOptions.cameraAutoColorAnalysisInsets)
|
|
507
|
+
? scanOptions.cameraAutoColorAnalysisInsets
|
|
508
|
+
: null;
|
|
509
|
+
|
|
510
|
+
pushDiagnostic(events, {
|
|
511
|
+
type: "method",
|
|
512
|
+
state: "trying",
|
|
513
|
+
method: "camera-auto-color",
|
|
514
|
+
message: `Fast scan failed · QuadQR Auto Color recovery inside camera guide (${cropInsets.map((v) => v ? `${Math.round(v * 100)}% crop` : "full frame").join(" → ")})`,
|
|
515
|
+
...frameDiagnostics
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
for (let profileIndex = 0; profileIndex < cropInsets.length; profileIndex++) {
|
|
519
|
+
const cropInset = cropInsets[profileIndex];
|
|
520
|
+
const cropped = cropImageDataInset(baseFrame.imageData, cropInset);
|
|
521
|
+
const defaultAnalysisInsets = [0.10, 0.08, 0.04, 0.10];
|
|
522
|
+
const analysisInset = clampNumber(
|
|
523
|
+
Number(explicitAnalysisInsets?.[profileIndex]
|
|
524
|
+
?? scanOptions.cameraAutoColorAnalysisInset
|
|
525
|
+
?? defaultAnalysisInsets[Math.min(profileIndex, defaultAnalysisInsets.length - 1)]),
|
|
526
|
+
0,
|
|
527
|
+
0.30
|
|
528
|
+
);
|
|
529
|
+
const autoColorObservations = [];
|
|
530
|
+
const autoColorVisionDiagnostics = { passes: [] };
|
|
531
|
+
const autoColorFrameDiagnostics = {};
|
|
532
|
+
const cropLabel = cropInset ? `${Math.round(cropInset * 100)}pct-crop` : "full";
|
|
533
|
+
const profileName = `camera-auto-color-${cropLabel}`;
|
|
534
|
+
try {
|
|
535
|
+
const recoveryStarted = nowMs();
|
|
536
|
+
const correctedFrame = autoColorImageData(cropped.imageData, {
|
|
537
|
+
blackClip: scanOptions.cameraAutoColorBlackClip ?? 0.0001,
|
|
538
|
+
whiteClip: scanOptions.cameraAutoColorWhiteClip ?? 0.004,
|
|
539
|
+
highlightPercentile: scanOptions.cameraAutoColorHighlightPercentile ?? 0.95,
|
|
540
|
+
outputHighlight: scanOptions.cameraAutoColorOutputHighlight ?? 190,
|
|
541
|
+
analysisInset,
|
|
542
|
+
minimumInputRange: scanOptions.cameraAutoColorMinimumInputRange ?? 72,
|
|
543
|
+
targetSamples: scanOptions.cameraAutoColorTargetSamples ?? 90000
|
|
544
|
+
});
|
|
545
|
+
const recovered = scanImageData(correctedFrame, {
|
|
546
|
+
...scanOptions,
|
|
547
|
+
_diagnosticLabel: profileName,
|
|
548
|
+
_visionDiagnostics: autoColorVisionDiagnostics,
|
|
549
|
+
finderRecovery: true,
|
|
550
|
+
autoEnhanceRecovery: false,
|
|
551
|
+
fullFrameAutoEnhanceRecovery: false,
|
|
552
|
+
_observationCollector: autoColorObservations
|
|
553
|
+
});
|
|
554
|
+
normalizeFrameDiagnostics(
|
|
555
|
+
autoColorFrameDiagnostics,
|
|
556
|
+
baseFrame.source,
|
|
557
|
+
correctedFrame.width,
|
|
558
|
+
correctedFrame.height,
|
|
559
|
+
autoColorVisionDiagnostics
|
|
560
|
+
);
|
|
561
|
+
autoColorFrameDiagnostics.frameWidth = baseFrame.scanWidth;
|
|
562
|
+
autoColorFrameDiagnostics.frameHeight = baseFrame.scanHeight;
|
|
563
|
+
autoColorFrameDiagnostics.scanRect = { ...cropped.rect };
|
|
564
|
+
autoColorFrameDiagnostics.autoColorCropInset = cropInset;
|
|
565
|
+
autoColorFrameDiagnostics.autoColorAnalysisInset = analysisInset;
|
|
566
|
+
attachGeometryDiagnostic(autoColorFrameDiagnostics, recovered);
|
|
567
|
+
const recoveryElapsedMs = nowMs() - recoveryStarted;
|
|
568
|
+
pushDiagnostic(events, {
|
|
569
|
+
type: "frame",
|
|
570
|
+
state: "decoded",
|
|
571
|
+
method: profileName,
|
|
572
|
+
elapsedMs: recoveryElapsedMs,
|
|
573
|
+
missStreak,
|
|
574
|
+
...autoColorFrameDiagnostics
|
|
575
|
+
});
|
|
576
|
+
pushDiagnostic(events, {
|
|
577
|
+
type: "success",
|
|
578
|
+
state: "decoded",
|
|
579
|
+
method: "camera-auto-color",
|
|
580
|
+
elapsedMs: recoveryElapsedMs,
|
|
581
|
+
message: `QuadQR Auto Color ${cropInset ? `${Math.round(cropInset * 100)}% crop` : "full frame"} decoded v${recovered.version} · ECC ${recovered.eccLevel} · ${Math.round(recoveryElapsedMs)} ms`,
|
|
582
|
+
...autoColorFrameDiagnostics
|
|
583
|
+
});
|
|
584
|
+
missStreak = 0;
|
|
585
|
+
observationHistory.clear();
|
|
586
|
+
return {
|
|
587
|
+
ok: true,
|
|
588
|
+
result: {
|
|
589
|
+
...recovered,
|
|
590
|
+
autoColorCorrected: true,
|
|
591
|
+
cameraProgressiveRecovery: true,
|
|
592
|
+
recoveryMode: "camera-auto-color",
|
|
593
|
+
cameraAutoColorCropInset: cropInset,
|
|
594
|
+
cameraAutoColorAnalysisInset: analysisInset
|
|
595
|
+
},
|
|
596
|
+
frameMeta: makeFrameMeta(baseFrame, autoColorFrameDiagnostics, {
|
|
597
|
+
imageData: correctedFrame,
|
|
598
|
+
rect: cropped.rect,
|
|
599
|
+
meta: { method: "camera-auto-color", cropInset, analysisInset }
|
|
600
|
+
}),
|
|
601
|
+
diagnostics: events,
|
|
602
|
+
elapsedMs: nowMs() - frameStarted,
|
|
603
|
+
missStreak
|
|
604
|
+
};
|
|
605
|
+
} catch (recoveryError) {
|
|
606
|
+
observations.push(...autoColorObservations);
|
|
607
|
+
normalizeFrameDiagnostics(
|
|
608
|
+
autoColorFrameDiagnostics,
|
|
609
|
+
baseFrame.source,
|
|
610
|
+
cropped.imageData.width,
|
|
611
|
+
cropped.imageData.height,
|
|
612
|
+
autoColorVisionDiagnostics
|
|
613
|
+
);
|
|
614
|
+
autoColorFrameDiagnostics.frameWidth = baseFrame.scanWidth;
|
|
615
|
+
autoColorFrameDiagnostics.frameHeight = baseFrame.scanHeight;
|
|
616
|
+
autoColorFrameDiagnostics.scanRect = { ...cropped.rect };
|
|
617
|
+
autoColorFrameDiagnostics.autoColorCropInset = cropInset;
|
|
618
|
+
autoColorFrameDiagnostics.autoColorAnalysisInset = analysisInset;
|
|
619
|
+
pushDiagnostic(events, {
|
|
620
|
+
type: "frame",
|
|
621
|
+
state: "miss",
|
|
622
|
+
method: profileName,
|
|
623
|
+
elapsedMs: nowMs() - frameStarted,
|
|
624
|
+
missStreak,
|
|
625
|
+
error: recoveryError?.message ?? String(recoveryError),
|
|
626
|
+
...autoColorFrameDiagnostics
|
|
627
|
+
});
|
|
628
|
+
pushDiagnostic(events, {
|
|
629
|
+
type: "method",
|
|
630
|
+
state: "failed",
|
|
631
|
+
method: profileName,
|
|
632
|
+
message: `QuadQR Auto Color ${cropInset ? `${Math.round(cropInset * 100)}% crop` : "full frame"} did not decode${autoColorFrameDiagnostics?.finderCount != null ? ` · ${autoColorFrameDiagnostics.finderCount} finder(s)` : ""}`,
|
|
633
|
+
...autoColorFrameDiagnostics
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
pushDiagnostic(events, {
|
|
638
|
+
type: "method",
|
|
639
|
+
state: "failed",
|
|
640
|
+
method: "camera-auto-color",
|
|
641
|
+
message: "All camera QuadQR Auto Color profiles failed · continuing deeper recovery",
|
|
642
|
+
...frameDiagnostics
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!allowAutoEnhance && scanOptions.autoEnhanceRecovery !== false) {
|
|
647
|
+
const strongObservation = selectBestFrameObservation(observations);
|
|
648
|
+
if (strongObservation) {
|
|
649
|
+
pushDiagnostic(events, {
|
|
650
|
+
type: "method",
|
|
651
|
+
state: "trying",
|
|
652
|
+
method: "qr-region-auto-enhance",
|
|
653
|
+
message: "Fast decode failed · trying QR-region Auto Tone / Contrast / Color",
|
|
654
|
+
...frameDiagnostics
|
|
655
|
+
});
|
|
656
|
+
try {
|
|
657
|
+
const recoveryStarted = nowMs();
|
|
658
|
+
const recoveryObservations = [];
|
|
659
|
+
const recoveryVisionDiagnostics = { passes: [] };
|
|
660
|
+
const recovered = scanImageData(baseFrame.imageData, {
|
|
661
|
+
...scanOptions,
|
|
662
|
+
_diagnosticLabel: "qr-region-auto-enhance",
|
|
663
|
+
_visionDiagnostics: recoveryVisionDiagnostics,
|
|
664
|
+
autoEnhanceRecovery: true,
|
|
665
|
+
autoEnhanceWhenNoGeometry: false,
|
|
666
|
+
fullFrameAutoEnhanceRecovery: false,
|
|
667
|
+
_observationCollector: recoveryObservations
|
|
668
|
+
});
|
|
669
|
+
const recoveryElapsedMs = nowMs() - recoveryStarted;
|
|
670
|
+
pushDiagnostic(events, {
|
|
671
|
+
type: "success",
|
|
672
|
+
state: "decoded",
|
|
673
|
+
method: recovered.recoveryMode ?? recovered.samplingMode ?? "qr-region-auto-enhance",
|
|
674
|
+
elapsedMs: recoveryElapsedMs,
|
|
675
|
+
message: `Recovery decoded v${recovered.version} · ECC ${recovered.eccLevel} · ${Math.round(recoveryElapsedMs)} ms`,
|
|
676
|
+
...frameDiagnostics
|
|
677
|
+
});
|
|
678
|
+
missStreak = 0;
|
|
679
|
+
observationHistory.clear();
|
|
680
|
+
return {
|
|
681
|
+
ok: true,
|
|
682
|
+
result: { ...recovered, cameraProgressiveRecovery: true },
|
|
683
|
+
frameMeta: makeFrameMeta(baseFrame, frameDiagnostics),
|
|
684
|
+
diagnostics: events,
|
|
685
|
+
elapsedMs: nowMs() - frameStarted,
|
|
686
|
+
missStreak
|
|
687
|
+
};
|
|
688
|
+
} catch {
|
|
689
|
+
pushDiagnostic(events, {
|
|
690
|
+
type: "method",
|
|
691
|
+
state: "failed",
|
|
692
|
+
method: "qr-region-auto-enhance",
|
|
693
|
+
message: "QR-region color recovery did not decode · trying multi-frame ECC",
|
|
694
|
+
...frameDiagnostics
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const combined = tryMultiFrameDecode(observations);
|
|
701
|
+
if (combined) {
|
|
702
|
+
pushDiagnostic(events, {
|
|
703
|
+
type: "success",
|
|
704
|
+
state: "decoded",
|
|
705
|
+
method: "multi-frame-confidence-fusion",
|
|
706
|
+
message: `Multi-frame confidence fusion decoded v${combined.version} from ${combined.multiFrameCombined} frames`,
|
|
707
|
+
...frameDiagnostics
|
|
708
|
+
});
|
|
709
|
+
missStreak = 0;
|
|
710
|
+
return {
|
|
711
|
+
ok: true,
|
|
712
|
+
result: combined,
|
|
713
|
+
frameMeta: makeFrameMeta(baseFrame, frameDiagnostics),
|
|
714
|
+
diagnostics: events,
|
|
715
|
+
elapsedMs: nowMs() - frameStarted,
|
|
716
|
+
missStreak
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
ok: false,
|
|
722
|
+
error: serializeError(error),
|
|
723
|
+
diagnostics: events,
|
|
724
|
+
elapsedMs: nowMs() - frameStarted,
|
|
725
|
+
missStreak
|
|
726
|
+
};
|
|
727
|
+
} finally {
|
|
728
|
+
try { bitmap.close(); } catch {}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function initialize(options) {
|
|
733
|
+
scanOptions = options ?? {};
|
|
734
|
+
missStreak = 0;
|
|
735
|
+
cameraGeometryHint = null;
|
|
736
|
+
cameraGeometryHintMisses = 0;
|
|
737
|
+
observationHistory = new Map();
|
|
738
|
+
try {
|
|
739
|
+
wasmState = await initWasm();
|
|
740
|
+
} catch {
|
|
741
|
+
wasmState = null;
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
worker: true,
|
|
745
|
+
wasm: wasmState,
|
|
746
|
+
offscreenCanvas: typeof OffscreenCanvas === "function"
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
self.addEventListener("message", async (event) => {
|
|
751
|
+
const message = event.data ?? {};
|
|
752
|
+
const id = message.id;
|
|
753
|
+
if (!id) return;
|
|
754
|
+
try {
|
|
755
|
+
if (message.type === "init") {
|
|
756
|
+
const result = await initialize(message.options ?? {});
|
|
757
|
+
self.postMessage({ id, ok: true, type: "init", result });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (message.type === "reset") {
|
|
761
|
+
missStreak = 0;
|
|
762
|
+
cameraGeometryHint = null;
|
|
763
|
+
cameraGeometryHintMisses = 0;
|
|
764
|
+
observationHistory.clear();
|
|
765
|
+
self.postMessage({ id, ok: true, type: "reset" });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (message.type === "scan" || message.type === "scan-full") {
|
|
769
|
+
if (!(message.bitmap instanceof ImageBitmap)) throw new Error("Camera worker expected an ImageBitmap frame.");
|
|
770
|
+
if (typeof OffscreenCanvas !== "function") throw new Error("OffscreenCanvas is unavailable in the camera worker.");
|
|
771
|
+
const previousOptions = scanOptions;
|
|
772
|
+
if (message.type === "scan-full") {
|
|
773
|
+
// Safety fallback for browsers that allow one module worker but reject
|
|
774
|
+
// creation of the second recovery worker. Run the exact full pipeline
|
|
775
|
+
// in the existing worker rather than silently losing recovery power.
|
|
776
|
+
scanOptions = { ...previousOptions, ...(message.options ?? {}), cameraPipelineMode: "full" };
|
|
777
|
+
}
|
|
778
|
+
let result;
|
|
779
|
+
try {
|
|
780
|
+
result = processFrame(message.bitmap, message.source, message.frame ?? 0);
|
|
781
|
+
} finally {
|
|
782
|
+
scanOptions = previousOptions;
|
|
783
|
+
}
|
|
784
|
+
const transfers = result.ok ? transferableBuffers(result.frameMeta) : [];
|
|
785
|
+
self.postMessage({ id, ok: true, type: message.type, result }, transfers);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
throw new Error(`Unknown camera worker message: ${message.type}`);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
try { message.bitmap?.close?.(); } catch {}
|
|
791
|
+
self.postMessage({ id, ok: false, type: message.type, error: serializeError(error) });
|
|
792
|
+
}
|
|
793
|
+
});
|