@gueshbzh-perso/scanner 0.5.1 → 0.7.0

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -14
  3. package/dist/camera.d.ts.map +1 -1
  4. package/dist/camera.js +5 -2
  5. package/dist/camera.js.map +1 -1
  6. package/dist/core.d.ts +28 -10
  7. package/dist/core.d.ts.map +1 -1
  8. package/dist/core.js +92 -42
  9. package/dist/core.js.map +1 -1
  10. package/dist/engine.d.ts +26 -3
  11. package/dist/engine.d.ts.map +1 -1
  12. package/dist/engine.js +159 -51
  13. package/dist/engine.js.map +1 -1
  14. package/dist/grabber.d.ts +28 -0
  15. package/dist/grabber.d.ts.map +1 -1
  16. package/dist/grabber.js +81 -3
  17. package/dist/grabber.js.map +1 -1
  18. package/dist/index.d.ts +0 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/overlay.d.ts.map +1 -1
  23. package/dist/overlay.js +12 -11
  24. package/dist/overlay.js.map +1 -1
  25. package/dist/protocol.d.ts +9 -13
  26. package/dist/protocol.d.ts.map +1 -1
  27. package/dist/reader.d.ts +33 -3
  28. package/dist/reader.d.ts.map +1 -1
  29. package/dist/reader.js +113 -25
  30. package/dist/reader.js.map +1 -1
  31. package/dist/types.d.ts +11 -5
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/wasm/THIRD_PARTY_LICENSES.md +3879 -0
  34. package/dist/wasm/scanner_wasm.d.ts +52 -0
  35. package/dist/wasm/scanner_wasm.js +87 -0
  36. package/dist/wasm/scanner_wasm_bg.wasm +0 -0
  37. package/dist/wasm/scanner_wasm_bg.wasm.d.ts +6 -0
  38. package/dist/worker.js +15 -17
  39. package/dist/worker.js.map +1 -1
  40. package/package.json +6 -3
  41. package/src/camera.ts +107 -0
  42. package/src/core.ts +189 -0
  43. package/src/engine.ts +348 -0
  44. package/src/feedback.ts +50 -0
  45. package/src/geometry.ts +55 -0
  46. package/src/grabber.ts +385 -0
  47. package/src/index.ts +20 -0
  48. package/src/overlay.ts +225 -0
  49. package/src/protocol.ts +38 -0
  50. package/src/reader.ts +474 -0
  51. package/src/scan-image.ts +88 -0
  52. package/src/types.ts +155 -0
  53. package/src/worker.ts +57 -0
  54. package/dist/wasm/scanner_wasm_simd_bg.wasm +0 -0
@@ -23,6 +23,18 @@ export type BarcodeFormat =
23
23
  export type PixelFormat = "rgba" | "rgb" | "luma";
24
24
 
25
25
 
