@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/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
- console.debug(`[QrScanner] acquireStream: ${(t1 - t0).toFixed(0)}ms`);
56
+ debug(`[QrScanner] acquireStream: ${(t1 - t0).toFixed(0)}ms`);
42
57
  await this.ensureBestCamera();
43
58
  const t2 = performance.now();
44
- console.debug(`[QrScanner] ensureBestCamera: ${(t2 - t1).toFixed(0)}ms`);
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
- console.debug(`[QrScanner] video.play: ${(t3 - t2).toFixed(0)}ms`);
50
- console.debug(`[QrScanner] camera.start total: ${(t3 - t0).toFixed(0)}ms`);
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
- console.debug(
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
- console.debug(
148
+ debug(
136
149
  `[QrScanner] ensureBestCamera: skipped (focusMode: ${JSON.stringify(capabilities.focusMode)})`
137
150
  );
138
151
  return;
139
152
  }
140
- console.debug(
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
- console.debug(
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
- console.debug(
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
- console.debug(
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
- try {
204
- this.stream = await navigator.mediaDevices.getUserMedia({
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
- } catch {
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
- this.buildConstraints()
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
- } catch {
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({ video, audio: false });
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
- // facingMode/deviceId + resolution
253
- this.buildConstraints(),
254
- // facingMode/deviceId only, no resolution
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 (let i = 0; i < attempts.length; i++) {
305
+ for (const attempt of attempts) {
261
306
  const t = performance.now();
262
307
  try {
263
- const stream = await navigator.mediaDevices.getUserMedia(attempts[i]);
264
- console.debug(
265
- `[QrScanner] getUserMedia(${labels[i]}): ${(performance.now() - t).toFixed(0)}ms \u2713`
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
- console.debug(
270
- `[QrScanner] getUserMedia(${labels[i]}): ${(performance.now() - t).toFixed(0)}ms \u2717 ${err instanceof DOMException ? err.name : err}`
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
- if (err.name === "NotAllowedError") {
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
- throw err;
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 scale = Math.max(
406
- elementWidth / videoWidth,
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
- console.debug(
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
- console.debug(
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
- console.debug(
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
- switch (mode) {
744
- case "original":
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/scan-image.ts
887
- var defaultReaderOptions = {
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
- ...defaultReaderOptions,
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);