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.
- package/dist/index.js +97 -62
- 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 =
|
|
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 +=
|
|
11846
|
+
score += 4000000;
|
|
11847
|
+
}
|
|
11848
|
+
if (isLikelyFrontCamera(label)) {
|
|
11849
|
+
score -= 12000000;
|
|
11799
11850
|
}
|
|
11800
11851
|
if (isLikelyUltraWideOrMacro(label)) {
|
|
11801
|
-
score -=
|
|
11852
|
+
score -= 10000000;
|
|
11853
|
+
}
|
|
11854
|
+
if (isLikelyTelephoto(label)) {
|
|
11855
|
+
score -= 2500000;
|
|
11802
11856
|
}
|
|
11803
|
-
if (aspectRatio
|
|
11804
|
-
score +=
|
|
11857
|
+
if (aspectRatio >= 1.6 && aspectRatio <= 1.9) {
|
|
11858
|
+
score += 700000;
|
|
11805
11859
|
}
|
|
11806
|
-
if (aspectRatio > 2.
|
|
11807
|
-
score -=
|
|
11860
|
+
if (aspectRatio > 2.05) {
|
|
11861
|
+
score -= 1500000;
|
|
11808
11862
|
}
|
|
11809
11863
|
if (width >= 1920) {
|
|
11810
|
-
score +=
|
|
11864
|
+
score += 500000;
|
|
11811
11865
|
}
|
|
11812
11866
|
if (width < 1280 || height < 720) {
|
|
11813
|
-
score -=
|
|
11867
|
+
score -= 4000000;
|
|
11814
11868
|
}
|
|
11815
11869
|
return score;
|
|
11816
11870
|
}
|
|
11817
|
-
async function probeCameraCandidate(device
|
|
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
|
|
11886
|
+
label,
|
|
11830
11887
|
deviceId: settings.deviceId || device.deviceId,
|
|
11831
11888
|
settings,
|
|
11832
11889
|
aspectRatio: getAspectRatio(settings),
|
|
11833
|
-
score
|
|
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
|
|
11851
|
-
|
|
11852
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
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
|
-
|
|
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: "
|
|
12031
|
+
strategy: "switched-after-ranking",
|
|
12032
|
+
tested: best.tested
|
|
11998
12033
|
};
|
|
11999
12034
|
} else {
|
|
12000
12035
|
await stopStream(probed.stream);
|