@agicash/qr-scanner 0.1.0 → 0.1.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/README.md +10 -0
- package/dist/index.cjs +126 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +126 -89
- package/dist/index.js.map +1 -1
- package/dist/worker.js +6 -4
- package/dist/worker.js.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -94,6 +94,16 @@ QrScanner.scanImage(source, options?): Promise<ScanResult>
|
|
|
94
94
|
QrScanner.preload(): Promise<void>
|
|
95
95
|
QrScanner.configureWasm(overrides): void
|
|
96
96
|
QrScanner.setWorkerUrl(url): void
|
|
97
|
+
QrScanner.setDebug(enabled): void
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Debug Logging
|
|
101
|
+
|
|
102
|
+
The library includes performance profiling logs (camera acquisition timing, best-camera selection, etc.) that are **off by default**. Enable them at runtime for debugging:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
QrScanner.setDebug(true); // logs appear in console
|
|
106
|
+
QrScanner.setDebug(false); // back to silent (default)
|
|
97
107
|
```
|
|
98
108
|
|
|
99
109
|
## Worker Loading
|
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,15 @@ __export(index_exports, {
|
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
|
|
29
|
+
// src/debug.ts
|
|
30
|
+
var enabled = false;
|
|
31
|
+
function setDebug(on) {
|
|
32
|
+
enabled = on;
|
|
33
|
+
}
|
|
34
|
+
function debug(...args) {
|
|
35
|
+
if (enabled) console.debug(...args);
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
// src/camera.ts
|
|
30
39
|
var CameraPermissionError = class extends Error {
|
|
31
40
|
constructor(message = "Camera access denied. Please grant camera permission and try again.") {
|
|
@@ -53,6 +62,12 @@ function setCachedDeviceId(facingMode, deviceId) {
|
|
|
53
62
|
} catch {
|
|
54
63
|
}
|
|
55
64
|
}
|
|
65
|
+
function clearCachedDeviceId(facingMode) {
|
|
66
|
+
try {
|
|
67
|
+
localStorage.removeItem(`${CACHE_KEY_PREFIX}${facingMode}`);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
56
71
|
var CameraManager = class {
|
|
57
72
|
constructor(config = {}) {
|
|
58
73
|
this.stream = null;
|
|
@@ -66,16 +81,16 @@ var CameraManager = class {
|
|
|
66
81
|
const t0 = performance.now();
|
|
67
82
|
this.stream = await this.acquireStream();
|
|
68
83
|
const t1 = performance.now();
|
|
69
|
-
|
|
84
|
+
debug(`[QrScanner] acquireStream: ${(t1 - t0).toFixed(0)}ms`);
|
|
70
85
|
await this.ensureBestCamera();
|
|
71
86
|
const t2 = performance.now();
|
|
72
|
-
|
|
87
|
+
debug(`[QrScanner] ensureBestCamera: ${(t2 - t1).toFixed(0)}ms`);
|
|
73
88
|
video.srcObject = this.stream;
|
|
74
89
|
video.setAttribute("playsinline", "true");
|
|
75
90
|
await video.play();
|
|
76
91
|
const t3 = performance.now();
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
debug(`[QrScanner] video.play: ${(t3 - t2).toFixed(0)}ms`);
|
|
93
|
+
debug(`[QrScanner] camera.start total: ${(t3 - t0).toFixed(0)}ms`);
|
|
79
94
|
if (this.facingMode === "environment" || this.facingMode === "user") {
|
|
80
95
|
const finalDeviceId = this.getVideoTrack()?.getSettings().deviceId;
|
|
81
96
|
if (finalDeviceId) {
|
|
@@ -150,9 +165,7 @@ var CameraManager = class {
|
|
|
150
165
|
*/
|
|
151
166
|
async ensureBestCamera() {
|
|
152
167
|
if (this.facingMode !== "environment" && this.facingMode !== "user") {
|
|
153
|
-
|
|
154
|
-
"[QrScanner] ensureBestCamera: skipped (specific deviceId)"
|
|
155
|
-
);
|
|
168
|
+
debug("[QrScanner] ensureBestCamera: skipped (specific deviceId)");
|
|
156
169
|
return;
|
|
157
170
|
}
|
|
158
171
|
const track = this.getVideoTrack();
|
|
@@ -160,29 +173,35 @@ var CameraManager = class {
|
|
|
160
173
|
try {
|
|
161
174
|
const capabilities = track.getCapabilities();
|
|
162
175
|
if (!capabilities.focusMode || capabilities.focusMode.includes("continuous")) {
|
|
163
|
-
|
|
176
|
+
debug(
|
|
164
177
|
`[QrScanner] ensureBestCamera: skipped (focusMode: ${JSON.stringify(capabilities.focusMode)})`
|
|
165
178
|
);
|
|
166
179
|
return;
|
|
167
180
|
}
|
|
168
|
-
|
|
181
|
+
debug(
|
|
169
182
|
`[QrScanner] ensureBestCamera: current camera lacks autofocus (focusMode: ${JSON.stringify(capabilities.focusMode)})`
|
|
170
183
|
);
|
|
171
|
-
} catch {
|
|
184
|
+
} catch (err) {
|
|
185
|
+
debug(
|
|
186
|
+
`[QrScanner] ensureBestCamera: skipped (getCapabilities failed: ${err instanceof Error ? err.message : err})`
|
|
187
|
+
);
|
|
172
188
|
return;
|
|
173
189
|
}
|
|
174
190
|
const currentDeviceId = track.getSettings().deviceId;
|
|
175
191
|
let devices;
|
|
176
192
|
try {
|
|
177
193
|
devices = await navigator.mediaDevices.enumerateDevices();
|
|
178
|
-
} catch {
|
|
194
|
+
} catch (err) {
|
|
195
|
+
debug(
|
|
196
|
+
`[QrScanner] ensureBestCamera: skipped (enumerateDevices failed: ${err instanceof Error ? err.message : err})`
|
|
197
|
+
);
|
|
179
198
|
return;
|
|
180
199
|
}
|
|
181
200
|
if (!Array.isArray(devices)) return;
|
|
182
201
|
const candidates = devices.filter(
|
|
183
202
|
(d) => d.kind === "videoinput" && d.deviceId !== currentDeviceId
|
|
184
203
|
);
|
|
185
|
-
|
|
204
|
+
debug(
|
|
186
205
|
`[QrScanner] ensureBestCamera: testing ${candidates.length} candidate camera(s)`
|
|
187
206
|
);
|
|
188
207
|
if (candidates.length === 0) return;
|
|
@@ -199,13 +218,16 @@ var CameraManager = class {
|
|
|
199
218
|
},
|
|
200
219
|
audio: false
|
|
201
220
|
});
|
|
202
|
-
|
|
221
|
+
debug(
|
|
203
222
|
`[QrScanner] ensureBestCamera: candidate ${candidate.label || candidate.deviceId.slice(0, 8)}: getUserMedia ${(performance.now() - t).toFixed(0)}ms`
|
|
204
223
|
);
|
|
205
|
-
} catch {
|
|
206
|
-
|
|
207
|
-
`[QrScanner] ensureBestCamera: candidate ${candidate.label || candidate.deviceId.slice(0, 8)}: getUserMedia failed ${(performance.now() - t).toFixed(0)}ms`
|
|
224
|
+
} catch (err) {
|
|
225
|
+
debug(
|
|
226
|
+
`[QrScanner] ensureBestCamera: candidate ${candidate.label || candidate.deviceId.slice(0, 8)}: getUserMedia failed ${(performance.now() - t).toFixed(0)}ms \u2014 ${err instanceof Error ? err.name : err}`
|
|
208
227
|
);
|
|
228
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
229
|
+
throw new CameraPermissionError();
|
|
230
|
+
}
|
|
209
231
|
continue;
|
|
210
232
|
}
|
|
211
233
|
const candidateTrack = candidateStream.getVideoTracks()[0];
|
|
@@ -224,27 +246,52 @@ var CameraManager = class {
|
|
|
224
246
|
this.stream = candidateStream;
|
|
225
247
|
return;
|
|
226
248
|
}
|
|
227
|
-
} catch {
|
|
249
|
+
} catch (err) {
|
|
250
|
+
debug(
|
|
251
|
+
`[QrScanner] ensureBestCamera: candidate getCapabilities failed \u2014 ${err instanceof Error ? err.message : err}`
|
|
252
|
+
);
|
|
228
253
|
}
|
|
229
254
|
for (const t2 of candidateStream.getTracks()) t2.stop();
|
|
230
255
|
}
|
|
231
|
-
|
|
232
|
-
|
|
256
|
+
const recoveryAttempts = [
|
|
257
|
+
// Original deviceId + resolution
|
|
258
|
+
{
|
|
233
259
|
video: {
|
|
234
260
|
deviceId: currentDeviceId ? { exact: currentDeviceId } : void 0,
|
|
235
261
|
width: this.resolution?.width ?? { ideal: 1920 },
|
|
236
262
|
height: this.resolution?.height ?? { ideal: 1080 }
|
|
237
263
|
},
|
|
238
264
|
audio: false
|
|
239
|
-
}
|
|
240
|
-
|
|
265
|
+
},
|
|
266
|
+
// facingMode + resolution
|
|
267
|
+
this.buildConstraints(),
|
|
268
|
+
// facingMode only
|
|
269
|
+
this.buildConstraints(false),
|
|
270
|
+
// Bare minimum — should always succeed if a camera exists
|
|
271
|
+
{ video: true, audio: false }
|
|
272
|
+
];
|
|
273
|
+
let lastRecoveryError;
|
|
274
|
+
for (const constraints of recoveryAttempts) {
|
|
241
275
|
try {
|
|
242
|
-
this.stream = await navigator.mediaDevices.getUserMedia(
|
|
243
|
-
|
|
276
|
+
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
277
|
+
return;
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
280
|
+
throw new CameraPermissionError();
|
|
281
|
+
}
|
|
282
|
+
if (err instanceof DOMException && err.name === "NotFoundError") {
|
|
283
|
+
throw new CameraNotFoundError();
|
|
284
|
+
}
|
|
285
|
+
lastRecoveryError = err;
|
|
286
|
+
debug(
|
|
287
|
+
`[QrScanner] ensureBestCamera recovery failed: ${err instanceof Error ? err.name : err}`
|
|
244
288
|
);
|
|
245
|
-
|
|
289
|
+
continue;
|
|
246
290
|
}
|
|
247
291
|
}
|
|
292
|
+
debug(
|
|
293
|
+
`[QrScanner] ensureBestCamera: all recovery attempts failed, last error: ${lastRecoveryError instanceof Error ? lastRecoveryError.message : lastRecoveryError}`
|
|
294
|
+
);
|
|
248
295
|
}
|
|
249
296
|
getVideoTrack() {
|
|
250
297
|
if (!this.stream) return null;
|
|
@@ -259,12 +306,10 @@ var CameraManager = class {
|
|
|
259
306
|
* fewer constraints lets us still open the camera on those browsers.
|
|
260
307
|
*/
|
|
261
308
|
async acquireStream() {
|
|
262
|
-
const labels = [];
|
|
263
309
|
const attempts = [];
|
|
264
310
|
if (this.facingMode === "environment" || this.facingMode === "user") {
|
|
265
311
|
const cachedId = getCachedDeviceId(this.facingMode);
|
|
266
312
|
if (cachedId) {
|
|
267
|
-
labels.push("cached deviceId");
|
|
268
313
|
const video = {
|
|
269
314
|
deviceId: { exact: cachedId }
|
|
270
315
|
};
|
|
@@ -272,42 +317,50 @@ var CameraManager = class {
|
|
|
272
317
|
else video.width = { ideal: 1920 };
|
|
273
318
|
if (this.resolution?.height) video.height = this.resolution.height;
|
|
274
319
|
else video.height = { ideal: 1080 };
|
|
275
|
-
attempts.push({
|
|
320
|
+
attempts.push({
|
|
321
|
+
label: "cached deviceId",
|
|
322
|
+
constraints: { video, audio: false },
|
|
323
|
+
isCachedDeviceId: true
|
|
324
|
+
});
|
|
276
325
|
}
|
|
277
326
|
}
|
|
278
|
-
labels.push("full constraints", "no resolution", "bare minimum");
|
|
279
327
|
attempts.push(
|
|
280
|
-
|
|
281
|
-
this.buildConstraints(),
|
|
282
|
-
|
|
283
|
-
this.buildConstraints(false),
|
|
284
|
-
// Bare minimum
|
|
285
|
-
{ video: true, audio: false }
|
|
328
|
+
{ label: "full constraints", constraints: this.buildConstraints() },
|
|
329
|
+
{ label: "no resolution", constraints: this.buildConstraints(false) },
|
|
330
|
+
{ label: "bare minimum", constraints: { video: true, audio: false } }
|
|
286
331
|
);
|
|
287
332
|
let lastError;
|
|
288
|
-
for (
|
|
333
|
+
for (const attempt of attempts) {
|
|
289
334
|
const t = performance.now();
|
|
290
335
|
try {
|
|
291
|
-
const stream = await navigator.mediaDevices.getUserMedia(
|
|
292
|
-
|
|
293
|
-
|
|
336
|
+
const stream = await navigator.mediaDevices.getUserMedia(
|
|
337
|
+
attempt.constraints
|
|
338
|
+
);
|
|
339
|
+
debug(
|
|
340
|
+
`[QrScanner] getUserMedia(${attempt.label}): ${(performance.now() - t).toFixed(0)}ms \u2713`
|
|
294
341
|
);
|
|
295
342
|
return stream;
|
|
296
343
|
} catch (err) {
|
|
297
|
-
|
|
298
|
-
`[QrScanner] getUserMedia(${
|
|
344
|
+
debug(
|
|
345
|
+
`[QrScanner] getUserMedia(${attempt.label}): ${(performance.now() - t).toFixed(0)}ms \u2717 ${err instanceof Error ? err.name : err}`
|
|
299
346
|
);
|
|
300
|
-
if (err instanceof DOMException) {
|
|
301
|
-
|
|
302
|
-
throw new CameraPermissionError();
|
|
303
|
-
}
|
|
304
|
-
if (err.name === "NotFoundError") {
|
|
305
|
-
throw new CameraNotFoundError();
|
|
306
|
-
}
|
|
307
|
-
lastError = err;
|
|
308
|
-
continue;
|
|
347
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
348
|
+
throw new CameraPermissionError();
|
|
309
349
|
}
|
|
310
|
-
|
|
350
|
+
if (err instanceof DOMException && err.name === "NotFoundError") {
|
|
351
|
+
throw new CameraNotFoundError();
|
|
352
|
+
}
|
|
353
|
+
if (!(err instanceof DOMException) && !(err instanceof OverconstrainedError)) {
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
if (attempt.isCachedDeviceId) {
|
|
357
|
+
clearCachedDeviceId(this.facingMode);
|
|
358
|
+
debug(
|
|
359
|
+
`[QrScanner] cleared stale cached deviceId for "${this.facingMode}"`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
lastError = err;
|
|
363
|
+
continue;
|
|
311
364
|
}
|
|
312
365
|
}
|
|
313
366
|
throw lastError;
|
|
@@ -417,9 +470,6 @@ var FrameExtractor = class {
|
|
|
417
470
|
markWorkerIdle() {
|
|
418
471
|
this.workerBusy = false;
|
|
419
472
|
}
|
|
420
|
-
markWorkerBusy() {
|
|
421
|
-
this.workerBusy = true;
|
|
422
|
-
}
|
|
423
473
|
};
|
|
424
474
|
|
|
425
475
|
// src/overlay.ts
|
|
@@ -429,22 +479,9 @@ function getRenderedVideoRect(video) {
|
|
|
429
479
|
const videoWidth = video.videoWidth || 1;
|
|
430
480
|
const videoHeight = video.videoHeight || 1;
|
|
431
481
|
const objectFit = getComputedStyle(video).objectFit;
|
|
432
|
-
if (objectFit === "cover") {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
elementHeight / videoHeight
|
|
436
|
-
);
|
|
437
|
-
const renderedWidth = videoWidth * scale;
|
|
438
|
-
const renderedHeight = videoHeight * scale;
|
|
439
|
-
return {
|
|
440
|
-
offsetX: (elementWidth - renderedWidth) / 2,
|
|
441
|
-
offsetY: (elementHeight - renderedHeight) / 2,
|
|
442
|
-
width: renderedWidth,
|
|
443
|
-
height: renderedHeight
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
if (objectFit === "contain") {
|
|
447
|
-
const scale = Math.min(
|
|
482
|
+
if (objectFit === "cover" || objectFit === "contain") {
|
|
483
|
+
const scaleFn = objectFit === "cover" ? Math.max : Math.min;
|
|
484
|
+
const scale = scaleFn(
|
|
448
485
|
elementWidth / videoWidth,
|
|
449
486
|
elementHeight / videoHeight
|
|
450
487
|
);
|
|
@@ -683,7 +720,7 @@ var Scanner = class {
|
|
|
683
720
|
}
|
|
684
721
|
}
|
|
685
722
|
await this.camera.start(this.video);
|
|
686
|
-
|
|
723
|
+
debug(
|
|
687
724
|
`[QrScanner] start: camera ready ${(performance.now() - t0).toFixed(0)}ms`
|
|
688
725
|
);
|
|
689
726
|
if (this.overlay) {
|
|
@@ -692,7 +729,7 @@ var Scanner = class {
|
|
|
692
729
|
if (!this.worker) {
|
|
693
730
|
const tw = performance.now();
|
|
694
731
|
this.worker = this.createWorker();
|
|
695
|
-
|
|
732
|
+
debug(
|
|
696
733
|
`[QrScanner] start: worker created ${(performance.now() - tw).toFixed(0)}ms`
|
|
697
734
|
);
|
|
698
735
|
}
|
|
@@ -707,9 +744,7 @@ var Scanner = class {
|
|
|
707
744
|
});
|
|
708
745
|
this.active = true;
|
|
709
746
|
this.paused = false;
|
|
710
|
-
|
|
711
|
-
`[QrScanner] start: total ${(performance.now() - t0).toFixed(0)}ms`
|
|
712
|
-
);
|
|
747
|
+
debug(`[QrScanner] start: total ${(performance.now() - t0).toFixed(0)}ms`);
|
|
713
748
|
}
|
|
714
749
|
stop() {
|
|
715
750
|
this.frameExtractor?.stop();
|
|
@@ -768,18 +803,9 @@ var Scanner = class {
|
|
|
768
803
|
}
|
|
769
804
|
setInversionMode(mode) {
|
|
770
805
|
if (!this.worker) return;
|
|
771
|
-
const options = {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
options.tryInvert = false;
|
|
775
|
-
break;
|
|
776
|
-
case "invert":
|
|
777
|
-
options.tryInvert = true;
|
|
778
|
-
break;
|
|
779
|
-
case "both":
|
|
780
|
-
options.tryInvert = true;
|
|
781
|
-
break;
|
|
782
|
-
}
|
|
806
|
+
const options = {
|
|
807
|
+
tryInvert: mode !== "original"
|
|
808
|
+
};
|
|
783
809
|
const msg = { type: "configure", options };
|
|
784
810
|
this.worker.postMessage(msg);
|
|
785
811
|
}
|
|
@@ -912,8 +938,8 @@ async function createImageBitmapFromBlob(blob) {
|
|
|
912
938
|
}
|
|
913
939
|
}
|
|
914
940
|
|
|
915
|
-
// src/
|
|
916
|
-
var
|
|
941
|
+
// src/decoder-utils.ts
|
|
942
|
+
var DEFAULT_READER_OPTIONS = {
|
|
917
943
|
formats: ["QRCode"],
|
|
918
944
|
tryHarder: true,
|
|
919
945
|
tryInvert: true,
|
|
@@ -930,12 +956,14 @@ function mapPosition(position) {
|
|
|
930
956
|
position.bottomLeft
|
|
931
957
|
];
|
|
932
958
|
}
|
|
959
|
+
|
|
960
|
+
// src/scan-image.ts
|
|
933
961
|
function isDirectInput(source) {
|
|
934
962
|
return source instanceof Blob || source instanceof ArrayBuffer || source instanceof Uint8Array || typeof ImageData !== "undefined" && source instanceof ImageData;
|
|
935
963
|
}
|
|
936
964
|
async function scanImage(source, options) {
|
|
937
965
|
const readerOptions = {
|
|
938
|
-
...
|
|
966
|
+
...DEFAULT_READER_OPTIONS,
|
|
939
967
|
...options?.decoderOptions,
|
|
940
968
|
formats: ["QRCode"]
|
|
941
969
|
};
|
|
@@ -1051,6 +1079,15 @@ var QrScanner = class {
|
|
|
1051
1079
|
static setWorkerUrl(url) {
|
|
1052
1080
|
setWorkerUrl(url);
|
|
1053
1081
|
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Enable or disable debug logging (performance timings, camera selection).
|
|
1084
|
+
* Off by default. Useful for diagnosing camera issues in the browser console.
|
|
1085
|
+
* @example
|
|
1086
|
+
* QrScanner.setDebug(true);
|
|
1087
|
+
*/
|
|
1088
|
+
static setDebug(enabled2) {
|
|
1089
|
+
setDebug(enabled2);
|
|
1090
|
+
}
|
|
1054
1091
|
/** Scan a single image (not a video stream). */
|
|
1055
1092
|
static scanImage(source, options) {
|
|
1056
1093
|
return scanImage(source, options);
|