@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/dist/index.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
// src/debug.ts
|
|
2
|
+
var enabled = false;
|
|
3
|
+
function setDebug(on) {
|
|
4
|
+
enabled = on;
|
|
5
|
+
}
|
|
6
|
+
function debug(...args) {
|
|
7
|
+
if (enabled) console.debug(...args);
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
// src/camera.ts
|
|
2
11
|
var CameraPermissionError = class extends Error {
|
|
3
12
|
constructor(message = "Camera access denied. Please grant camera permission and try again.") {
|
|
@@ -25,6 +34,12 @@ function setCachedDeviceId(facingMode, deviceId) {
|
|
|
25
34
|
} catch {
|
|
26
35
|
}
|
|
27
36
|
}
|
|
37
|
+
function clearCachedDeviceId(facingMode) {
|
|
38
|
+
try {
|
|
39
|
+
localStorage.removeItem(`${CACHE_KEY_PREFIX}${facingMode}`);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
var CameraManager = class {
|
|
29
44
|
constructor(config = {}) {
|
|
30
45
|
this.stream = null;
|
|
@@ -38,16 +53,16 @@ var CameraManager = class {
|
|
|
38
53
|
const t0 = performance.now();
|
|
39
54
|
this.stream = await this.acquireStream();
|
|
40
55
|
const t1 = performance.now();
|
|
41
|
-
|
|
56
|
+
debug(`[QrScanner] acquireStream: ${(t1 - t0).toFixed(0)}ms`);
|
|
42
57
|
await this.ensureBestCamera();
|
|
43
58
|
const t2 = performance.now();
|
|
44
|
-
|
|
59
|
+
debug(`[QrScanner] ensureBestCamera: ${(t2 - t1).toFixed(0)}ms`);
|
|
45
60
|
video.srcObject = this.stream;
|
|
46
61
|
video.setAttribute("playsinline", "true");
|
|
47
62
|
await video.play();
|
|
48
63
|
const t3 = performance.now();
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
debug(`[QrScanner] video.play: ${(t3 - t2).toFixed(0)}ms`);
|
|
65
|
+
debug(`[QrScanner] camera.start total: ${(t3 - t0).toFixed(0)}ms`);
|
|
51
66
|
if (this.facingMode === "environment" || this.facingMode === "user") {
|
|
52
67
|
const finalDeviceId = this.getVideoTrack()?.getSettings().deviceId;
|
|
53
68
|
if (finalDeviceId) {
|
|
@@ -122,9 +137,7 @@ var CameraManager = class {
|
|
|
122
137
|
*/
|
|
123
138
|
async ensureBestCamera() {
|
|
124
139
|
if (this.facingMode !== "environment" && this.facingMode !== "user") {
|
|
125
|
-
|
|
126
|
-
"[QrScanner] ensureBestCamera: skipped (specific deviceId)"
|
|
127
|
-
);
|
|
140
|
+
debug("[QrScanner] ensureBestCamera: skipped (specific deviceId)");
|
|
128
141
|
return;
|
|
129
142
|
}
|
|
130
143
|
const track = this.getVideoTrack();
|
|
@@ -132,29 +145,35 @@ var CameraManager = class {
|
|
|
132
145
|
try {
|
|
133
146
|
const capabilities = track.getCapabilities();
|
|
134
147
|
if (!capabilities.focusMode || capabilities.focusMode.includes("continuous")) {
|
|
135
|
-
|
|
148
|
+
debug(
|
|
136
149
|
`[QrScanner] ensureBestCamera: skipped (focusMode: ${JSON.stringify(capabilities.focusMode)})`
|
|
137
150
|
);
|
|
138
151
|
return;
|
|
139
152
|
}
|
|
140
|
-
|
|
153
|
+
debug(
|
|
141
154
|
`[QrScanner] ensureBestCamera: current camera lacks autofocus (focusMode: ${JSON.stringify(capabilities.focusMode)})`
|
|
142
155
|
);
|
|
143
|
-
} catch {
|
|
156
|
+
} catch (err) {
|
|
157
|
+
debug(
|
|
158
|
+
`[QrScanner] ensureBestCamera: skipped (getCapabilities failed: ${err instanceof Error ? err.message : err})`
|
|
159
|
+
);
|
|
144
160
|
return;
|
|
145
161
|
}
|
|
146
162
|
const currentDeviceId = track.getSettings().deviceId;
|
|
147
163
|
let devices;
|
|
148
164
|
try {
|
|
149
165
|
devices = await navigator.mediaDevices.enumerateDevices();
|
|
150
|
-
} catch {
|
|
166
|
+
} catch (err) {
|
|
167
|
+
debug(
|
|
168
|
+
`[QrScanner] ensureBestCamera: skipped (enumerateDevices failed: ${err instanceof Error ? err.message : err})`
|
|
169
|
+
);
|
|
151
170
|
return;
|
|
152
171
|
}
|
|
153
172
|
if (!Array.isArray(devices)) return;
|
|
154
173
|
const candidates = devices.filter(
|
|
155
174
|
(d) => d.kind === "videoinput" && d.deviceId !== currentDeviceId
|
|
156
175
|
);
|
|
157
|
-
|
|
176
|
+
debug(
|
|
158
177
|
`[QrScanner] ensureBestCamera: testing ${candidates.length} candidate camera(s)`
|
|
159
178
|
);
|
|
160
179
|
if (candidates.length === 0) return;
|
|
@@ -171,13 +190,16 @@ var CameraManager = class {
|
|
|
171
190
|
},
|
|
172
191
|
audio: false
|
|
173
192
|
});
|
|
174
|
-
|
|
193
|
+
debug(
|
|
175
194
|
`[QrScanner] ensureBestCamera: candidate ${candidate.label || candidate.deviceId.slice(0, 8)}: getUserMedia ${(performance.now() - t).toFixed(0)}ms`
|
|
176
195
|
);
|
|
177
|
-
} catch {
|
|
178
|
-
|
|
179
|
-
`[QrScanner] ensureBestCamera: candidate ${candidate.label || candidate.deviceId.slice(0, 8)}: getUserMedia failed ${(performance.now() - t).toFixed(0)}ms`
|
|
196
|
+
} catch (err) {
|
|
197
|
+
debug(
|
|
198
|
+
`[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}`
|
|
180
199
|
);
|
|
200
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
201
|
+
throw new CameraPermissionError();
|
|
202
|
+
}
|
|
181
203
|
continue;
|
|
182
204
|
}
|
|
183
205
|
const candidateTrack = candidateStream.getVideoTracks()[0];
|
|
@@ -196,27 +218,52 @@ var CameraManager = class {
|
|
|
196
218
|
this.stream = candidateStream;
|
|
197
219
|
return;
|
|
198
220
|
}
|
|
199
|
-
} catch {
|
|
221
|
+
} catch (err) {
|
|
222
|
+
debug(
|
|
223
|
+
`[QrScanner] ensureBestCamera: candidate getCapabilities failed \u2014 ${err instanceof Error ? err.message : err}`
|
|
224
|
+
);
|
|
200
225
|
}
|
|
201
226
|
for (const t2 of candidateStream.getTracks()) t2.stop();
|
|
202
227
|
}
|
|
203
|
-
|
|
204
|
-
|
|
228
|
+
const recoveryAttempts = [
|
|
229
|
+
// Original deviceId + resolution
|
|
230
|
+
{
|
|
205
231
|
video: {
|
|
206
232
|
deviceId: currentDeviceId ? { exact: currentDeviceId } : void 0,
|
|
207
233
|
width: this.resolution?.width ?? { ideal: 1920 },
|
|
208
234
|
height: this.resolution?.height ?? { ideal: 1080 }
|
|
209
235
|
},
|
|
210
236
|
audio: false
|
|
211
|
-
}
|
|
212
|
-
|
|
237
|
+
},
|
|
238
|
+
// facingMode + resolution
|
|
239
|
+
this.buildConstraints(),
|
|
240
|
+
// facingMode only
|
|
241
|
+
this.buildConstraints(false),
|
|
242
|
+
// Bare minimum — should always succeed if a camera exists
|
|
243
|
+
{ video: true, audio: false }
|
|
244
|
+
];
|
|
245
|
+
let lastRecoveryError;
|
|
246
|
+
for (const constraints of recoveryAttempts) {
|
|
213
247
|
try {
|
|
214
|
-
this.stream = await navigator.mediaDevices.getUserMedia(
|
|
215
|
-
|
|
248
|
+
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
249
|
+
return;
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
252
|
+
throw new CameraPermissionError();
|
|
253
|
+
}
|
|
254
|
+
if (err instanceof DOMException && err.name === "NotFoundError") {
|
|
255
|
+
throw new CameraNotFoundError();
|
|
256
|
+
}
|
|
257
|
+
lastRecoveryError = err;
|
|
258
|
+
debug(
|
|
259
|
+
`[QrScanner] ensureBestCamera recovery failed: ${err instanceof Error ? err.name : err}`
|
|
216
260
|
);
|
|
217
|
-
|
|
261
|
+
continue;
|
|
218
262
|
}
|
|
219
263
|
}
|
|
264
|
+
debug(
|
|
265
|
+
`[QrScanner] ensureBestCamera: all recovery attempts failed, last error: ${lastRecoveryError instanceof Error ? lastRecoveryError.message : lastRecoveryError}`
|
|
266
|
+
);
|
|
220
267
|
}
|
|
221
268
|
getVideoTrack() {
|
|
222
269
|
if (!this.stream) return null;
|
|
@@ -231,12 +278,10 @@ var CameraManager = class {
|
|
|
231
278
|
* fewer constraints lets us still open the camera on those browsers.
|
|
232
279
|
*/
|
|
233
280
|
async acquireStream() {
|
|
234
|
-
const labels = [];
|
|
235
281
|
const attempts = [];
|
|
236
282
|
if (this.facingMode === "environment" || this.facingMode === "user") {
|
|
237
283
|
const cachedId = getCachedDeviceId(this.facingMode);
|
|
238
284
|
if (cachedId) {
|
|
239
|
-
labels.push("cached deviceId");
|
|
240
285
|
const video = {
|
|
241
286
|
deviceId: { exact: cachedId }
|
|
242
287
|
};
|
|
@@ -244,42 +289,50 @@ var CameraManager = class {
|
|
|
244
289
|
else video.width = { ideal: 1920 };
|
|
245
290
|
if (this.resolution?.height) video.height = this.resolution.height;
|
|
246
291
|
else video.height = { ideal: 1080 };
|
|
247
|
-
attempts.push({
|
|
292
|
+
attempts.push({
|
|
293
|
+
label: "cached deviceId",
|
|
294
|
+
constraints: { video, audio: false },
|
|
295
|
+
isCachedDeviceId: true
|
|
296
|
+
});
|
|
248
297
|
}
|
|
249
298
|
}
|
|
250
|
-
labels.push("full constraints", "no resolution", "bare minimum");
|
|
251
299
|
attempts.push(
|
|
252
|
-
|
|
253
|
-
this.buildConstraints(),
|
|
254
|
-
|
|
255
|
-
this.buildConstraints(false),
|
|
256
|
-
// Bare minimum
|
|
257
|
-
{ video: true, audio: false }
|
|
300
|
+
{ label: "full constraints", constraints: this.buildConstraints() },
|
|
301
|
+
{ label: "no resolution", constraints: this.buildConstraints(false) },
|
|
302
|
+
{ label: "bare minimum", constraints: { video: true, audio: false } }
|
|
258
303
|
);
|
|
259
304
|
let lastError;
|
|
260
|
-
for (
|
|
305
|
+
for (const attempt of attempts) {
|
|
261
306
|
const t = performance.now();
|
|
262
307
|
try {
|
|
263
|
-
const stream = await navigator.mediaDevices.getUserMedia(
|
|
264
|
-
|
|
265
|
-
|
|
308
|
+
const stream = await navigator.mediaDevices.getUserMedia(
|
|
309
|
+
attempt.constraints
|
|
310
|
+
);
|
|
311
|
+
debug(
|
|
312
|
+
`[QrScanner] getUserMedia(${attempt.label}): ${(performance.now() - t).toFixed(0)}ms \u2713`
|
|
266
313
|
);
|
|
267
314
|
return stream;
|
|
268
315
|
} catch (err) {
|
|
269
|
-
|
|
270
|
-
`[QrScanner] getUserMedia(${
|
|
316
|
+
debug(
|
|
317
|
+
`[QrScanner] getUserMedia(${attempt.label}): ${(performance.now() - t).toFixed(0)}ms \u2717 ${err instanceof Error ? err.name : err}`
|
|
271
318
|
);
|
|
272
|
-
if (err instanceof DOMException) {
|
|
273
|
-
|
|
274
|
-
throw new CameraPermissionError();
|
|
275
|
-
}
|
|
276
|
-
if (err.name === "NotFoundError") {
|
|
277
|
-
throw new CameraNotFoundError();
|
|
278
|
-
}
|
|
279
|
-
lastError = err;
|
|
280
|
-
continue;
|
|
319
|
+
if (err instanceof DOMException && err.name === "NotAllowedError") {
|
|
320
|
+
throw new CameraPermissionError();
|
|
281
321
|
}
|
|
282
|
-
|
|
322
|
+
if (err instanceof DOMException && err.name === "NotFoundError") {
|
|
323
|
+
throw new CameraNotFoundError();
|
|
324
|
+
}
|
|
325
|
+
if (!(err instanceof DOMException) && !(err instanceof OverconstrainedError)) {
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
328
|
+
if (attempt.isCachedDeviceId) {
|
|
329
|
+
clearCachedDeviceId(this.facingMode);
|
|
330
|
+
debug(
|
|
331
|
+
`[QrScanner] cleared stale cached deviceId for "${this.facingMode}"`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
lastError = err;
|
|
335
|
+
continue;
|
|
283
336
|
}
|
|
284
337
|
}
|
|
285
338
|
throw lastError;
|
|
@@ -389,9 +442,6 @@ var FrameExtractor = class {
|
|
|
389
442
|
markWorkerIdle() {
|
|
390
443
|
this.workerBusy = false;
|
|
391
444
|
}
|
|
392
|
-
markWorkerBusy() {
|
|
393
|
-
this.workerBusy = true;
|
|
394
|
-
}
|
|
395
445
|
};
|
|
396
446
|
|
|
397
447
|
// src/overlay.ts
|
|
@@ -401,22 +451,9 @@ function getRenderedVideoRect(video) {
|
|
|
401
451
|
const videoWidth = video.videoWidth || 1;
|
|
402
452
|
const videoHeight = video.videoHeight || 1;
|
|
403
453
|
const objectFit = getComputedStyle(video).objectFit;
|
|
404
|
-
if (objectFit === "cover") {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
elementHeight / videoHeight
|
|
408
|
-
);
|
|
409
|
-
const renderedWidth = videoWidth * scale;
|
|
410
|
-
const renderedHeight = videoHeight * scale;
|
|
411
|
-
return {
|
|
412
|
-
offsetX: (elementWidth - renderedWidth) / 2,
|
|
413
|
-
offsetY: (elementHeight - renderedHeight) / 2,
|
|
414
|
-
width: renderedWidth,
|
|
415
|
-
height: renderedHeight
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
if (objectFit === "contain") {
|
|
419
|
-
const scale = Math.min(
|
|
454
|
+
if (objectFit === "cover" || objectFit === "contain") {
|
|
455
|
+
const scaleFn = objectFit === "cover" ? Math.max : Math.min;
|
|
456
|
+
const scale = scaleFn(
|
|
420
457
|
elementWidth / videoWidth,
|
|
421
458
|
elementHeight / videoHeight
|
|
422
459
|
);
|
|
@@ -654,7 +691,7 @@ var Scanner = class {
|
|
|
654
691
|
}
|
|
655
692
|
}
|
|
656
693
|
await this.camera.start(this.video);
|
|
657
|
-
|
|
694
|
+
debug(
|
|
658
695
|
`[QrScanner] start: camera ready ${(performance.now() - t0).toFixed(0)}ms`
|
|
659
696
|
);
|
|
660
697
|
if (this.overlay) {
|
|
@@ -663,7 +700,7 @@ var Scanner = class {
|
|
|
663
700
|
if (!this.worker) {
|
|
664
701
|
const tw = performance.now();
|
|
665
702
|
this.worker = this.createWorker();
|
|
666
|
-
|
|
703
|
+
debug(
|
|
667
704
|
`[QrScanner] start: worker created ${(performance.now() - tw).toFixed(0)}ms`
|
|
668
705
|
);
|
|
669
706
|
}
|
|
@@ -678,9 +715,7 @@ var Scanner = class {
|
|
|
678
715
|
});
|
|
679
716
|
this.active = true;
|
|
680
717
|
this.paused = false;
|
|
681
|
-
|
|
682
|
-
`[QrScanner] start: total ${(performance.now() - t0).toFixed(0)}ms`
|
|
683
|
-
);
|
|
718
|
+
debug(`[QrScanner] start: total ${(performance.now() - t0).toFixed(0)}ms`);
|
|
684
719
|
}
|
|
685
720
|
stop() {
|
|
686
721
|
this.frameExtractor?.stop();
|
|
@@ -739,18 +774,9 @@ var Scanner = class {
|
|
|
739
774
|
}
|
|
740
775
|
setInversionMode(mode) {
|
|
741
776
|
if (!this.worker) return;
|
|
742
|
-
const options = {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
options.tryInvert = false;
|
|
746
|
-
break;
|
|
747
|
-
case "invert":
|
|
748
|
-
options.tryInvert = true;
|
|
749
|
-
break;
|
|
750
|
-
case "both":
|
|
751
|
-
options.tryInvert = true;
|
|
752
|
-
break;
|
|
753
|
-
}
|
|
777
|
+
const options = {
|
|
778
|
+
tryInvert: mode !== "original"
|
|
779
|
+
};
|
|
754
780
|
const msg = { type: "configure", options };
|
|
755
781
|
this.worker.postMessage(msg);
|
|
756
782
|
}
|
|
@@ -883,8 +909,8 @@ async function createImageBitmapFromBlob(blob) {
|
|
|
883
909
|
}
|
|
884
910
|
}
|
|
885
911
|
|
|
886
|
-
// src/
|
|
887
|
-
var
|
|
912
|
+
// src/decoder-utils.ts
|
|
913
|
+
var DEFAULT_READER_OPTIONS = {
|
|
888
914
|
formats: ["QRCode"],
|
|
889
915
|
tryHarder: true,
|
|
890
916
|
tryInvert: true,
|
|
@@ -901,12 +927,14 @@ function mapPosition(position) {
|
|
|
901
927
|
position.bottomLeft
|
|
902
928
|
];
|
|
903
929
|
}
|
|
930
|
+
|
|
931
|
+
// src/scan-image.ts
|
|
904
932
|
function isDirectInput(source) {
|
|
905
933
|
return source instanceof Blob || source instanceof ArrayBuffer || source instanceof Uint8Array || typeof ImageData !== "undefined" && source instanceof ImageData;
|
|
906
934
|
}
|
|
907
935
|
async function scanImage(source, options) {
|
|
908
936
|
const readerOptions = {
|
|
909
|
-
...
|
|
937
|
+
...DEFAULT_READER_OPTIONS,
|
|
910
938
|
...options?.decoderOptions,
|
|
911
939
|
formats: ["QRCode"]
|
|
912
940
|
};
|
|
@@ -1022,6 +1050,15 @@ var QrScanner = class {
|
|
|
1022
1050
|
static setWorkerUrl(url) {
|
|
1023
1051
|
setWorkerUrl(url);
|
|
1024
1052
|
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Enable or disable debug logging (performance timings, camera selection).
|
|
1055
|
+
* Off by default. Useful for diagnosing camera issues in the browser console.
|
|
1056
|
+
* @example
|
|
1057
|
+
* QrScanner.setDebug(true);
|
|
1058
|
+
*/
|
|
1059
|
+
static setDebug(enabled2) {
|
|
1060
|
+
setDebug(enabled2);
|
|
1061
|
+
}
|
|
1025
1062
|
/** Scan a single image (not a video stream). */
|
|
1026
1063
|
static scanImage(source, options) {
|
|
1027
1064
|
return scanImage(source, options);
|