@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 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
- console.debug(`[QrScanner] acquireStream: ${(t1 - t0).toFixed(0)}ms`);
84
+ debug(`[QrScanner] acquireStream: ${(t1 - t0).toFixed(0)}ms`);
70
85
  await this.ensureBestCamera();
71
86
  const t2 = performance.now();
72
- console.debug(`[QrScanner] ensureBestCamera: ${(t2 - t1).toFixed(0)}ms`);
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
- console.debug(`[QrScanner] video.play: ${(t3 - t2).toFixed(0)}ms`);
78
- console.debug(`[QrScanner] camera.start total: ${(t3 - t0).toFixed(0)}ms`);
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
- console.debug(
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
- console.debug(
176
+ debug(
164
177
  `[QrScanner] ensureBestCamera: skipped (focusMode: ${JSON.stringify(capabilities.focusMode)})`
165
178
  );
166
179
  return;
167
180
  }
168
- console.debug(
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
- console.debug(
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
- console.debug(
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
- console.debug(
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
- try {
232
- this.stream = await navigator.mediaDevices.getUserMedia({
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
- } catch {
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
- this.buildConstraints()
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
- } catch {
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({ video, audio: false });
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
- // facingMode/deviceId + resolution
281
- this.buildConstraints(),
282
- // facingMode/deviceId only, no resolution
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 (let i = 0; i < attempts.length; i++) {
333
+ for (const attempt of attempts) {
289
334
  const t = performance.now();
290
335
  try {
291
- const stream = await navigator.mediaDevices.getUserMedia(attempts[i]);
292
- console.debug(
293
- `[QrScanner] getUserMedia(${labels[i]}): ${(performance.now() - t).toFixed(0)}ms \u2713`
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
- console.debug(
298
- `[QrScanner] getUserMedia(${labels[i]}): ${(performance.now() - t).toFixed(0)}ms \u2717 ${err instanceof DOMException ? err.name : err}`
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
- if (err.name === "NotAllowedError") {
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
- throw err;
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 scale = Math.max(
434
- elementWidth / videoWidth,
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
- console.debug(
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
- console.debug(
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
- console.debug(
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
- switch (mode) {
773
- case "original":
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/scan-image.ts
916
- var defaultReaderOptions = {
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
- ...defaultReaderOptions,
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);