scandoc-ai-components 0.1.19 → 0.1.20

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.
Files changed (2) hide show
  1. package/dist/index.js +97 -62
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11178,6 +11178,7 @@ class ExtractorVideo {
11178
11178
  this.candidateImages = [];
11179
11179
  this.extractionImages = {};
11180
11180
  this.onExtractedResults = onExtractedResults;
11181
+ this.onCameraSelected = null;
11181
11182
  this.pastBlurValues = [];
11182
11183
  this.isRunning = false;
11183
11184
  this.scanStartTime = null;
@@ -11208,6 +11209,23 @@ class ExtractorVideo {
11208
11209
  clearTimeout(this.turnMessageTimer);
11209
11210
  this.turnMessageTimer = null;
11210
11211
  }
11212
+ emitCameraInfo(cameraInfo) {
11213
+ this.lastCameraInfo = cameraInfo;
11214
+ try {
11215
+ if (typeof this.onCameraSelected === "function") {
11216
+ this.onCameraSelected(cameraInfo);
11217
+ }
11218
+ } catch (err) {
11219
+ console.warn("onCameraSelected callback failed:", err);
11220
+ }
11221
+ try {
11222
+ window.dispatchEvent(new CustomEvent("ScanDocAICameraSelected", {
11223
+ detail: cameraInfo
11224
+ }));
11225
+ } catch (err) {
11226
+ console.warn("Could not dispatch ScanDocAICameraSelected event:", err);
11227
+ }
11228
+ }
11211
11229
  async analyzeVideoStream() {
11212
11230
  if (!this.isRunning) return;
11213
11231
  const now = Date.now();
@@ -11402,6 +11420,7 @@ class ExtractorVideo {
11402
11420
  });
11403
11421
  this.currentStream = cameraResult.stream;
11404
11422
  this.lastCameraInfo = cameraResult;
11423
+ this.emitCameraInfo(cameraResult);
11405
11424
  try {
11406
11425
  const focusResult = await tryEnableDocumentFocus(this.currentStream);
11407
11426
  console.log("Focus result:", focusResult);
@@ -11425,7 +11444,8 @@ class ExtractorVideo {
11425
11444
  deviceId: cameraResult.deviceId,
11426
11445
  strategy: cameraResult.strategy,
11427
11446
  settings: cameraResult.settings,
11428
- score: cameraResult.score
11447
+ score: cameraResult.score,
11448
+ tested: cameraResult.tested
11429
11449
  });
11430
11450
  return true;
11431
11451
  } catch (error) {
@@ -11454,7 +11474,7 @@ class ExtractorVideo {
11454
11474
  try {
11455
11475
  this.video.pause();
11456
11476
  } catch (_) {}
11457
- if (this.video.srcObject) {
11477
+ if (this.video.srcObject !== undefined && this.video.srcObject !== null) {
11458
11478
  try {
11459
11479
  this.video.srcObject.getTracks().forEach(t => t.stop());
11460
11480
  } catch (_) {}
@@ -11642,6 +11662,18 @@ class ExtractorVideo {
11642
11662
  align-items: center;
11643
11663
  color: ${messageColor};
11644
11664
  }
11665
+ .scandoc-camera-debug {
11666
+ margin-top: 12px;
11667
+ padding: 10px;
11668
+ max-width: 95vw;
11669
+ overflow: auto;
11670
+ font-size: 12px;
11671
+ background: #f3f3f3;
11672
+ border: 1px solid #ddd;
11673
+ border-radius: 8px;
11674
+ white-space: pre-wrap;
11675
+ word-break: break-word;
11676
+ }
11645
11677
  </style>
11646
11678
  `;
11647
11679
  }
@@ -11694,6 +11726,7 @@ function getAspectRatio(settings = {}) {
11694
11726
  return null;
11695
11727
  }
11696
11728
  async function attachStreamToVideo(videoEl, stream) {
11729
+ if (!videoEl) return;
11697
11730
  videoEl.srcObject = stream;
11698
11731
  try {
11699
11732
  await videoEl.play();
@@ -11780,60 +11813,81 @@ function isLikelyRearCamera(label = "") {
11780
11813
  const v = normalizeLabel(label);
11781
11814
  return v.includes("back") || v.includes("rear") || v.includes("environment") || v.includes("world");
11782
11815
  }
11816
+ function isLikelyFrontCamera(label = "") {
11817
+ const v = normalizeLabel(label);
11818
+ return v.includes("front") || v.includes("user") || v.includes("facetime") || v.includes("selfie");
11819
+ }
11783
11820
  function isLikelyUltraWideOrMacro(label = "") {
11784
11821
  const v = normalizeLabel(label);
11785
11822
  return v.includes("ultra") || v.includes("ultrawide") || v.includes("wide angle") || v.includes("0.5") || v.includes("macro") || v.includes("fisheye");
11786
11823
  }
11824
+ function isLikelyTelephoto(label = "") {
11825
+ const v = normalizeLabel(label);
11826
+ return v.includes("tele") || v.includes("zoom") || v.includes("3x") || v.includes("5x") || v.includes("10x");
11827
+ }
11828
+ function getPixelCount(settings = {}) {
11829
+ return Number(settings.width || 0) * Number(settings.height || 0);
11830
+ }
11787
11831
  function scoreCameraCandidate({
11788
11832
  label,
11789
- settings
11833
+ settings,
11834
+ isBootstrap = false
11790
11835
  }) {
11791
11836
  const width = Number(settings?.width || 0);
11792
11837
  const height = Number(settings?.height || 0);
11793
- const pixels = width * height;
11838
+ const pixels = getPixelCount(settings);
11794
11839
  const aspectRatio = getAspectRatio(settings) || 0;
11795
11840
  let score = 0;
11796
11841
  score += pixels;
11842
+ if (isBootstrap) {
11843
+ score += 400000;
11844
+ }
11797
11845
  if (isLikelyRearCamera(label)) {
11798
- score += 5_000_000;
11846
+ score += 4000000;
11847
+ }
11848
+ if (isLikelyFrontCamera(label)) {
11849
+ score -= 12000000;
11799
11850
  }
11800
11851
  if (isLikelyUltraWideOrMacro(label)) {
11801
- score -= 8_000_000;
11852
+ score -= 10000000;
11853
+ }
11854
+ if (isLikelyTelephoto(label)) {
11855
+ score -= 2500000;
11802
11856
  }
11803
- if (aspectRatio > 1.65 && aspectRatio < 1.85) {
11804
- score += 750_000;
11857
+ if (aspectRatio >= 1.6 && aspectRatio <= 1.9) {
11858
+ score += 700000;
11805
11859
  }
11806
- if (aspectRatio > 2.1) {
11807
- score -= 2_000_000;
11860
+ if (aspectRatio > 2.05) {
11861
+ score -= 1500000;
11808
11862
  }
11809
11863
  if (width >= 1920) {
11810
- score += 500_000;
11864
+ score += 500000;
11811
11865
  }
11812
11866
  if (width < 1280 || height < 720) {
11813
- score -= 3_000_000;
11867
+ score -= 4000000;
11814
11868
  }
11815
11869
  return score;
11816
11870
  }
11817
- async function probeCameraCandidate(device, videoElement) {
11871
+ async function probeCameraCandidate(device) {
11818
11872
  let stream = null;
11819
11873
  try {
11820
11874
  stream = await navigator.mediaDevices.getUserMedia(buildDeviceConstraints(device.deviceId));
11821
- await attachStreamToVideo(videoElement, stream);
11822
- await waitForVideoReady(videoElement, 800);
11823
11875
  const track = stream.getVideoTracks?.()[0];
11824
11876
  const settings = track?.getSettings?.() || {};
11877
+ const label = track?.label || device.label || "";
11878
+ const score = scoreCameraCandidate({
11879
+ label,
11880
+ settings
11881
+ });
11825
11882
  return {
11826
11883
  ok: true,
11827
11884
  device,
11828
11885
  stream,
11829
- label: track?.label || device.label || "",
11886
+ label,
11830
11887
  deviceId: settings.deviceId || device.deviceId,
11831
11888
  settings,
11832
11889
  aspectRatio: getAspectRatio(settings),
11833
- score: scoreCameraCandidate({
11834
- label: track?.label || device.label || "",
11835
- settings
11836
- })
11890
+ score
11837
11891
  };
11838
11892
  } catch (error) {
11839
11893
  if (stream) {
@@ -11847,30 +11901,9 @@ async function probeCameraCandidate(device, videoElement) {
11847
11901
  };
11848
11902
  }
11849
11903
  }
11850
- function waitForVideoReady(videoEl, timeoutMs = 800) {
11851
- return new Promise(resolve => {
11852
- if (videoEl.readyState >= 2 && videoEl.videoWidth > 0 && videoEl.videoHeight > 0) {
11853
- resolve();
11854
- return;
11855
- }
11856
- let done = false;
11857
- const finish = () => {
11858
- if (done) return;
11859
- done = true;
11860
- videoEl.removeEventListener("loadeddata", onReady);
11861
- videoEl.removeEventListener("canplay", onReady);
11862
- clearTimeout(timer);
11863
- resolve();
11864
- };
11865
- const onReady = () => finish();
11866
- const timer = setTimeout(finish, timeoutMs);
11867
- videoEl.addEventListener("loadeddata", onReady, {
11868
- once: true
11869
- });
11870
- videoEl.addEventListener("canplay", onReady, {
11871
- once: true
11872
- });
11873
- });
11904
+ function isIOSLike() {
11905
+ const ua = navigator.userAgent || "";
11906
+ return /iPhone|iPad|iPod/i.test(ua);
11874
11907
  }
11875
11908
  async function tryEnableDocumentFocus(stream) {
11876
11909
  const track = stream?.getVideoTracks?.()[0];
@@ -11947,27 +11980,21 @@ async function openBestDocumentCamera({
11947
11980
  if (!window.isSecureContext) {
11948
11981
  throw new Error("Camera requires HTTPS / secure context");
11949
11982
  }
11950
- let bootstrapStream = null;
11983
+ let bootstrapStream;
11951
11984
  try {
11952
11985
  bootstrapStream = await navigator.mediaDevices.getUserMedia(buildDocumentScanConstraints(preferredFacingMode));
11953
- await attachStreamToVideo(videoElement, bootstrapStream);
11954
- await waitForVideoReady(videoElement, 800);
11955
11986
  } catch (preferredError) {
11956
11987
  console.warn("Preferred camera open failed, falling back:", preferredError);
11957
11988
  bootstrapStream = await navigator.mediaDevices.getUserMedia({
11958
11989
  video: true,
11959
11990
  audio: false
11960
11991
  });
11961
- await attachStreamToVideo(videoElement, bootstrapStream);
11962
- await waitForVideoReady(videoElement, 800);
11963
11992
  }
11964
11993
  const bootstrapTrack = bootstrapStream.getVideoTracks?.()[0];
11965
11994
  const bootstrapSettings = bootstrapTrack?.getSettings?.() || {};
11966
11995
  const bootstrapLabel = bootstrapTrack?.label || "";
11967
11996
  const bootstrapDeviceId = bootstrapSettings.deviceId || null;
11968
11997
  let best = {
11969
- ok: true,
11970
- device: null,
11971
11998
  stream: bootstrapStream,
11972
11999
  label: bootstrapLabel,
11973
12000
  deviceId: bootstrapDeviceId,
@@ -11975,26 +12002,34 @@ async function openBestDocumentCamera({
11975
12002
  aspectRatio: getAspectRatio(bootstrapSettings),
11976
12003
  score: scoreCameraCandidate({
11977
12004
  label: bootstrapLabel,
11978
- settings: bootstrapSettings
12005
+ settings: bootstrapSettings,
12006
+ isBootstrap: true
11979
12007
  }),
11980
- strategy: "bootstrap"
12008
+ strategy: "bootstrap-environment",
12009
+ tested: []
11981
12010
  };
11982
12011
  const devices = await enumerateVideoInputDevices();
11983
- if (devices.length <= 1) {
11984
- return best;
11985
- }
11986
12012
  const rearDevices = devices.filter(d => isLikelyRearCamera(d.label));
11987
- const candidates = rearDevices.length > 0 ? rearDevices : devices;
12013
+ const candidatesBase = rearDevices.length > 0 ? rearDevices : devices;
12014
+ const candidates = candidatesBase.filter(d => d.deviceId && d.deviceId !== bootstrapDeviceId).filter(d => !isLikelyFrontCamera(d.label)).slice(0, isIOSLike() ? 2 : 4);
11988
12015
  for (const device of candidates) {
11989
- if (!device.deviceId) continue;
11990
- if (device.deviceId === bootstrapDeviceId) continue;
11991
- const probed = await probeCameraCandidate(device, videoElement);
12016
+ const probed = await probeCameraCandidate(device);
12017
+ best.tested.push({
12018
+ label: device.label || "",
12019
+ deviceId: device.deviceId,
12020
+ ok: probed.ok,
12021
+ settings: probed.settings || null,
12022
+ score: probed.score,
12023
+ error: probed.ok ? null : String(probed.error?.message || probed.error || "")
12024
+ });
11992
12025
  if (!probed.ok) continue;
11993
- if (probed.score > best.score) {
12026
+ const clearlyBetter = probed.score > best.score + 1000000;
12027
+ if (clearlyBetter) {
11994
12028
  await stopStream(best.stream);
11995
12029
  best = {
11996
12030
  ...probed,
11997
- strategy: "probed-better-camera"
12031
+ strategy: "switched-after-ranking",
12032
+ tested: best.tested
11998
12033
  };
11999
12034
  } else {
12000
12035
  await stopStream(probed.stream);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scandoc-ai-components",
3
3
  "author": "ScanDoc-AI",
4
- "version": "0.1.19",
4
+ "version": "0.1.20",
5
5
  "private": false,
6
6
  "description": "Pure JavaScript package for integrating ScanDoc-AI services.",
7
7
  "keywords": [