@marianmeres/scanner 1.0.1
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/AGENTS.md +144 -0
- package/API.md +696 -0
- package/CLAUDE.md +3 -0
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/dist/camera-adapter.d.ts +31 -0
- package/dist/camera-adapter.js +96 -0
- package/dist/detector.d.ts +25 -0
- package/dist/detector.js +36 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.js +5 -0
- package/dist/scan-image.d.ts +24 -0
- package/dist/scan-image.js +35 -0
- package/dist/scanner.d.ts +17 -0
- package/dist/scanner.js +594 -0
- package/dist/stage.d.ts +59 -0
- package/dist/stage.js +334 -0
- package/dist/types.d.ts +262 -0
- package/dist/types.js +28 -0
- package/package.json +40 -0
package/API.md
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
# API
|
|
2
|
+
|
|
3
|
+
Package exports:
|
|
4
|
+
|
|
5
|
+
- `@marianmeres/scanner` — headless core (no DOM construction; DOM types only)
|
|
6
|
+
- `@marianmeres/scanner/stage` — framework-agnostic DOM stage UI
|
|
7
|
+
|
|
8
|
+
## Functions
|
|
9
|
+
|
|
10
|
+
### `createScanner(config?)`
|
|
11
|
+
|
|
12
|
+
Create a headless (no UI) camera barcode scanner.
|
|
13
|
+
|
|
14
|
+
Reactive state is exposed via the svelte-store-compatible `subscribe`/`get`. Methods
|
|
15
|
+
never throw — errors land in `state.error` (see
|
|
16
|
+
[`ScannerErrorCode`](#scannererrorcode)).
|
|
17
|
+
|
|
18
|
+
**Parameters:**
|
|
19
|
+
|
|
20
|
+
- `config` (`ScannerConfig`, optional) — Configuration options. See
|
|
21
|
+
[`ScannerConfig`](#scannerconfig).
|
|
22
|
+
|
|
23
|
+
**Returns:** `Scanner` — Scanner instance with reactive state
|
|
24
|
+
|
|
25
|
+
**Example:**
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const scanner = createScanner({ onScan: (r) => console.log(r.value) });
|
|
29
|
+
const result = await scanner.start(); // single-shot: auto-stops on first hit
|
|
30
|
+
scanner.destroy();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### `scanImage(source, options?)`
|
|
36
|
+
|
|
37
|
+
Decode barcodes from a still image — no camera involved. Useful for file uploads,
|
|
38
|
+
drag & drop, or as a fallback where `getUserMedia` is unavailable.
|
|
39
|
+
|
|
40
|
+
Unlike the scanner instance methods (which never throw), this standalone utility
|
|
41
|
+
**THROWS** on fetch/decode failure. An image without any recognizable code resolves
|
|
42
|
+
with `[]` (not an error).
|
|
43
|
+
|
|
44
|
+
**Parameters:**
|
|
45
|
+
|
|
46
|
+
- `source` (`DetectorSource | string`) — The image to decode: `Blob` / `File`,
|
|
47
|
+
`ImageData`, `ImageBitmap`, `HTMLImageElement`, `HTMLCanvasElement`,
|
|
48
|
+
`HTMLVideoElement`, or a URL string (fetched, then decoded as a `Blob`).
|
|
49
|
+
- `options` (`ScanImageOptions`, optional)
|
|
50
|
+
- `options.formats` (`BarcodeFormat[]`) — Formats to detect. Default `["qr_code"]`.
|
|
51
|
+
- `options.detector` (`Detector`) — Decoding engine seam. Default wraps the
|
|
52
|
+
`barcode-detector` ponyfill.
|
|
53
|
+
- `options.wasmOverrides` (`Record<string, unknown>`) — See
|
|
54
|
+
[`ScannerConfig.wasmOverrides`](#scannerconfig).
|
|
55
|
+
|
|
56
|
+
**Returns:** `Promise<ScanResult[]>` — All detections found in the image (all share
|
|
57
|
+
one timestamp).
|
|
58
|
+
|
|
59
|
+
**Example:**
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const results = await scanImage(file); // File | Blob | ImageData | <img> | url
|
|
63
|
+
if (results.length) console.log(results[0].value);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### `createDefaultCameraAdapter(options?)`
|
|
69
|
+
|
|
70
|
+
Create the default [`CameraAdapter`](#cameraadapter). Stream acquisition and
|
|
71
|
+
device enumeration wrap `navigator.mediaDevices` directly (the scanner must
|
|
72
|
+
RETAIN the stream — deliberately out of mediaperms' scope); the permission
|
|
73
|
+
lifecycle (query + change tracking, Android-WebView sticky-denial coercion,
|
|
74
|
+
bfcache/app-resume rechecks) is delegated to `@marianmeres/mediaperms`. Used
|
|
75
|
+
internally when no `config.adapter` is given; exported for consumers who want
|
|
76
|
+
to share a mediaperms instance or extend the default behavior.
|
|
77
|
+
|
|
78
|
+
**Parameters:**
|
|
79
|
+
|
|
80
|
+
- `options` (`CreateDefaultCameraAdapterOptions`, optional):
|
|
81
|
+
- `perms` (`MediaPerms`, optional) — share an existing app-owned
|
|
82
|
+
`@marianmeres/mediaperms` instance (camera kind). The adapter will NOT
|
|
83
|
+
destroy a shared instance. When omitted, the adapter lazily creates its
|
|
84
|
+
own via `createCamPerms()` and destroys it in `destroy()`.
|
|
85
|
+
- `permsConfig` (`MediaPermsConfig`, optional) — forwarded to
|
|
86
|
+
`createCamPerms()` when the adapter creates its own instance (platform
|
|
87
|
+
hints, webview bridges, logger…). Ignored when `perms` is provided.
|
|
88
|
+
|
|
89
|
+
**Returns:** `CameraAdapter`
|
|
90
|
+
|
|
91
|
+
**Example:**
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { createCamPerms } from "@marianmeres/mediaperms";
|
|
95
|
+
|
|
96
|
+
// self-contained (internal mediaperms instance):
|
|
97
|
+
const scanner = createScanner({ adapter: createDefaultCameraAdapter() });
|
|
98
|
+
|
|
99
|
+
// or share the app's instance:
|
|
100
|
+
const perms = createCamPerms();
|
|
101
|
+
const adapter = createDefaultCameraAdapter({ perms });
|
|
102
|
+
const scanner2 = createScanner({ adapter });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `createDefaultDetector(options?)`
|
|
108
|
+
|
|
109
|
+
Create the default [`Detector`](#detector) wrapping the `barcode-detector` ponyfill
|
|
110
|
+
(zxing-cpp wasm engine, all major 1D/2D formats).
|
|
111
|
+
|
|
112
|
+
NOTE: the ~1 MB decoder `.wasm` is fetched lazily (on first `detect()`) from the
|
|
113
|
+
jsDelivr CDN by default — pass `wasmOverrides.locateFile` to self-host it.
|
|
114
|
+
|
|
115
|
+
**Parameters:**
|
|
116
|
+
|
|
117
|
+
- `options` (`CreateDefaultDetectorOptions`, optional)
|
|
118
|
+
- `options.formats` (`BarcodeFormat[]`) — Formats to detect. Default `["qr_code"]`.
|
|
119
|
+
- `options.wasmOverrides` (`Record<string, unknown>`) — Overrides passed to
|
|
120
|
+
zxing-wasm's `prepareZXingModule` — most notably `locateFile` for self-hosting
|
|
121
|
+
the `.wasm` binary.
|
|
122
|
+
|
|
123
|
+
**Returns:** `Detector`
|
|
124
|
+
|
|
125
|
+
**Example:**
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const detector = createDefaultDetector({
|
|
129
|
+
formats: ["qr_code", "ean_13"],
|
|
130
|
+
wasmOverrides: { locateFile: (path: string) => `/assets/${path}` },
|
|
131
|
+
});
|
|
132
|
+
const scanner = createScanner({ detector });
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### `classifyAcquireError(e)`
|
|
138
|
+
|
|
139
|
+
Classify a `getUserMedia` (or adapter) rejection into a [`ScannerError`](#scannererror).
|
|
140
|
+
Maps `NotFoundError`/`DevicesNotFoundError` → `NO_DEVICE`, `SecurityError` →
|
|
141
|
+
`INSECURE_CONTEXT`, `NotReadableError`/`TrackStartError` → `DEVICE_BUSY`,
|
|
142
|
+
`NotSupportedError` → `NOT_SUPPORTED`, anything else → `REQUEST_FAILED`. Note:
|
|
143
|
+
permission denial (`NotAllowedError`/`PermissionDeniedError`) is handled
|
|
144
|
+
separately by the scanner (→ `PERMISSION_DENIED` + `state.permission =
|
|
145
|
+
"denied"`) before this classifier applies. Used internally; exported for
|
|
146
|
+
custom adapters that want identical taxonomy.
|
|
147
|
+
|
|
148
|
+
**Parameters:**
|
|
149
|
+
|
|
150
|
+
- `e` (`unknown`) — The thrown value (typically a `DOMException`).
|
|
151
|
+
|
|
152
|
+
**Returns:** `ScannerError` — `{ code, message }`
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### `detectFacing(label)`
|
|
157
|
+
|
|
158
|
+
Best-effort camera facing detection from a device label (`/back|rear|environment/i`
|
|
159
|
+
→ `"environment"`, `/front|user|facetime|selfie/i` → `"user"`).
|
|
160
|
+
|
|
161
|
+
**Parameters:**
|
|
162
|
+
|
|
163
|
+
- `label` (`string`) — Device label as reported by `enumerateDevices`.
|
|
164
|
+
|
|
165
|
+
**Returns:** `CameraFacing | null` — `null` when the label matches neither pattern.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### `toScanResult(d, timestamp?)`
|
|
170
|
+
|
|
171
|
+
Map a raw detection ([`DetectedBarcodeLike`](#detectedbarcodelike)) to the public
|
|
172
|
+
[`ScanResult`](#scanresult) shape. Used internally; useful when implementing a custom
|
|
173
|
+
`Detector`.
|
|
174
|
+
|
|
175
|
+
**Parameters:**
|
|
176
|
+
|
|
177
|
+
- `d` (`DetectedBarcodeLike`) — The raw detection.
|
|
178
|
+
- `timestamp` (`number`, optional) — Detection timestamp. Default `Date.now()`.
|
|
179
|
+
|
|
180
|
+
**Returns:** `ScanResult`
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Scanner Instance
|
|
185
|
+
|
|
186
|
+
Returned by `createScanner()`. After `destroy()`, the lifecycle methods become
|
|
187
|
+
no-ops (`start()` logs a warning and resolves `null`, `setTorch()` resolves `false`);
|
|
188
|
+
`subscribe()`/`get()` keep working on the final state.
|
|
189
|
+
|
|
190
|
+
### `subscribe(cb)`
|
|
191
|
+
|
|
192
|
+
Subscribe to reactive state changes. Callback fires immediately with the current
|
|
193
|
+
state, then on every change. Compatible with Svelte's `$store` contract.
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
|
|
197
|
+
- `cb` (`(state: ScannerState) => void`) — State callback
|
|
198
|
+
|
|
199
|
+
**Returns:** `Unsubscribe` (from `@marianmeres/store`) — idempotent unsubscribe
|
|
200
|
+
function that also implements `Symbol.dispose` (usable with `using`)
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
### `get()`
|
|
205
|
+
|
|
206
|
+
Get the current state snapshot.
|
|
207
|
+
|
|
208
|
+
**Returns:** `ScannerState`
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
### `start()`
|
|
213
|
+
|
|
214
|
+
Acquire the camera and start scanning. Never rejects.
|
|
215
|
+
|
|
216
|
+
- `"single"` mode: resolves with the first [`ScanResult`](#scanresult), or `null` when
|
|
217
|
+
cancelled via `stop()` (or on failure — check `state.error`).
|
|
218
|
+
- `"continuous"` mode: resolves `null` when stopped.
|
|
219
|
+
|
|
220
|
+
Concurrent calls while active return the same in-flight promise. On acquisition
|
|
221
|
+
failure the status returns to `"idle"`; after a successful scan/cancel it is
|
|
222
|
+
`"stopped"`. A permission denial additionally sets `state.permission = "denied"`.
|
|
223
|
+
|
|
224
|
+
**Returns:** `Promise<ScanResult | null>`
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### `stop()`
|
|
229
|
+
|
|
230
|
+
Cancel: stop the detection loop, release the camera (all tracks stopped, video
|
|
231
|
+
`srcObject` detached), resolve a pending `start()` with `null`, set status
|
|
232
|
+
`"stopped"`. No-op when nothing is running.
|
|
233
|
+
|
|
234
|
+
**Returns:** `void`
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### `listCameras()`
|
|
239
|
+
|
|
240
|
+
List available video input devices (also updates `state.cameras`). NOTE: browsers
|
|
241
|
+
return empty labels until camera permission has been granted at least once — the
|
|
242
|
+
scanner re-enumerates automatically after a successful `start()`. Resolves `[]` on
|
|
243
|
+
enumeration failure (never rejects).
|
|
244
|
+
|
|
245
|
+
**Returns:** `Promise<CameraInfo[]>`
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### `setCamera(deviceId)`
|
|
250
|
+
|
|
251
|
+
Switch to another camera. While scanning, performs a live switch (the current stream
|
|
252
|
+
is released first — most mobile browsers cannot open two cameras at once); otherwise
|
|
253
|
+
the device is remembered and applied on the next `start()`. Concurrent calls are
|
|
254
|
+
serialized (the second switch runs after the first completes); switching to the
|
|
255
|
+
already-active camera is a no-op. On a failed live switch the error is classified
|
|
256
|
+
into `state.error` and the scanner stops.
|
|
257
|
+
|
|
258
|
+
**Parameters:**
|
|
259
|
+
|
|
260
|
+
- `deviceId` (`string`) — Device id from [`CameraInfo`](#camerainfo).
|
|
261
|
+
|
|
262
|
+
**Returns:** `Promise<void>`
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### `setTorch(on)`
|
|
267
|
+
|
|
268
|
+
Turn the torch (flashlight) on/off via `MediaStreamTrack.applyConstraints`. Resolves
|
|
269
|
+
`true` on success, `false` when unsupported or failed (never rejects). Support is
|
|
270
|
+
reflected in `state.torch.supported` — probed at stream acquisition and re-probed
|
|
271
|
+
~500 ms later (Android Chrome reports track capabilities only once the camera is
|
|
272
|
+
actually streaming); the torch resets to off on stream release/switch.
|
|
273
|
+
|
|
274
|
+
**Parameters:**
|
|
275
|
+
|
|
276
|
+
- `on` (`boolean`) — Desired torch state.
|
|
277
|
+
|
|
278
|
+
**Returns:** `Promise<boolean>`
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `getVideo()`
|
|
283
|
+
|
|
284
|
+
The video element used for the live preview. Lazily created when none was configured
|
|
285
|
+
(with `playsinline`, `muted`, `autoplay` handled); returns `null` in non-DOM
|
|
286
|
+
environments. The shipped stage UI renders this element.
|
|
287
|
+
|
|
288
|
+
**Returns:** `HTMLVideoElement | null`
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### `destroy()`
|
|
293
|
+
|
|
294
|
+
Stop everything and release all resources/listeners (including the permission-change
|
|
295
|
+
subscription). Idempotent.
|
|
296
|
+
|
|
297
|
+
**Returns:** `void`
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Types
|
|
302
|
+
|
|
303
|
+
### `ScannerConfig`
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
interface ScannerConfig {
|
|
307
|
+
video?: HTMLVideoElement;
|
|
308
|
+
formats?: BarcodeFormat[]; // default ["qr_code"]
|
|
309
|
+
mode?: ScanMode; // default "single"
|
|
310
|
+
onScan?: (result: ScanResult) => void;
|
|
311
|
+
onError?: (error: ScannerError) => void;
|
|
312
|
+
preferredCamera?: CameraFacing | (string & Record<never, never>); // default "environment"
|
|
313
|
+
scanIntervalMs?: number; // default 100 (~10 fps)
|
|
314
|
+
dedupeMs?: number; // default 1500
|
|
315
|
+
adapter?: CameraAdapter;
|
|
316
|
+
detector?: Detector;
|
|
317
|
+
wasmOverrides?: Record<string, unknown>;
|
|
318
|
+
logger?: LoggerLike;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
| Field | Description |
|
|
323
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
324
|
+
| `video` | Video element to play the camera stream in. Created lazily when omitted (requires DOM). |
|
|
325
|
+
| `formats` | Formats to detect. Fewer formats = faster decode, fewer false positives. |
|
|
326
|
+
| `mode` | `"single"` — auto-stop after first detection; `"continuous"` — keep scanning, dedupe within `dedupeMs`. |
|
|
327
|
+
| `onScan` | Called for every successful detection. |
|
|
328
|
+
| `onError` | Called whenever an error is set on the state. |
|
|
329
|
+
| `preferredCamera` | `"environment"` / `"user"` facing hint (via `facingMode: { ideal }`), or a concrete `deviceId` (via `deviceId: { exact }`). |
|
|
330
|
+
| `scanIntervalMs` | Minimal interval between decode attempts in ms (clamped to `>= 0`). |
|
|
331
|
+
| `dedupeMs` | Continuous mode: suppress repeated detections of the identical value within this window (clamped to `>= 0`). |
|
|
332
|
+
| `adapter` | Camera acquisition seam. Default wraps `navigator.mediaDevices`. |
|
|
333
|
+
| `detector` | Decoding engine seam. Default wraps the `barcode-detector` ponyfill (created lazily on first `start()`; when set, `formats`/`wasmOverrides` are ignored). |
|
|
334
|
+
| `wasmOverrides` | Passed to zxing-wasm's `prepareZXingModule` — most notably `locateFile` for self-hosting the `.wasm` (CSP-strict or offline apps). |
|
|
335
|
+
| `logger` | Custom logger (`@marianmeres/clog` compatible). Default `createClog("scanner")`. |
|
|
336
|
+
|
|
337
|
+
### `Scanner`
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
interface Scanner {
|
|
341
|
+
subscribe(cb: (state: ScannerState) => void): () => void;
|
|
342
|
+
get(): ScannerState;
|
|
343
|
+
start(): Promise<ScanResult | null>;
|
|
344
|
+
stop(): void;
|
|
345
|
+
listCameras(): Promise<CameraInfo[]>;
|
|
346
|
+
setCamera(deviceId: string): Promise<void>;
|
|
347
|
+
setTorch(on: boolean): Promise<boolean>;
|
|
348
|
+
getVideo(): HTMLVideoElement | null;
|
|
349
|
+
destroy(): void;
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### `ScannerState`
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
interface ScannerState {
|
|
357
|
+
status: ScannerStatus;
|
|
358
|
+
error: ScannerError | null;
|
|
359
|
+
permission: CameraPermissionStatus;
|
|
360
|
+
torch: { supported: boolean; on: boolean };
|
|
361
|
+
cameras: CameraInfo[];
|
|
362
|
+
activeCameraId: string | null;
|
|
363
|
+
lastResult: ScanResult | null;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
| Field | Description |
|
|
368
|
+
| ---------------- | -------------------------------------------------------------------------------------------------------- |
|
|
369
|
+
| `status` | `"idle"` → `"initializing"` → `"scanning"` → `"stopped"` (back to `"idle"` on failed start) |
|
|
370
|
+
| `error` | Last error, or `null`. Methods never throw — errors land here. Cleared on each `start()`. |
|
|
371
|
+
| `permission` | Camera permission, best effort (Permissions API where available, otherwise from `getUserMedia` outcomes) |
|
|
372
|
+
| `torch` | Torch capability + current state of the active track |
|
|
373
|
+
| `cameras` | Populated on `listCameras()` and after successful `start()` (labels require permission) |
|
|
374
|
+
| `activeCameraId` | Device id of the currently active camera, or `null` |
|
|
375
|
+
| `lastResult` | Most recent successful detection, or `null`. Cleared on each `start()`. |
|
|
376
|
+
|
|
377
|
+
### `ScanResult`
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
interface ScanResult {
|
|
381
|
+
value: string; // decoded payload
|
|
382
|
+
format: BarcodeFormat; // e.g. "qr_code"
|
|
383
|
+
cornerPoints: Point2D[]; // corners in source coordinates
|
|
384
|
+
boundingBox: DOMRectReadOnly; // bounding box in source coordinates
|
|
385
|
+
timestamp: number; // Date.now()
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### `ScannerError`
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
interface ScannerError {
|
|
393
|
+
code: ScannerErrorCode;
|
|
394
|
+
message: string;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### `ScannerStatus`
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
type ScannerStatus = "idle" | "initializing" | "scanning" | "stopped";
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### `ScanMode`
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
type ScanMode = "single" | "continuous";
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### `CameraPermissionStatus`
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
type CameraPermissionStatus = "unknown" | "prompt" | "granted" | "denied";
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### `CameraFacing`
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
type CameraFacing = "user" | "environment";
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### `CameraInfo`
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
interface CameraInfo {
|
|
426
|
+
deviceId: string; // usable with scanner.setCamera()
|
|
427
|
+
label: string; // empty until permission has been granted at least once
|
|
428
|
+
facing: CameraFacing | null; // best-effort, derived from the label
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### `BarcodeFormat`
|
|
433
|
+
|
|
434
|
+
Formats supported by the underlying zxing-cpp engine. The special values
|
|
435
|
+
`"linear_codes"`, `"matrix_codes"` and `"any"` are engine-level shortcuts expanding to
|
|
436
|
+
the respective format groups. The type is open (`string`-compatible) for
|
|
437
|
+
forward-compat while keeping autocomplete on the known values.
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
type BarcodeFormat =
|
|
441
|
+
| "aztec"
|
|
442
|
+
| "codabar"
|
|
443
|
+
| "code_39"
|
|
444
|
+
| "code_93"
|
|
445
|
+
| "code_128"
|
|
446
|
+
| "data_matrix"
|
|
447
|
+
| "databar"
|
|
448
|
+
| "databar_expanded"
|
|
449
|
+
| "databar_limited"
|
|
450
|
+
| "dx_film_edge"
|
|
451
|
+
| "ean_8"
|
|
452
|
+
| "ean_13"
|
|
453
|
+
| "itf"
|
|
454
|
+
| "maxi_code"
|
|
455
|
+
| "micro_qr_code"
|
|
456
|
+
| "pdf417"
|
|
457
|
+
| "qr_code"
|
|
458
|
+
| "rm_qr_code"
|
|
459
|
+
| "upc_a"
|
|
460
|
+
| "upc_e"
|
|
461
|
+
| "linear_codes"
|
|
462
|
+
| "matrix_codes"
|
|
463
|
+
| "any"
|
|
464
|
+
| (string & Record<never, never>);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### `Point2D`
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
interface Point2D {
|
|
471
|
+
x: number;
|
|
472
|
+
y: number;
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### `Detector`
|
|
477
|
+
|
|
478
|
+
Abstraction over the barcode decoding engine — shaped like the W3C `BarcodeDetector`.
|
|
479
|
+
Injectable via `ScannerConfig.detector` for testing or engine swaps.
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
interface Detector {
|
|
483
|
+
detect(source: DetectorSource): Promise<DetectedBarcodeLike[]>;
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### `DetectorSource`
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
type DetectorSource =
|
|
491
|
+
| HTMLVideoElement
|
|
492
|
+
| HTMLImageElement
|
|
493
|
+
| HTMLCanvasElement
|
|
494
|
+
| ImageBitmap
|
|
495
|
+
| ImageData
|
|
496
|
+
| Blob;
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### `DetectedBarcodeLike`
|
|
500
|
+
|
|
501
|
+
The raw detection shape produced by a `Detector` — structurally compatible with the
|
|
502
|
+
W3C `DetectedBarcode`.
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
interface DetectedBarcodeLike {
|
|
506
|
+
rawValue: string;
|
|
507
|
+
format: string;
|
|
508
|
+
cornerPoints: Point2D[];
|
|
509
|
+
boundingBox: DOMRectReadOnly;
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### `CameraAdapter`
|
|
514
|
+
|
|
515
|
+
Abstraction over camera acquisition. Injectable via `ScannerConfig.adapter` for
|
|
516
|
+
testing or replacement. The default implementation
|
|
517
|
+
([`createDefaultCameraAdapter`](#createdefaultcameraadapteroptions)) wraps
|
|
518
|
+
`navigator.mediaDevices` for streams/enumeration and delegates the permission
|
|
519
|
+
lifecycle to `@marianmeres/mediaperms`.
|
|
520
|
+
|
|
521
|
+
NOTE (difference vs mediaperms): the scanner RETAINS the acquired stream for the live
|
|
522
|
+
preview — the adapter must return a live `MediaStream`, the scanner is responsible for
|
|
523
|
+
stopping its tracks.
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
interface CameraAdapter {
|
|
527
|
+
getStream(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
|
528
|
+
enumerateVideoDevices(): Promise<CameraInfo[]>;
|
|
529
|
+
queryPermission(): Promise<CameraPermissionStatus | null>;
|
|
530
|
+
onPermissionChange(
|
|
531
|
+
cb: (status: CameraPermissionStatus) => void,
|
|
532
|
+
): (() => void) | null;
|
|
533
|
+
destroy?(): void;
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
| Method | Description |
|
|
538
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
539
|
+
| `getStream(constraints)` | Acquire a camera stream. May prompt. Rejections are classified by the scanner. |
|
|
540
|
+
| `enumerateVideoDevices()` | List available video input devices. |
|
|
541
|
+
| `queryPermission()` | Query camera permission without prompting. Return `null` when undeterminable. |
|
|
542
|
+
| `onPermissionChange(cb)` | Subscribe to permission changes. Return unsubscribe fn, or `null` when not supported. |
|
|
543
|
+
| `destroy()` (optional) | Release adapter-owned resources. Called by `Scanner.destroy()` ONLY for the adapter the scanner created itself. |
|
|
544
|
+
|
|
545
|
+
### `LoggerLike`
|
|
546
|
+
|
|
547
|
+
Logger shape (`@marianmeres/clog` compatible; `console` also satisfies it).
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
interface LoggerLike {
|
|
551
|
+
(...args: any[]): any;
|
|
552
|
+
debug: (...args: any[]) => any;
|
|
553
|
+
warn: (...args: any[]) => any;
|
|
554
|
+
error: (...args: any[]) => any;
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### `CreateDefaultDetectorOptions`
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
interface CreateDefaultDetectorOptions {
|
|
562
|
+
formats?: BarcodeFormat[]; // default ["qr_code"]
|
|
563
|
+
wasmOverrides?: Record<string, unknown>;
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### `ScanImageOptions`
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
interface ScanImageOptions {
|
|
571
|
+
formats?: BarcodeFormat[]; // default ["qr_code"]
|
|
572
|
+
detector?: Detector;
|
|
573
|
+
wasmOverrides?: Record<string, unknown>;
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Constants
|
|
580
|
+
|
|
581
|
+
### `ScannerErrorCode`
|
|
582
|
+
|
|
583
|
+
Machine-readable error codes attached to `ScannerState.error.code`.
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
const ScannerErrorCode = {
|
|
587
|
+
NoDevice: "NO_DEVICE",
|
|
588
|
+
InsecureContext: "INSECURE_CONTEXT",
|
|
589
|
+
DeviceBusy: "DEVICE_BUSY",
|
|
590
|
+
PermissionDenied: "PERMISSION_DENIED",
|
|
591
|
+
RequestFailed: "REQUEST_FAILED",
|
|
592
|
+
NotSupported: "NOT_SUPPORTED",
|
|
593
|
+
DetectorFailed: "DETECTOR_FAILED",
|
|
594
|
+
} as const;
|
|
595
|
+
|
|
596
|
+
type ScannerErrorCode = typeof ScannerErrorCode[keyof typeof ScannerErrorCode];
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
| Code | Cause |
|
|
600
|
+
| ------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
601
|
+
| `NO_DEVICE` | `getUserMedia` threw `NotFoundError` / `DevicesNotFoundError` — no camera available |
|
|
602
|
+
| `INSECURE_CONTEXT` | `getUserMedia` threw `SecurityError` — origin not secure (camera requires HTTPS) or Permissions-Policy blocks it |
|
|
603
|
+
| `DEVICE_BUSY` | `getUserMedia` threw `NotReadableError` / `TrackStartError` — hardware held by another consumer |
|
|
604
|
+
| `PERMISSION_DENIED` | Camera access denied by the user or platform policy (`NotAllowedError`) — `state.permission` is set to `"denied"` too |
|
|
605
|
+
| `REQUEST_FAILED` | Camera acquisition failed with a non-classified error, or the live camera track ended unexpectedly |
|
|
606
|
+
| `NOT_SUPPORTED` | Required platform API is missing (no `mediaDevices`, no DOM for the video element, …) |
|
|
607
|
+
| `DETECTOR_FAILED` | The barcode detector threw repeatedly (5 consecutive failures stop the scan) or failed to construct |
|
|
608
|
+
|
|
609
|
+
### `DEFAULT_FORMATS`
|
|
610
|
+
|
|
611
|
+
`["qr_code"]` — Default formats (QR only — the fastest and the primary use case).
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## Stage (`@marianmeres/scanner/stage`)
|
|
616
|
+
|
|
617
|
+
### `createScannerStage(scanner, options)`
|
|
618
|
+
|
|
619
|
+
Mount the scanning stage UI into `options.container`: the scanner's cover-fit
|
|
620
|
+
`<video>` preview, a dimmed overlay with a rounded viewfinder cutout + corner guides,
|
|
621
|
+
an optional animated scan line (`prefers-reduced-motion` honored), a brief success
|
|
622
|
+
flash on detection, and opt-in control buttons. One scoped `<style>` element is
|
|
623
|
+
injected into `document.head` (once, id `mms-styles`).
|
|
624
|
+
|
|
625
|
+
The stage is a pure consumer of the headless `Scanner` — destroying the stage does
|
|
626
|
+
NOT stop or destroy the scanner (and vice versa).
|
|
627
|
+
|
|
628
|
+
Unlike the scanner methods, this factory **throws**: when called without a DOM
|
|
629
|
+
(`document` undefined), without `options.container`, or when the scanner cannot
|
|
630
|
+
provide a video element.
|
|
631
|
+
|
|
632
|
+
**Parameters:**
|
|
633
|
+
|
|
634
|
+
- `scanner` (`Scanner`) — The headless scanner instance (its `getVideo()` element is
|
|
635
|
+
rendered).
|
|
636
|
+
- `options` (`ScannerStageOptions`)
|
|
637
|
+
|
|
638
|
+
| Field | Type | Description |
|
|
639
|
+
| ----------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
640
|
+
| `container` | `HTMLElement` — **required** | Element to mount the stage into (its `position` is set to `relative` when unset). |
|
|
641
|
+
| `controls` | `{ cancel?: boolean; torch?: boolean; cameraSwitch?: boolean }` | Which built-in buttons to render. Default `{ cancel: true }`. Buttons for unsupported features hide automatically (torch without capability, camera switch with a single camera). |
|
|
642
|
+
| `scanLine` | `boolean` — default `true` | Render the animated scan line inside the viewfinder. |
|
|
643
|
+
| `theme` | `"auto" \| "light" \| "dark"` — default `"auto"` | `"auto"` follows `prefers-color-scheme` (live). |
|
|
644
|
+
| `accent` | `string` — optional | Accent color (any CSS color value) — sets `--mms-accent`. |
|
|
645
|
+
| `onCancel` | `() => void` — optional | Called after the built-in cancel button stopped the scanner. |
|
|
646
|
+
|
|
647
|
+
**Returns:** `ScannerStage`
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
interface ScannerStage {
|
|
651
|
+
el: HTMLElement; // the stage root (already mounted into the container)
|
|
652
|
+
destroy(): void; // unmount + release stage resources; does NOT touch the scanner
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
**Example:**
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
import { createScanner } from "@marianmeres/scanner";
|
|
660
|
+
import { createScannerStage } from "@marianmeres/scanner/stage";
|
|
661
|
+
|
|
662
|
+
const scanner = createScanner();
|
|
663
|
+
const stage = createScannerStage(scanner, {
|
|
664
|
+
container: document.getElementById("scan")!,
|
|
665
|
+
controls: { cancel: true, torch: true, cameraSwitch: true },
|
|
666
|
+
accent: "#e11d48",
|
|
667
|
+
onCancel: () => console.log("cancelled"),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const result = await scanner.start();
|
|
671
|
+
stage.destroy();
|
|
672
|
+
scanner.destroy();
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### `ScannerStageTheme`
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
type ScannerStageTheme = "auto" | "light" | "dark";
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Stage theming (CSS custom properties)
|
|
682
|
+
|
|
683
|
+
Scoped to the `.mms-stage` root — override on the container or via a stylesheet:
|
|
684
|
+
|
|
685
|
+
| Variable | Default | Purpose |
|
|
686
|
+
| -------------------- | -------------------------- | --------------------------------- |
|
|
687
|
+
| `--mms-accent` | `#3b82f6` | Scan line + active torch button |
|
|
688
|
+
| `--mms-success` | `#22c55e` | Detection success flash |
|
|
689
|
+
| `--mms-dim` | `rgba(0, 0, 0, 0.55)` | Overlay dim around the viewfinder |
|
|
690
|
+
| `--mms-corner-color` | `rgba(255, 255, 255, 0.9)` | Corner guide color |
|
|
691
|
+
| `--mms-corner-size` | `26px` | Corner guide length |
|
|
692
|
+
| `--mms-corner-width` | `4px` | Corner guide stroke width |
|
|
693
|
+
| `--mms-frame-size` | `min(65%, 65vmin)` | Viewfinder size |
|
|
694
|
+
| `--mms-radius` | `0` | Stage border radius |
|
|
695
|
+
| `--mms-btn-bg` | theme-dependent | Control button background |
|
|
696
|
+
| `--mms-btn-fg` | theme-dependent | Control button foreground |
|
package/CLAUDE.md
ADDED