26
+ export interface FramePlan {
27
+ fallback?: number;
28
+ }
29
+
30
+ export interface FrameReport {
31
+ plan: FramePlan;
32
+ results: ScanResult[];
33
+ fallbackRan: boolean;
34
+ fallbackRead: boolean;
35
+ elapsedMs: number;
36
+ }
37
+
26
38
  export interface Point {
27
39
  x: number;
28
40
  y: number;
@@ -66,6 +78,29 @@ export interface WasmScanConfig {
66
78
  }
67
79
 
68
80
 
81
+ /**
82
+ * Stateless frame decoder for parallel decoding: one per Web Worker, all
83
+ * serving a single `WasmScanSession` (created with the same configuration)
84
+ * that plans the frames and commits the reports.
85
+ */
86
+ export class WasmFrameDecoder {
87
+ free(): void;
88
+ [Symbol.dispose](): void;
89
+ /**
90
+ * Decode the frame previously written into `frameBuffer`, following `plan`.
91
+ */
92
+ decodeFrameBuffer(width: number, height: number, format: PixelFormat, plan: FramePlan): FrameReport;
93
+ /**
94
+ * Get a view of the internal frame buffer resized to `len` bytes, then
95
+ * call `decodeFrameBuffer`. **Do not keep the view** across calls.
96
+ */
97
+ frameBuffer(len: number): Uint8Array;
98
+ /**
99
+ * Create a decoder. Use the configuration of the session it serves.
100
+ */
101
+ constructor(config?: WasmScanConfig | null);
102
+ }
103
+
69
104
  /**
70
105
  * Stateful session for camera streams: strategy rotation across frames,
71
106
  * deduplication, multi-frame confirmation and optional throttling.
@@ -77,6 +112,11 @@ export class WasmScanSession {
77
112
  * Allow `text` to be reported again immediately.
78
113
  */
79
114
  clearCooldown(text: string): void;
115
+ /**
116
+ * Record a frame decoded by a `WasmFrameDecoder`. Returns the **new**
117
+ * codes, or `null`, like `processFrameBuffer`.
118
+ */
119
+ commit(report: FrameReport): ScanResult[] | null;
80
120
  /**
81
121
  * Get a view of the internal frame buffer resized to `len` bytes.
82
122
  *
@@ -89,6 +129,12 @@ export class WasmScanSession {
89
129
  * Create a session for a camera stream.
90
130
  */
91
131
  constructor(config?: WasmScanConfig | null);
132
+ /**
133
+ * Decide what the next frame runs, when frames are decoded elsewhere
134
+ * (`WasmFrameDecoder` in Web Workers). Every plan must come back to
135
+ * `commit`, in any order. The adaptive throttle does not apply.
136
+ */
137
+ plan(): FramePlan;
92
138
  /**
93
139
  * Process the frame previously written into `frameBuffer`.
94
140
  */
@@ -165,9 +211,13 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
165
211
 
166
212
  export interface InitOutput {
167
213
  readonly memory: WebAssembly.Memory;
214
+ readonly __wbg_wasmframedecoder_free: (a: number, b: number) => void;
168
215
  readonly __wbg_wasmscanner_free: (a: number, b: number) => void;
169
216
  readonly __wbg_wasmscansession_free: (a: number, b: number) => void;
170
217
  readonly version: (a: number) => void;
218
+ readonly wasmframedecoder_decodeFrameBuffer: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
219
+ readonly wasmframedecoder_frameBuffer: (a: number, b: number) => number;
220
+ readonly wasmframedecoder_new: (a: number) => number;
171
221
  readonly wasmscanner_frameBuffer: (a: number, b: number) => number;
172
222
  readonly wasmscanner_new: (a: number) => number;
173
223
  readonly wasmscanner_scanFrameBuffer: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
@@ -175,9 +225,11 @@ export interface InitOutput {
175
225
  readonly wasmscanner_scanRgb: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
176
226
  readonly wasmscanner_scanRgba: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
177
227
  readonly wasmscansession_clearCooldown: (a: number, b: number, c: number) => void;
228
+ readonly wasmscansession_commit: (a: number, b: number) => number;
178
229
  readonly wasmscansession_frameBuffer: (a: number, b: number) => number;
179
230
  readonly wasmscansession_lastDetections: (a: number) => number;
180
231
  readonly wasmscansession_new: (a: number) => number;
232
+ readonly wasmscansession_plan: (a: number) => number;
181
233
  readonly wasmscansession_processFrameBuffer: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
182
234
  readonly wasmscansession_processFrameLuma: (a: number, b: number, c: number, d: number, e: number) => number;
183
235
  readonly wasmscansession_processFrameRgba: (a: number, b: number, c: number, d: number, e: number) => number;
@@ -1,5 +1,69 @@
1
1
  /* @ts-self-types="./scanner_wasm.d.ts" */
2
2
 
3
+ /**
4
+ * Stateless frame decoder for parallel decoding: one per Web Worker, all
5
+ * serving a single `WasmScanSession` (created with the same configuration)
6
+ * that plans the frames and commits the reports.
7
+ */
8
+ export class WasmFrameDecoder {
9
+ __destroy_into_raw() {
10
+ const ptr = this.__wbg_ptr;
11
+ this.__wbg_ptr = 0;
12
+ WasmFrameDecoderFinalization.unregister(this);
13
+ return ptr;
14
+ }
15
+ free() {
16
+ const ptr = this.__destroy_into_raw();
17
+ wasm.__wbg_wasmframedecoder_free(ptr, 0);
18
+ }
19
+ /**
20
+ * Decode the frame previously written into `frameBuffer`, following `plan`.
21
+ * @param {number} width
22
+ * @param {number} height
23
+ * @param {PixelFormat} format
24
+ * @param {FramePlan} plan
25
+ * @returns {FrameReport}
26
+ */
27
+ decodeFrameBuffer(width, height, format, plan) {
28
+ try {
29
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
30
+ const ptr0 = passStringToWasm0(format, wasm.__wbindgen_export, wasm.__wbindgen_export2);
31
+ const len0 = WASM_VECTOR_LEN;
32
+ wasm.wasmframedecoder_decodeFrameBuffer(retptr, this.__wbg_ptr, width, height, ptr0, len0, addHeapObject(plan));
33
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
34
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
35
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
36
+ if (r2) {
37
+ throw takeObject(r1);
38
+ }
39
+ return takeObject(r0);
40
+ } finally {
41
+ wasm.__wbindgen_add_to_stack_pointer(16);
42
+ }
43
+ }
44
+ /**
45
+ * Get a view of the internal frame buffer resized to `len` bytes, then
46
+ * call `decodeFrameBuffer`. **Do not keep the view** across calls.
47
+ * @param {number} len
48
+ * @returns {Uint8Array}
49
+ */
50
+ frameBuffer(len) {
51
+ const ret = wasm.wasmframedecoder_frameBuffer(this.__wbg_ptr, len);
52
+ return takeObject(ret);
53
+ }
54
+ /**
55
+ * Create a decoder. Use the configuration of the session it serves.
56
+ * @param {WasmScanConfig | null} [config]
57
+ */
58
+ constructor(config) {
59
+ const ret = wasm.wasmframedecoder_new(isLikeNone(config) ? 0 : addHeapObject(config));
60
+ this.__wbg_ptr = ret;
61
+ WasmFrameDecoderFinalization.register(this, this.__wbg_ptr, this);
62
+ return this;
63
+ }
64
+ }
65
+ if (Symbol.dispose) WasmFrameDecoder.prototype[Symbol.dispose] = WasmFrameDecoder.prototype.free;
66
+
3
67
  /**
4
68
  * Stateful session for camera streams: strategy rotation across frames,
5
69
  * deduplication, multi-frame confirmation and optional throttling.
@@ -24,6 +88,16 @@ export class WasmScanSession {
24
88
  const len0 = WASM_VECTOR_LEN;
25
89
  wasm.wasmscansession_clearCooldown(this.__wbg_ptr, ptr0, len0);
26
90
  }
91
+ /**
92
+ * Record a frame decoded by a `WasmFrameDecoder`. Returns the **new**
93
+ * codes, or `null`, like `processFrameBuffer`.
94
+ * @param {FrameReport} report
95
+ * @returns {ScanResult[] | null}
96
+ */
97
+ commit(report) {
98
+ const ret = wasm.wasmscansession_commit(this.__wbg_ptr, addHeapObject(report));
99
+ return takeObject(ret);
100
+ }
27
101
  /**
28
102
  * Get a view of the internal frame buffer resized to `len` bytes.
29
103
  *
@@ -56,6 +130,16 @@ export class WasmScanSession {
56
130
  WasmScanSessionFinalization.register(this, this.__wbg_ptr, this);
57
131
  return this;
58
132
  }
133
+ /**
134
+ * Decide what the next frame runs, when frames are decoded elsewhere
135
+ * (`WasmFrameDecoder` in Web Workers). Every plan must come back to
136
+ * `commit`, in any order. The adaptive throttle does not apply.
137
+ * @returns {FramePlan}
138
+ */
139
+ plan() {
140
+ const ret = wasm.wasmscansession_plan(this.__wbg_ptr);
141
+ return takeObject(ret);
142
+ }
59
143
  /**
60
144
  * Process the frame previously written into `frameBuffer`.
61
145
  * @param {number} width
@@ -548,6 +632,9 @@ function __wbg_get_imports() {
548
632
  };
549
633
  }
550
634
 
635
+ const WasmFrameDecoderFinalization = (typeof FinalizationRegistry === 'undefined')
636
+ ? { register: () => {}, unregister: () => {} }
637
+ : new FinalizationRegistry(ptr => wasm.__wbg_wasmframedecoder_free(ptr, 1));
551
638
  const WasmScanSessionFinalization = (typeof FinalizationRegistry === 'undefined')
552
639
  ? { register: () => {}, unregister: () => {} }
553
640
  : new FinalizationRegistry(ptr => wasm.__wbg_wasmscansession_free(ptr, 1));
Binary file
@@ -1,9 +1,13 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
+ export const __wbg_wasmframedecoder_free: (a: number, b: number) => void;
4
5
  export const __wbg_wasmscanner_free: (a: number, b: number) => void;
5
6
  export const __wbg_wasmscansession_free: (a: number, b: number) => void;
6
7
  export const version: (a: number) => void;
8
+ export const wasmframedecoder_decodeFrameBuffer: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
9
+ export const wasmframedecoder_frameBuffer: (a: number, b: number) => number;
10
+ export const wasmframedecoder_new: (a: number) => number;
7
11
  export const wasmscanner_frameBuffer: (a: number, b: number) => number;
8
12
  export const wasmscanner_new: (a: number) => number;
9
13
  export const wasmscanner_scanFrameBuffer: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
@@ -11,9 +15,11 @@ export const wasmscanner_scanLuma: (a: number, b: number, c: number, d: number,
11
15
  export const wasmscanner_scanRgb: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
12
16
  export const wasmscanner_scanRgba: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
13
17
  export const wasmscansession_clearCooldown: (a: number, b: number, c: number) => void;
18
+ export const wasmscansession_commit: (a: number, b: number) => number;
14
19
  export const wasmscansession_frameBuffer: (a: number, b: number) => number;
15
20
  export const wasmscansession_lastDetections: (a: number) => number;
16
21
  export const wasmscansession_new: (a: number) => number;
22
+ export const wasmscansession_plan: (a: number) => number;
17
23
  export const wasmscansession_processFrameBuffer: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
18
24
  export const wasmscansession_processFrameLuma: (a: number, b: number, c: number, d: number, e: number) => number;
19
25
  export const wasmscansession_processFrameRgba: (a: number, b: number, c: number, d: number, e: number) => number;
package/dist/worker.js CHANGED
@@ -1,5 +1,7 @@
1
- // Web Worker entry point: decodes frames off the main thread.
2
- import { EngineCore } from './core.js';
1
+ // Web Worker entry point: decodes frames off the main thread. Stateless: the
2
+ // scan session (scheduling, confirmations, de-duplication) lives on the main
3
+ // thread, so any number of these workers can serve one camera stream.
4
+ import { DecoderCore, loadWasm } from './core.js';
3
5
  const scope = self;
4
6
  let core = null;
5
7
  function engine() {
@@ -12,33 +14,29 @@ scope.onmessage = async (ev) => {
12
14
  const buffer = 'buffer' in msg ? msg.buffer : undefined;
13
15
  try {
14
16
  switch (msg.type) {
15
- case 'init':
16
- core = await EngineCore.create(msg.wasmUrl);
17
- scope.postMessage({ id: msg.id, ok: true, version: core.version });
17
+ case 'init': {
18
+ const version = await loadWasm(msg.module);
19
+ core = new DecoderCore();
20
+ scope.postMessage({ id: msg.id, ok: true, version });
18
21
  break;
22
+ }
19
23
  case 'configure':
20
24
  engine().configure(msg.options);
21
25
  scope.postMessage({ id: msg.id, ok: true });
22
26
  break;
23
- case 'frame': {
24
- const out = engine().process(new Uint8Array(msg.buffer), msg.width, msg.height, msg.format);
27
+ case 'decode': {
28
+ const pixels = new Uint8Array(msg.buffer);
29
+ const report = engine().decode(pixels, msg.width, msg.height, msg.format, msg.plan);
25
30
  // Hand the pixel buffer back so the main thread can reuse it.
26
- scope.postMessage({ id: msg.id, ok: true, ...out, buffer: msg.buffer }, [msg.buffer]);
31
+ scope.postMessage({ id: msg.id, ok: true, report, buffer: msg.buffer }, [msg.buffer]);
27
32
  break;
28
33
  }
29
34
  case 'scan': {
30
- const results = engine().scan(new Uint8Array(msg.buffer), msg.width, msg.height, msg.format, msg.options);
35
+ const pixels = new Uint8Array(msg.buffer);
36
+ const results = engine().scan(pixels, msg.width, msg.height, msg.format, msg.options);
31
37
  scope.postMessage({ id: msg.id, ok: true, results, buffer: msg.buffer }, [msg.buffer]);
32
38
  break;
33
39
  }
34
- case 'reset':
35
- engine().reset();
36
- scope.postMessage({ id: msg.id, ok: true });
37
- break;
38
- case 'clearCooldown':
39
- engine().clearCooldown(msg.text);
40
- scope.postMessage({ id: msg.id, ok: true });
41
- break;
42
40
  }
43
41
  }
44
42
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@gueshbzh-perso/scanner",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Fast QR code & barcode scanning for the web: camera, WebGL frame pipeline, Web Worker and Rust/WebAssembly decoder.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/romainrodrigues/scanner"
9
+ "url": "https://github.com/RomainRodrigues/scanner"
10
10
  },
11
11
  "keywords": [
12
12
  "barcode",
@@ -33,6 +33,8 @@
33
33
  },
34
34
  "files": [
35
35
  "dist",
36
+ "src",
37
+ "!src/wasm",
36
38
  "README.md"
37
39
  ],
38
40
  "sideEffects": false,
@@ -42,7 +44,8 @@
42
44
  "build": "npm run build:wasm && npm run build:ts",
43
45
  "typecheck": "tsc -p tsconfig.json --noEmit",
44
46
  "test:e2e": "node scripts/e2e.mjs",
45
- "serve": "node scripts/serve.mjs"
47
+ "serve": "node scripts/serve.mjs",
48
+ "test": "node --test test/unit/*.test.mjs"
46
49
  },
47
50
  "devDependencies": {
48
51
  "binaryen": "^132.0.0",
package/src/camera.ts ADDED
@@ -0,0 +1,107 @@
1
+ // Camera access helpers (getUserMedia, device listing, torch, zoom, focus).
2
+ import type { CameraOptions, CameraResolution } from './types.js';
3
+
4
+ const RESOLUTIONS: Record<CameraResolution, [number, number]> = {
5
+ sd: [640, 480],
6
+ hd: [1280, 720],
7
+ fhd: [1920, 1080],
8
+ };
9
+
10
+ // Namespaced: the host page shares localStorage with the SDK.
11
+ const STORAGE_KEY = '@gueshbzh-perso/scanner:cameraId';
12
+ /** Key used up to 0.6: still read, so the remembered camera survives the upgrade. */
13
+ const LEGACY_STORAGE_KEY = 'scanner.cameraId';
14
+
15
+ export function rememberedCamera(): string | undefined {
16
+ try {
17
+ return localStorage.getItem(STORAGE_KEY) ?? localStorage.getItem(LEGACY_STORAGE_KEY) ?? undefined;
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ export function rememberCamera(id: string | undefined): void {
24
+ try {
25
+ if (id) localStorage.setItem(STORAGE_KEY, id);
26
+ } catch {
27
+ /* storage unavailable: ignore */
28
+ }
29
+ }
30
+
31
+ export async function openCamera(options: CameraOptions = {}): Promise<MediaStream> {
32
+ if (!navigator.mediaDevices?.getUserMedia) {
33
+ throw new Error('Camera access is not available (a secure https:// context is required)');
34
+ }
35
+ const [width, height] = RESOLUTIONS[options.resolution ?? 'hd'];
36
+ const deviceId = options.deviceId ?? (options.rememberDevice !== false ? rememberedCamera() : undefined);
37
+
38
+ const video: MediaTrackConstraints = {
39
+ width: { ideal: width },
40
+ height: { ideal: height },
41
+ frameRate: { ideal: 30 },
42
+ };
43
+ if (deviceId) video.deviceId = { exact: deviceId };
44
+ else video.facingMode = { ideal: options.facingMode ?? 'environment' };
45
+
46
+ let stream: MediaStream;
47
+ try {
48
+ stream = await navigator.mediaDevices.getUserMedia({ video, audio: false });
49
+ } catch (err) {
50
+ // A remembered camera may have been unplugged: retry with the facing mode.
51
+ if (!deviceId || options.deviceId) throw err;
52
+ delete video.deviceId;
53
+ video.facingMode = { ideal: options.facingMode ?? 'environment' };
54
+ stream = await navigator.mediaDevices.getUserMedia({ video, audio: false });
55
+ }
56
+
57
+ const track = stream.getVideoTracks()[0];
58
+ if (track) await applyAdvanced(track, { focusMode: 'continuous' });
59
+ return stream;
60
+ }
61
+
62
+ /** Video input devices. Labels are only populated once camera permission was granted. */
63
+ export async function listCameras(): Promise<MediaDeviceInfo[]> {
64
+ if (!navigator.mediaDevices?.enumerateDevices) return [];
65
+ const devices = await navigator.mediaDevices.enumerateDevices();
66
+ return devices.filter((d) => d.kind === 'videoinput');
67
+ }
68
+
69
+ interface ExtendedCapabilities extends MediaTrackCapabilities {
70
+ torch?: boolean;
71
+ zoom?: { min: number; max: number; step?: number };
72
+ focusMode?: string[];
73
+ }
74
+
75
+ export function capabilities(track: MediaStreamTrack | undefined): ExtendedCapabilities {
76
+ try {
77
+ return (track?.getCapabilities?.() ?? {}) as ExtendedCapabilities;
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+
83
+ export async function setTorch(track: MediaStreamTrack, on: boolean): Promise<void> {
84
+ if (!capabilities(track).torch) throw new Error('This camera has no controllable flashlight');
85
+ await track.applyConstraints({ advanced: [{ torch: on } as MediaTrackConstraintSet] });
86
+ }
87
+
88
+ export async function setZoom(track: MediaStreamTrack, zoom: number): Promise<void> {
89
+ const range = capabilities(track).zoom;
90
+ if (!range) throw new Error('This camera does not support zoom');
91
+ const value = Math.min(range.max, Math.max(range.min, zoom));
92
+ await track.applyConstraints({ advanced: [{ zoom: value } as MediaTrackConstraintSet] });
93
+ }
94
+
95
+ async function applyAdvanced(track: MediaStreamTrack, set: Record<string, unknown>): Promise<void> {
96
+ const caps = capabilities(track) as Record<string, unknown>;
97
+ const supported = Object.keys(set).filter((k) => {
98
+ const c = caps[k];
99
+ return Array.isArray(c) ? c.includes(set[k]) : c !== undefined;
100
+ });
101
+ if (supported.length === 0) return;
102
+ try {
103
+ await track.applyConstraints({ advanced: [Object.fromEntries(supported.map((k) => [k, set[k]]))] });
104
+ } catch {
105
+ /* best effort */
106
+ }
107
+ }
package/src/core.ts ADDED
@@ -0,0 +1,189 @@
1
+ // Engine core: owns the WebAssembly module and its objects.
2
+ //
3
+ // Two roles, so that frames can be decoded on several threads while a single
4
+ // session keeps the state (strategy scheduling, multi-frame confirmation,
5
+ // de-duplication):
6
+ // - `SessionCore`: the session. Cheap calls (`plan`, `commit`), main thread.
7
+ // - `DecoderCore`: the expensive decoding, one per Web Worker.
8
+ // The inline engine (no Worker support) uses both on the main thread.
9
+ import init, { WasmFrameDecoder, WasmScanner, WasmScanSession, version } from './wasm/scanner_wasm.js';
10
+ import type { FramePlan, FrameReport, PixelFormat, ScanResult, WasmScanConfig } from './wasm/scanner_wasm.js';
11
+ import type { Detection, EngineOptions } from './types.js';
12
+
13
+ export type { FramePlan, FrameReport };
14
+
15
+ /** Default location of the engine binary, next to this module. */
16
+ export function defaultWasmUrl(): URL {
17
+ return new URL('./wasm/scanner_wasm_bg.wasm', import.meta.url);
18
+ }
19
+
20
+ /**
21
+ * Compile the engine once. The compiled module can be posted to any number
22
+ * of workers, which then only instantiate it (no second download or compile).
23
+ */
24
+ export async function compileWasm(url: string | URL): Promise<WebAssembly.Module> {
25
+ const response = fetch(url);
26
+ try {
27
+ return await WebAssembly.compileStreaming(response);
28
+ } catch {
29
+ // Servers that do not send `application/wasm` reject streaming compilation.
30
+ return WebAssembly.compile(await (await response).arrayBuffer());
31
+ }
32
+ }
33
+
34
+ let initPromise: Promise<string> | null = null;
35
+
36
+ /** Instantiate the engine in this realm (once). Returns the engine version. */
37
+ export function loadWasm(source: string | URL | WebAssembly.Module): Promise<string> {
38
+ initPromise ??= init({ module_or_path: source }).then(() => version());
39
+ return initPromise;
40
+ }
41
+
42
+ /** Keep only the engine options (drops URLs and other non-cloneable values). */
43
+ export function pickEngineOptions(o: EngineOptions): EngineOptions {
44
+ const { formats, preprocessing, tryInvert, maxResults, duplicateIntervalMs, linearConfirmations } = o;
45
+ return { formats, preprocessing, tryInvert, maxResults, duplicateIntervalMs, linearConfirmations };
46
+ }
47
+
48
+ export function toWasmConfig(o: EngineOptions, extra: Partial<WasmScanConfig> = {}): WasmScanConfig {
49
+ const cfg: WasmScanConfig = { ...extra };
50
+ if (o.formats) cfg.formats = o.formats;
51
+ if (o.preprocessing !== undefined) cfg.preprocessing = o.preprocessing;
52
+ if (o.tryInvert !== undefined) cfg.tryInvert = o.tryInvert;
53
+ if (o.maxResults !== undefined) cfg.maxResults = o.maxResults;
54
+ cfg.dedupCooldownMs = o.duplicateIntervalMs ?? 1500;
55
+ if (o.linearConfirmations !== undefined) cfg.linearConfirmations = o.linearConfirmations;
56
+ return cfg;
57
+ }
58
+
59
+ /**
60
+ * Configuration of camera sessions and their decoders: they must be equal,
61
+ * since a plan refers to the decoder's list of fallback strategies.
62
+ */
63
+ function streamConfig(o: EngineOptions): WasmScanConfig {
64
+ // Back-pressure is handled by the SDK (frames are skipped, never queued),
65
+ // so the engine's own frame-skipping throttle would only drop useful frames.
66
+ return toWasmConfig(o, { adaptiveThrottle: false });
67
+ }
68
+
69
+ export function toDetection(r: ScanResult): Detection {
70
+ const d: Detection = { text: r.text, format: r.format, points: r.points, timestamp: r.timestampMs };
71
+ if (r.rawBytes) d.rawBytes = r.rawBytes;
72
+ return d;
73
+ }
74
+
75
+ export interface FrameOutcome {
76
+ /** New codes, or `null`. */
77
+ results: Detection[] | null;
78
+ /** Every code seen in this frame (including duplicates in cooldown). */
79
+ live: Detection[];
80
+ decodeMs: number;
81
+ }
82
+
83
+ /** The scan session of a camera stream (state only, cheap calls). */
84
+ export class SessionCore {
85
+ private session: WasmScanSession | null = null;
86
+
87
+ configure(options: EngineOptions): void {
88
+ this.session?.free();
89
+ this.session = new WasmScanSession(streamConfig(options));
90
+ }
91
+
92
+ /** Strategy plan of the next frame, for a `DecoderCore` configured the same way. */
93
+ plan(): FramePlan {
94
+ return this.active().plan();
95
+ }
96
+
97
+ /** Record a decoded frame; returns the new codes and the live ones. */
98
+ commit(report: FrameReport): FrameOutcome {
99
+ const session = this.active();
100
+ const fresh = session.commit(report);
101
+ const results = fresh ? fresh.map(toDetection) : null;
102
+ return { results, live: results ?? session.lastDetections.map(toDetection), decodeMs: report.elapsedMs };
103
+ }
104
+
105
+ /** Decode on this thread (inline engine): plan, decode and commit at once. */
106
+ process(pixels: Uint8Array, width: number, height: number, format: PixelFormat): FrameOutcome {
107
+ const session = this.active();
108
+ // Write straight into WASM memory: avoids one more copy of the frame.
109
+ session.frameBuffer(pixels.length).set(pixels);
110
+ const t0 = performance.now();
111
+ const fresh = session.processFrameBuffer(width, height, format);
112
+ const decodeMs = performance.now() - t0;
113
+ const results = fresh ? fresh.map(toDetection) : null;
114
+ return { results, live: results ?? session.lastDetections.map(toDetection), decodeMs };
115
+ }
116
+
117
+ reset(): void {
118
+ this.session?.reset();
119
+ }
120
+
121
+ clearCooldown(text: string): void {
122
+ this.session?.clearCooldown(text);
123
+ }
124
+
125
+ destroy(): void {
126
+ this.session?.free();
127
+ this.session = null;
128
+ }
129
+
130
+ private active(): WasmScanSession {
131
+ if (!this.session) throw new Error('Engine not configured');
132
+ return this.session;
133
+ }
134
+ }
135
+
136
+ /** Frame decoding (stateless) and still-image scanning: the expensive part. */
137
+ export class DecoderCore {
138
+ private decoder: WasmFrameDecoder | null = null;
139
+ private scanners = new Map<string, WasmScanner>();
140
+
141
+ configure(options: EngineOptions): void {
142
+ this.decoder?.free();
143
+ this.decoder = new WasmFrameDecoder(streamConfig(options));
144
+ }
145
+
146
+ decode(
147
+ pixels: Uint8Array,
148
+ width: number,
149
+ height: number,
150
+ format: PixelFormat,
151
+ plan: FramePlan,
152
+ ): FrameReport {
153
+ const decoder = this.decoder;
154
+ if (!decoder) throw new Error('Engine not configured');
155
+ decoder.frameBuffer(pixels.length).set(pixels);
156
+ return decoder.decodeFrameBuffer(width, height, format, plan);
157
+ }
158
+
159
+ /** One-shot scan of a still image, running every strategy. */
160
+ scan(
161
+ pixels: Uint8Array,
162
+ width: number,
163
+ height: number,
164
+ format: PixelFormat,
165
+ options: EngineOptions,
166
+ ): Detection[] {
167
+ const config = toWasmConfig(options);
168
+ const key = JSON.stringify(config);
169
+ let scanner = this.scanners.get(key);
170
+ if (!scanner) {
171
+ if (this.scanners.size >= 4) {
172
+ const [oldKey, old] = this.scanners.entries().next().value as [string, WasmScanner];
173
+ old.free();
174
+ this.scanners.delete(oldKey);
175
+ }
176
+ scanner = new WasmScanner(config);
177
+ this.scanners.set(key, scanner);
178
+ }
179
+ scanner.frameBuffer(pixels.length).set(pixels);
180
+ return scanner.scanFrameBuffer(width, height, format).map(toDetection);
181
+ }
182
+
183
+ destroy(): void {
184
+ this.decoder?.free();
185
+ this.decoder = null;
186
+ for (const s of this.scanners.values()) s.free();
187
+ this.scanners.clear();
188
+ }
189
+ }