scandoc-ai-components 0.1.17 → 0.1.19
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 +474 -482
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11174,16 +11174,6 @@ class ExtractorVideo {
|
|
|
11174
11174
|
static VALIDATION_BATCH_SIZE = 1;
|
|
11175
11175
|
static VALIDATION_IMG_WIDTH = 384;
|
|
11176
11176
|
static VALIDATION_IMG_HEIGHT = 384;
|
|
11177
|
-
static VIDEO_CANDIDATE_SETTINGS = Object.freeze([{
|
|
11178
|
-
width: 1920,
|
|
11179
|
-
height: 1080
|
|
11180
|
-
}, {
|
|
11181
|
-
width: 1280,
|
|
11182
|
-
height: 960
|
|
11183
|
-
}, {
|
|
11184
|
-
width: 1280,
|
|
11185
|
-
height: 720
|
|
11186
|
-
}]);
|
|
11187
11177
|
constructor(onExtractedResults) {
|
|
11188
11178
|
this.candidateImages = [];
|
|
11189
11179
|
this.extractionImages = {};
|
|
@@ -11197,7 +11187,8 @@ class ExtractorVideo {
|
|
|
11197
11187
|
this.waitingForSecondSide = false;
|
|
11198
11188
|
this.hasShownTurnMessage = false;
|
|
11199
11189
|
this.turnMessageTimer = null;
|
|
11200
|
-
this.
|
|
11190
|
+
this.currentStream = null;
|
|
11191
|
+
this.lastCameraInfo = null;
|
|
11201
11192
|
}
|
|
11202
11193
|
reset() {
|
|
11203
11194
|
this.stopVideo();
|
|
@@ -11213,6 +11204,7 @@ class ExtractorVideo {
|
|
|
11213
11204
|
this.lastMessage = null;
|
|
11214
11205
|
this.waitingForSecondSide = false;
|
|
11215
11206
|
this.hasShownTurnMessage = false;
|
|
11207
|
+
this.lastCameraInfo = null;
|
|
11216
11208
|
clearTimeout(this.turnMessageTimer);
|
|
11217
11209
|
this.turnMessageTimer = null;
|
|
11218
11210
|
}
|
|
@@ -11365,7 +11357,7 @@ class ExtractorVideo {
|
|
|
11365
11357
|
this.turnMessageTimer = null;
|
|
11366
11358
|
this.stopVideo();
|
|
11367
11359
|
if (isExtractionOk) {
|
|
11368
|
-
this.showMessage("Success - data extracted",
|
|
11360
|
+
this.showMessage("Success - data extracted", true);
|
|
11369
11361
|
const shouldStop = this.onExtractedResults({
|
|
11370
11362
|
success: true,
|
|
11371
11363
|
code: "001",
|
|
@@ -11373,7 +11365,7 @@ class ExtractorVideo {
|
|
|
11373
11365
|
data: extractionData
|
|
11374
11366
|
});
|
|
11375
11367
|
if (shouldStop === true) {
|
|
11376
|
-
setTimeout(() => this.
|
|
11368
|
+
setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
|
|
11377
11369
|
}
|
|
11378
11370
|
} else {
|
|
11379
11371
|
this.onExtractedResults({
|
|
@@ -11381,378 +11373,8 @@ class ExtractorVideo {
|
|
|
11381
11373
|
code: "005",
|
|
11382
11374
|
info: "Document validation passed but extraction failed."
|
|
11383
11375
|
});
|
|
11384
|
-
setTimeout(() => this.
|
|
11385
|
-
}
|
|
11386
|
-
}
|
|
11387
|
-
getSupportedConstraintsSafe() {
|
|
11388
|
-
if (!navigator.mediaDevices?.getSupportedConstraints) return {};
|
|
11389
|
-
try {
|
|
11390
|
-
return navigator.mediaDevices.getSupportedConstraints();
|
|
11391
|
-
} catch (_) {
|
|
11392
|
-
return {};
|
|
11393
|
-
}
|
|
11394
|
-
}
|
|
11395
|
-
getAspectRatio(settings) {
|
|
11396
|
-
if (typeof settings?.aspectRatio === "number" && settings.aspectRatio > 0) {
|
|
11397
|
-
return settings.aspectRatio;
|
|
11398
|
-
}
|
|
11399
|
-
if (settings?.width && settings?.height) {
|
|
11400
|
-
return settings.width / settings.height;
|
|
11401
|
-
}
|
|
11402
|
-
return null;
|
|
11403
|
-
}
|
|
11404
|
-
isLandscape(settings) {
|
|
11405
|
-
return (settings?.width || 0) > (settings?.height || 0);
|
|
11406
|
-
}
|
|
11407
|
-
isFrontLabel(label) {
|
|
11408
|
-
const l = (label || "").toLowerCase();
|
|
11409
|
-
return l.includes("front") || l.includes("selfie") || l.includes("user facing") || l.includes("facing front");
|
|
11410
|
-
}
|
|
11411
|
-
isRearLikeLabel(label) {
|
|
11412
|
-
const l = (label || "").toLowerCase();
|
|
11413
|
-
return l.includes("rear") || l.includes("back") || l.includes("environment") || l.includes("facing back") || l.includes("facing rear");
|
|
11414
|
-
}
|
|
11415
|
-
scoreTrack({
|
|
11416
|
-
settings,
|
|
11417
|
-
label
|
|
11418
|
-
}) {
|
|
11419
|
-
const width = settings?.width || 0;
|
|
11420
|
-
const height = settings?.height || 0;
|
|
11421
|
-
const pixels = width * height;
|
|
11422
|
-
const aspectRatio = this.getAspectRatio(settings) || 0;
|
|
11423
|
-
let score = pixels;
|
|
11424
|
-
if (width > height) score += 1_000_000_000;
|
|
11425
|
-
if (aspectRatio >= 1.3) score += 100_000_000;
|
|
11426
|
-
if (this.isRearLikeLabel(label)) score += 10_000_000;
|
|
11427
|
-
if (this.isFrontLabel(label)) score -= 10_000_000_000;
|
|
11428
|
-
return score;
|
|
11429
|
-
}
|
|
11430
|
-
buildVideoConstraints({
|
|
11431
|
-
width,
|
|
11432
|
-
height,
|
|
11433
|
-
mode,
|
|
11434
|
-
deviceId
|
|
11435
|
-
}) {
|
|
11436
|
-
const supported = this.getSupportedConstraintsSafe();
|
|
11437
|
-
const video = {};
|
|
11438
|
-
if (deviceId) {
|
|
11439
|
-
video.deviceId = {
|
|
11440
|
-
exact: deviceId
|
|
11441
|
-
};
|
|
11442
|
-
} else if (supported.facingMode) {
|
|
11443
|
-
if (typeof mode === "string") {
|
|
11444
|
-
video.facingMode = {
|
|
11445
|
-
ideal: mode
|
|
11446
|
-
};
|
|
11447
|
-
} else if (mode && typeof mode === "object") {
|
|
11448
|
-
video.facingMode = mode;
|
|
11449
|
-
} else {
|
|
11450
|
-
video.facingMode = {
|
|
11451
|
-
ideal: "environment"
|
|
11452
|
-
};
|
|
11453
|
-
}
|
|
11454
|
-
}
|
|
11455
|
-
if (supported.width) {
|
|
11456
|
-
video.width = {
|
|
11457
|
-
ideal: width
|
|
11458
|
-
};
|
|
11459
|
-
}
|
|
11460
|
-
if (supported.height) {
|
|
11461
|
-
video.height = {
|
|
11462
|
-
ideal: height
|
|
11463
|
-
};
|
|
11464
|
-
}
|
|
11465
|
-
if (supported.aspectRatio) {
|
|
11466
|
-
video.aspectRatio = {
|
|
11467
|
-
ideal: width / height
|
|
11468
|
-
};
|
|
11469
|
-
}
|
|
11470
|
-
return {
|
|
11471
|
-
video,
|
|
11472
|
-
audio: false
|
|
11473
|
-
};
|
|
11474
|
-
}
|
|
11475
|
-
async pauseBetweenCameraOps(ms = 180) {
|
|
11476
|
-
await new Promise(resolve => setTimeout(resolve, ms));
|
|
11477
|
-
}
|
|
11478
|
-
async stopMediaStream(stream) {
|
|
11479
|
-
if (!stream) return;
|
|
11480
|
-
try {
|
|
11481
|
-
stream.getTracks().forEach(t => t.stop());
|
|
11482
|
-
} catch (_) {}
|
|
11483
|
-
await this.pauseBetweenCameraOps();
|
|
11484
|
-
}
|
|
11485
|
-
async tryEnableDocumentFocus(stream) {
|
|
11486
|
-
const track = stream?.getVideoTracks?.()[0];
|
|
11487
|
-
if (!track) {
|
|
11488
|
-
return {
|
|
11489
|
-
ok: false,
|
|
11490
|
-
reason: "No video track found"
|
|
11491
|
-
};
|
|
11492
|
-
}
|
|
11493
|
-
if (!track.getCapabilities || !track.applyConstraints) {
|
|
11494
|
-
return {
|
|
11495
|
-
ok: false,
|
|
11496
|
-
reason: "Focus APIs unavailable on this browser/device",
|
|
11497
|
-
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11498
|
-
};
|
|
11499
|
-
}
|
|
11500
|
-
const caps = track.getCapabilities();
|
|
11501
|
-
const result = {
|
|
11502
|
-
ok: true,
|
|
11503
|
-
capabilities: caps,
|
|
11504
|
-
attempts: [],
|
|
11505
|
-
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11506
|
-
};
|
|
11507
|
-
try {
|
|
11508
|
-
let applied = false;
|
|
11509
|
-
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("continuous")) {
|
|
11510
|
-
await track.applyConstraints({
|
|
11511
|
-
advanced: [{
|
|
11512
|
-
focusMode: "continuous"
|
|
11513
|
-
}]
|
|
11514
|
-
});
|
|
11515
|
-
result.attempts.push("Applied focusMode=continuous");
|
|
11516
|
-
applied = true;
|
|
11517
|
-
}
|
|
11518
|
-
if (caps.focusDistance) {
|
|
11519
|
-
const min = caps.focusDistance.min;
|
|
11520
|
-
const max = caps.focusDistance.max;
|
|
11521
|
-
const docDistance = min + (max - min) * 0.30;
|
|
11522
|
-
const advanced = [];
|
|
11523
|
-
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("manual")) {
|
|
11524
|
-
advanced.push({
|
|
11525
|
-
focusMode: "manual"
|
|
11526
|
-
});
|
|
11527
|
-
}
|
|
11528
|
-
advanced.push({
|
|
11529
|
-
focusDistance: docDistance
|
|
11530
|
-
});
|
|
11531
|
-
await track.applyConstraints({
|
|
11532
|
-
advanced
|
|
11533
|
-
});
|
|
11534
|
-
result.attempts.push(`Applied focusDistance=${docDistance}`);
|
|
11535
|
-
applied = true;
|
|
11536
|
-
}
|
|
11537
|
-
if (!applied) {
|
|
11538
|
-
result.ok = false;
|
|
11539
|
-
result.reason = "No supported focus controls were exposed";
|
|
11540
|
-
}
|
|
11541
|
-
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11542
|
-
return result;
|
|
11543
|
-
} catch (err) {
|
|
11544
|
-
result.ok = false;
|
|
11545
|
-
result.error = err.message || String(err);
|
|
11546
|
-
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11547
|
-
return result;
|
|
11548
|
-
}
|
|
11549
|
-
}
|
|
11550
|
-
async openCandidateStream({
|
|
11551
|
-
mode,
|
|
11552
|
-
deviceId,
|
|
11553
|
-
width,
|
|
11554
|
-
height
|
|
11555
|
-
}) {
|
|
11556
|
-
return navigator.mediaDevices.getUserMedia(this.buildVideoConstraints({
|
|
11557
|
-
width,
|
|
11558
|
-
height,
|
|
11559
|
-
mode,
|
|
11560
|
-
deviceId
|
|
11561
|
-
}));
|
|
11562
|
-
}
|
|
11563
|
-
async testDeviceAtPreferredResolutions(device, mode) {
|
|
11564
|
-
const attempts = [];
|
|
11565
|
-
for (const size of ExtractorVideo.VIDEO_CANDIDATE_SETTINGS) {
|
|
11566
|
-
let stream = null;
|
|
11567
|
-
try {
|
|
11568
|
-
stream = await this.openCandidateStream({
|
|
11569
|
-
mode,
|
|
11570
|
-
deviceId: device?.deviceId,
|
|
11571
|
-
width: size.width,
|
|
11572
|
-
height: size.height
|
|
11573
|
-
});
|
|
11574
|
-
const track = stream.getVideoTracks()[0];
|
|
11575
|
-
const settings = track.getSettings();
|
|
11576
|
-
const label = track.label || device?.label || "";
|
|
11577
|
-
const aspectRatio = this.getAspectRatio(settings);
|
|
11578
|
-
const result = {
|
|
11579
|
-
stream,
|
|
11580
|
-
label,
|
|
11581
|
-
settings,
|
|
11582
|
-
aspectRatio,
|
|
11583
|
-
landscape: this.isLandscape(settings),
|
|
11584
|
-
score: this.scoreTrack({
|
|
11585
|
-
settings,
|
|
11586
|
-
label
|
|
11587
|
-
}),
|
|
11588
|
-
requested: size
|
|
11589
|
-
};
|
|
11590
|
-
attempts.push({
|
|
11591
|
-
ok: true,
|
|
11592
|
-
requested: size,
|
|
11593
|
-
returnedWidth: settings.width,
|
|
11594
|
-
returnedHeight: settings.height,
|
|
11595
|
-
aspectRatio,
|
|
11596
|
-
label
|
|
11597
|
-
});
|
|
11598
|
-
return {
|
|
11599
|
-
result,
|
|
11600
|
-
attempts
|
|
11601
|
-
};
|
|
11602
|
-
} catch (err) {
|
|
11603
|
-
attempts.push({
|
|
11604
|
-
ok: false,
|
|
11605
|
-
requested: size,
|
|
11606
|
-
error: err.message || String(err)
|
|
11607
|
-
});
|
|
11608
|
-
await this.stopMediaStream(stream);
|
|
11609
|
-
}
|
|
11376
|
+
setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
|
|
11610
11377
|
}
|
|
11611
|
-
return {
|
|
11612
|
-
result: null,
|
|
11613
|
-
attempts
|
|
11614
|
-
};
|
|
11615
|
-
}
|
|
11616
|
-
async pickBestPhoneCamera(mode = "environment") {
|
|
11617
|
-
const debug = {
|
|
11618
|
-
mode,
|
|
11619
|
-
tested: []
|
|
11620
|
-
};
|
|
11621
|
-
|
|
11622
|
-
// First try a warmup stream to unlock labels
|
|
11623
|
-
let warmupStream = null;
|
|
11624
|
-
let warmupResult = null;
|
|
11625
|
-
try {
|
|
11626
|
-
const warmupSize = ExtractorVideo.VIDEO_CANDIDATE_SETTINGS[2]; // 1280x720
|
|
11627
|
-
warmupStream = await this.openCandidateStream({
|
|
11628
|
-
mode,
|
|
11629
|
-
width: warmupSize.width,
|
|
11630
|
-
height: warmupSize.height
|
|
11631
|
-
});
|
|
11632
|
-
const track = warmupStream.getVideoTracks()[0];
|
|
11633
|
-
const settings = track.getSettings();
|
|
11634
|
-
const label = track.label || "";
|
|
11635
|
-
warmupResult = {
|
|
11636
|
-
stream: warmupStream,
|
|
11637
|
-
label,
|
|
11638
|
-
settings,
|
|
11639
|
-
aspectRatio: this.getAspectRatio(settings),
|
|
11640
|
-
landscape: this.isLandscape(settings),
|
|
11641
|
-
score: this.scoreTrack({
|
|
11642
|
-
settings,
|
|
11643
|
-
label
|
|
11644
|
-
}),
|
|
11645
|
-
requested: warmupSize
|
|
11646
|
-
};
|
|
11647
|
-
} catch (_) {
|
|
11648
|
-
warmupStream = null;
|
|
11649
|
-
}
|
|
11650
|
-
let devices = [];
|
|
11651
|
-
try {
|
|
11652
|
-
devices = await navigator.mediaDevices.enumerateDevices();
|
|
11653
|
-
} catch (_) {
|
|
11654
|
-
devices = [];
|
|
11655
|
-
}
|
|
11656
|
-
const videoInputs = devices.filter(d => d.kind === "videoinput");
|
|
11657
|
-
const candidateDevices = mode === "environment" ? videoInputs.filter(d => !this.isFrontLabel(d.label)) : videoInputs;
|
|
11658
|
-
if (warmupStream) {
|
|
11659
|
-
await this.stopMediaStream(warmupStream);
|
|
11660
|
-
warmupStream = null;
|
|
11661
|
-
}
|
|
11662
|
-
const usableResults = [];
|
|
11663
|
-
for (let i = 0; i < candidateDevices.length; i += 1) {
|
|
11664
|
-
const device = candidateDevices[i];
|
|
11665
|
-
const {
|
|
11666
|
-
result,
|
|
11667
|
-
attempts
|
|
11668
|
-
} = await this.testDeviceAtPreferredResolutions(device, mode);
|
|
11669
|
-
debug.tested.push({
|
|
11670
|
-
deviceId: device.deviceId,
|
|
11671
|
-
label: device.label,
|
|
11672
|
-
attempts,
|
|
11673
|
-
final: result ? {
|
|
11674
|
-
label: result.label,
|
|
11675
|
-
settings: result.settings,
|
|
11676
|
-
aspectRatio: result.aspectRatio,
|
|
11677
|
-
landscape: result.landscape,
|
|
11678
|
-
score: result.score,
|
|
11679
|
-
requested: result.requested
|
|
11680
|
-
} : null
|
|
11681
|
-
});
|
|
11682
|
-
if (result) {
|
|
11683
|
-
const frontRejected = mode === "environment" && this.isFrontLabel(result.label);
|
|
11684
|
-
const geometryOk = result.landscape;
|
|
11685
|
-
if (!frontRejected && geometryOk) {
|
|
11686
|
-
usableResults.push(result);
|
|
11687
|
-
} else {
|
|
11688
|
-
await this.stopMediaStream(result.stream);
|
|
11689
|
-
}
|
|
11690
|
-
}
|
|
11691
|
-
}
|
|
11692
|
-
if (usableResults.length > 0) {
|
|
11693
|
-
usableResults.sort((a, b) => b.score - a.score);
|
|
11694
|
-
const chosen = usableResults[0];
|
|
11695
|
-
for (const candidate of usableResults) {
|
|
11696
|
-
if (candidate !== chosen) {
|
|
11697
|
-
await this.stopMediaStream(candidate.stream);
|
|
11698
|
-
}
|
|
11699
|
-
}
|
|
11700
|
-
debug.chosen = {
|
|
11701
|
-
label: chosen.label,
|
|
11702
|
-
settings: chosen.settings,
|
|
11703
|
-
aspectRatio: chosen.aspectRatio,
|
|
11704
|
-
requested: chosen.requested,
|
|
11705
|
-
source: "device-test"
|
|
11706
|
-
};
|
|
11707
|
-
return {
|
|
11708
|
-
chosen,
|
|
11709
|
-
debug
|
|
11710
|
-
};
|
|
11711
|
-
}
|
|
11712
|
-
|
|
11713
|
-
// Final fallback: simple facingMode-based opens in descending priority
|
|
11714
|
-
for (const size of ExtractorVideo.VIDEO_CANDIDATE_SETTINGS) {
|
|
11715
|
-
try {
|
|
11716
|
-
const stream = await this.openCandidateStream({
|
|
11717
|
-
mode,
|
|
11718
|
-
width: size.width,
|
|
11719
|
-
height: size.height
|
|
11720
|
-
});
|
|
11721
|
-
const track = stream.getVideoTracks()[0];
|
|
11722
|
-
const settings = track.getSettings();
|
|
11723
|
-
const label = track.label || "";
|
|
11724
|
-
const aspectRatio = this.getAspectRatio(settings);
|
|
11725
|
-
const landscape = this.isLandscape(settings);
|
|
11726
|
-
if (mode === "environment" && !landscape) {
|
|
11727
|
-
await this.stopMediaStream(stream);
|
|
11728
|
-
continue;
|
|
11729
|
-
}
|
|
11730
|
-
const chosen = {
|
|
11731
|
-
stream,
|
|
11732
|
-
label,
|
|
11733
|
-
settings,
|
|
11734
|
-
aspectRatio,
|
|
11735
|
-
landscape,
|
|
11736
|
-
score: this.scoreTrack({
|
|
11737
|
-
settings,
|
|
11738
|
-
label
|
|
11739
|
-
}),
|
|
11740
|
-
requested: size
|
|
11741
|
-
};
|
|
11742
|
-
debug.chosen = {
|
|
11743
|
-
label,
|
|
11744
|
-
settings,
|
|
11745
|
-
aspectRatio,
|
|
11746
|
-
requested: size,
|
|
11747
|
-
source: "fallback-facingMode"
|
|
11748
|
-
};
|
|
11749
|
-
return {
|
|
11750
|
-
chosen,
|
|
11751
|
-
debug
|
|
11752
|
-
};
|
|
11753
|
-
} catch (_) {}
|
|
11754
|
-
}
|
|
11755
|
-
throw new Error("Could not start a suitable camera.");
|
|
11756
11378
|
}
|
|
11757
11379
|
async startVideo() {
|
|
11758
11380
|
try {
|
|
@@ -11762,29 +11384,49 @@ class ExtractorVideo {
|
|
|
11762
11384
|
if (!videoElem) {
|
|
11763
11385
|
throw new Error("Video element not found.");
|
|
11764
11386
|
}
|
|
11387
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
11388
|
+
throw new Error("getUserMedia is not available");
|
|
11389
|
+
}
|
|
11390
|
+
if (!window.isSecureContext) {
|
|
11391
|
+
throw new Error("Camera requires HTTPS / secure context");
|
|
11392
|
+
}
|
|
11765
11393
|
this.video = videoElem;
|
|
11766
11394
|
this.isRunning = true;
|
|
11767
11395
|
this.scanStartTime = Date.now();
|
|
11768
11396
|
const cfgValues = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfigValues)();
|
|
11769
11397
|
const mode = cfgValues.VIDEO_FACING_MODE ?? "environment";
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
this.video.srcObject = chosen.stream;
|
|
11775
|
-
await this.video.play().catch(e => {
|
|
11776
|
-
console.warn(`Error on video play: ${e}`);
|
|
11398
|
+
await this.stopCurrentStream();
|
|
11399
|
+
const cameraResult = await openBestDocumentCamera({
|
|
11400
|
+
videoElement: this.video,
|
|
11401
|
+
preferredFacingMode: mode
|
|
11777
11402
|
});
|
|
11778
|
-
this.
|
|
11779
|
-
this.
|
|
11780
|
-
|
|
11781
|
-
this.
|
|
11782
|
-
|
|
11783
|
-
|
|
11403
|
+
this.currentStream = cameraResult.stream;
|
|
11404
|
+
this.lastCameraInfo = cameraResult;
|
|
11405
|
+
try {
|
|
11406
|
+
const focusResult = await tryEnableDocumentFocus(this.currentStream);
|
|
11407
|
+
console.log("Focus result:", focusResult);
|
|
11408
|
+
} catch (focusError) {
|
|
11409
|
+
console.warn("Could not apply focus settings:", focusError);
|
|
11410
|
+
}
|
|
11411
|
+
if (!this._boundLoadedDataHandler) {
|
|
11412
|
+
this._boundLoadedDataHandler = () => this.adjustOverlayPosition();
|
|
11413
|
+
this.video.addEventListener("loadeddata", this._boundLoadedDataHandler);
|
|
11414
|
+
}
|
|
11415
|
+
if (!this._boundResizeHandler) {
|
|
11416
|
+
this._boundResizeHandler = () => this.adjustOverlayPosition();
|
|
11417
|
+
window.addEventListener("resize", this._boundResizeHandler);
|
|
11418
|
+
}
|
|
11784
11419
|
setTimeout(() => this.adjustOverlayPosition(), 500);
|
|
11785
11420
|
this.scanStartTime = Date.now();
|
|
11786
11421
|
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
11787
11422
|
this.showMessage("Starting scanning");
|
|
11423
|
+
console.log("Chosen camera:", {
|
|
11424
|
+
label: cameraResult.label,
|
|
11425
|
+
deviceId: cameraResult.deviceId,
|
|
11426
|
+
strategy: cameraResult.strategy,
|
|
11427
|
+
settings: cameraResult.settings,
|
|
11428
|
+
score: cameraResult.score
|
|
11429
|
+
});
|
|
11788
11430
|
return true;
|
|
11789
11431
|
} catch (error) {
|
|
11790
11432
|
console.error("startVideo failed:", error);
|
|
@@ -11798,26 +11440,47 @@ class ExtractorVideo {
|
|
|
11798
11440
|
return false;
|
|
11799
11441
|
}
|
|
11800
11442
|
}
|
|
11443
|
+
async stopCurrentStream() {
|
|
11444
|
+
await stopStream(this.currentStream);
|
|
11445
|
+
this.currentStream = null;
|
|
11446
|
+
if (this.video) {
|
|
11447
|
+
this.video.srcObject = null;
|
|
11448
|
+
}
|
|
11449
|
+
await delay(200);
|
|
11450
|
+
}
|
|
11801
11451
|
stopVideo() {
|
|
11802
11452
|
this.isRunning = false;
|
|
11803
11453
|
if (this.video) {
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11454
|
+
try {
|
|
11455
|
+
this.video.pause();
|
|
11456
|
+
} catch (_) {}
|
|
11457
|
+
if (this.video.srcObject) {
|
|
11458
|
+
try {
|
|
11459
|
+
this.video.srcObject.getTracks().forEach(t => t.stop());
|
|
11460
|
+
} catch (_) {}
|
|
11807
11461
|
}
|
|
11808
11462
|
this.video.srcObject = null;
|
|
11809
11463
|
}
|
|
11464
|
+
if (this.currentStream) {
|
|
11465
|
+
try {
|
|
11466
|
+
this.currentStream.getTracks().forEach(t => t.stop());
|
|
11467
|
+
} catch (_) {}
|
|
11468
|
+
}
|
|
11469
|
+
this.currentStream = null;
|
|
11810
11470
|
clearTimeout(this.turnMessageTimer);
|
|
11811
11471
|
this.turnMessageTimer = null;
|
|
11812
11472
|
}
|
|
11813
11473
|
adjustOverlayPosition() {
|
|
11814
11474
|
const video = this.video;
|
|
11815
11475
|
const dot = document.querySelector(".centerGuideDot");
|
|
11816
|
-
if (!video || !dot || video.videoWidth === 0 || video.videoHeight === 0)
|
|
11476
|
+
if (!video || !dot || video.videoWidth === 0 || video.videoHeight === 0) {
|
|
11477
|
+
return;
|
|
11478
|
+
}
|
|
11817
11479
|
const videoRatio = video.videoWidth / video.videoHeight;
|
|
11818
11480
|
const container = video.parentElement;
|
|
11819
11481
|
const containerRatio = container.clientWidth / container.clientHeight;
|
|
11820
|
-
let scaleWidth
|
|
11482
|
+
let scaleWidth;
|
|
11483
|
+
let scaleHeight;
|
|
11821
11484
|
if (videoRatio > containerRatio) {
|
|
11822
11485
|
scaleWidth = container.clientWidth;
|
|
11823
11486
|
scaleHeight = container.clientWidth / videoRatio;
|
|
@@ -11901,87 +11564,86 @@ class ExtractorVideo {
|
|
|
11901
11564
|
}
|
|
11902
11565
|
</style>
|
|
11903
11566
|
`;
|
|
11904
|
-
} else {
|
|
11905
|
-
return `
|
|
11906
|
-
<div class="desktopFeedback" id="ScanDocAIMessage"></div>
|
|
11907
|
-
<div class="desktopVideoArea">
|
|
11908
|
-
<div class="desktopVideoHolder">
|
|
11909
|
-
<video id="ScanDocAIVideoElement" class="desktopVideo" autoplay muted playsinline></video>
|
|
11910
|
-
<div class="backgroundOverlay"></div>
|
|
11911
|
-
<div class="centerGuideDot"></div>
|
|
11912
|
-
</div>
|
|
11913
|
-
</div>
|
|
11914
|
-
|
|
11915
|
-
<style>
|
|
11916
|
-
.desktopVideoArea {
|
|
11917
|
-
display: flex;
|
|
11918
|
-
flex-direction: column;
|
|
11919
|
-
gap: 8px;
|
|
11920
|
-
padding: 16px;
|
|
11921
|
-
background-color: rgba(255, 255, 255, 0.05);
|
|
11922
|
-
border: 1px solid ${borderColor};
|
|
11923
|
-
border-radius: 15px;
|
|
11924
|
-
margin-left: 20%;
|
|
11925
|
-
margin-right: 20%;
|
|
11926
|
-
}
|
|
11927
|
-
|
|
11928
|
-
.desktopVideoHolder {
|
|
11929
|
-
position: relative;
|
|
11930
|
-
width: 100%;
|
|
11931
|
-
overflow: hidden;
|
|
11932
|
-
}
|
|
11933
|
-
|
|
11934
|
-
.desktopVideo {
|
|
11935
|
-
width: 100%;
|
|
11936
|
-
height: auto;
|
|
11937
|
-
margin-left: auto;
|
|
11938
|
-
margin-right: auto;
|
|
11939
|
-
max-width: 100vw;
|
|
11940
|
-
max-height: 100vh;
|
|
11941
|
-
}
|
|
11942
|
-
|
|
11943
|
-
.centerGuideDot {
|
|
11944
|
-
position: absolute;
|
|
11945
|
-
width: 16px;
|
|
11946
|
-
height: 16px;
|
|
11947
|
-
background-color: #00ff55;
|
|
11948
|
-
border-radius: 50%;
|
|
11949
|
-
box-shadow: 0 0 8px 2px rgba(0, 255, 100, 0.8);
|
|
11950
|
-
z-index: 4;
|
|
11951
|
-
animation: flicker 1s infinite;
|
|
11952
|
-
pointer-events: none;
|
|
11953
|
-
display: none;
|
|
11954
|
-
}
|
|
11955
|
-
|
|
11956
|
-
@keyframes flicker {
|
|
11957
|
-
0%, 100% { opacity: 1; }
|
|
11958
|
-
50% { opacity: 0.3; }
|
|
11959
|
-
}
|
|
11960
|
-
|
|
11961
|
-
.backgroundOverlay {
|
|
11962
|
-
position: absolute;
|
|
11963
|
-
top: 0;
|
|
11964
|
-
left: 0;
|
|
11965
|
-
width: 100%;
|
|
11966
|
-
height: 100%;
|
|
11967
|
-
background-color: rgba(0, 0, 0, 0.051);
|
|
11968
|
-
z-index: 2;
|
|
11969
|
-
pointer-events: none;
|
|
11970
|
-
}
|
|
11971
|
-
|
|
11972
|
-
.desktopFeedback {
|
|
11973
|
-
position: relative;
|
|
11974
|
-
display: flex;
|
|
11975
|
-
font-size: 22px;
|
|
11976
|
-
font-weight: 600;
|
|
11977
|
-
min-height: 36px;
|
|
11978
|
-
justify-content: center;
|
|
11979
|
-
align-items: center;
|
|
11980
|
-
color: ${messageColor};
|
|
11981
|
-
}
|
|
11982
|
-
</style>
|
|
11983
|
-
`;
|
|
11984
11567
|
}
|
|
11568
|
+
return `
|
|
11569
|
+
<div class="desktopFeedback" id="ScanDocAIMessage"></div>
|
|
11570
|
+
<div class="desktopVideoArea">
|
|
11571
|
+
<div class="desktopVideoHolder">
|
|
11572
|
+
<video id="ScanDocAIVideoElement" class="desktopVideo" autoplay muted playsinline></video>
|
|
11573
|
+
<div class="backgroundOverlay"></div>
|
|
11574
|
+
<div class="centerGuideDot"></div>
|
|
11575
|
+
</div>
|
|
11576
|
+
</div>
|
|
11577
|
+
|
|
11578
|
+
<style>
|
|
11579
|
+
.desktopVideoArea {
|
|
11580
|
+
display: flex;
|
|
11581
|
+
flex-direction: column;
|
|
11582
|
+
gap: 8px;
|
|
11583
|
+
padding: 16px;
|
|
11584
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
11585
|
+
border: 1px solid ${borderColor};
|
|
11586
|
+
border-radius: 15px;
|
|
11587
|
+
margin-left: 20%;
|
|
11588
|
+
margin-right: 20%;
|
|
11589
|
+
}
|
|
11590
|
+
|
|
11591
|
+
.desktopVideoHolder {
|
|
11592
|
+
position: relative;
|
|
11593
|
+
width: 100%;
|
|
11594
|
+
overflow: hidden;
|
|
11595
|
+
}
|
|
11596
|
+
|
|
11597
|
+
.desktopVideo {
|
|
11598
|
+
width: 100%;
|
|
11599
|
+
height: auto;
|
|
11600
|
+
margin-left: auto;
|
|
11601
|
+
margin-right: auto;
|
|
11602
|
+
max-width: 100vw;
|
|
11603
|
+
max-height: 100vh;
|
|
11604
|
+
}
|
|
11605
|
+
|
|
11606
|
+
.centerGuideDot {
|
|
11607
|
+
position: absolute;
|
|
11608
|
+
width: 16px;
|
|
11609
|
+
height: 16px;
|
|
11610
|
+
background-color: #00ff55;
|
|
11611
|
+
border-radius: 50%;
|
|
11612
|
+
box-shadow: 0 0 8px 2px rgba(0, 255, 100, 0.8);
|
|
11613
|
+
z-index: 4;
|
|
11614
|
+
animation: flicker 1s infinite;
|
|
11615
|
+
pointer-events: none;
|
|
11616
|
+
display: none;
|
|
11617
|
+
}
|
|
11618
|
+
|
|
11619
|
+
@keyframes flicker {
|
|
11620
|
+
0%, 100% { opacity: 1; }
|
|
11621
|
+
50% { opacity: 0.3; }
|
|
11622
|
+
}
|
|
11623
|
+
|
|
11624
|
+
.backgroundOverlay {
|
|
11625
|
+
position: absolute;
|
|
11626
|
+
top: 0;
|
|
11627
|
+
left: 0;
|
|
11628
|
+
width: 100%;
|
|
11629
|
+
height: 100%;
|
|
11630
|
+
background-color: rgba(0, 0, 0, 0.051);
|
|
11631
|
+
z-index: 2;
|
|
11632
|
+
pointer-events: none;
|
|
11633
|
+
}
|
|
11634
|
+
|
|
11635
|
+
.desktopFeedback {
|
|
11636
|
+
position: relative;
|
|
11637
|
+
display: flex;
|
|
11638
|
+
font-size: 22px;
|
|
11639
|
+
font-weight: 600;
|
|
11640
|
+
min-height: 36px;
|
|
11641
|
+
justify-content: center;
|
|
11642
|
+
align-items: center;
|
|
11643
|
+
color: ${messageColor};
|
|
11644
|
+
}
|
|
11645
|
+
</style>
|
|
11646
|
+
`;
|
|
11985
11647
|
}
|
|
11986
11648
|
}
|
|
11987
11649
|
let EXTRACTION_VIDEO = undefined;
|
|
@@ -12011,6 +11673,336 @@ function getExtractionVideo(tokenOrCallback, maybeCallback) {
|
|
|
12011
11673
|
}
|
|
12012
11674
|
return EXTRACTION_VIDEO;
|
|
12013
11675
|
}
|
|
11676
|
+
function delay(ms) {
|
|
11677
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
11678
|
+
}
|
|
11679
|
+
async function stopStream(stream) {
|
|
11680
|
+
if (!stream) return;
|
|
11681
|
+
for (const track of stream.getTracks()) {
|
|
11682
|
+
try {
|
|
11683
|
+
track.stop();
|
|
11684
|
+
} catch (_) {}
|
|
11685
|
+
}
|
|
11686
|
+
}
|
|
11687
|
+
function getAspectRatio(settings = {}) {
|
|
11688
|
+
if (typeof settings.aspectRatio === "number" && settings.aspectRatio > 0) {
|
|
11689
|
+
return settings.aspectRatio;
|
|
11690
|
+
}
|
|
11691
|
+
if (settings.width && settings.height) {
|
|
11692
|
+
return settings.width / settings.height;
|
|
11693
|
+
}
|
|
11694
|
+
return null;
|
|
11695
|
+
}
|
|
11696
|
+
async function attachStreamToVideo(videoEl, stream) {
|
|
11697
|
+
videoEl.srcObject = stream;
|
|
11698
|
+
try {
|
|
11699
|
+
await videoEl.play();
|
|
11700
|
+
} catch (_) {}
|
|
11701
|
+
}
|
|
11702
|
+
function getSupportedConstraintsSafe() {
|
|
11703
|
+
try {
|
|
11704
|
+
return navigator.mediaDevices?.getSupportedConstraints?.() || {};
|
|
11705
|
+
} catch (_) {
|
|
11706
|
+
return {};
|
|
11707
|
+
}
|
|
11708
|
+
}
|
|
11709
|
+
function buildDocumentScanConstraints(mode = "environment") {
|
|
11710
|
+
const supported = getSupportedConstraintsSafe();
|
|
11711
|
+
const video = {};
|
|
11712
|
+
if (supported.facingMode) {
|
|
11713
|
+
video.facingMode = typeof mode === "string" ? {
|
|
11714
|
+
ideal: mode
|
|
11715
|
+
} : mode || {
|
|
11716
|
+
ideal: "environment"
|
|
11717
|
+
};
|
|
11718
|
+
}
|
|
11719
|
+
if (supported.width) {
|
|
11720
|
+
video.width = {
|
|
11721
|
+
ideal: 1920
|
|
11722
|
+
};
|
|
11723
|
+
}
|
|
11724
|
+
if (supported.height) {
|
|
11725
|
+
video.height = {
|
|
11726
|
+
ideal: 1080
|
|
11727
|
+
};
|
|
11728
|
+
}
|
|
11729
|
+
if (supported.aspectRatio) {
|
|
11730
|
+
video.aspectRatio = {
|
|
11731
|
+
ideal: 16 / 9
|
|
11732
|
+
};
|
|
11733
|
+
}
|
|
11734
|
+
return {
|
|
11735
|
+
video,
|
|
11736
|
+
audio: false
|
|
11737
|
+
};
|
|
11738
|
+
}
|
|
11739
|
+
function buildDeviceConstraints(deviceId) {
|
|
11740
|
+
const supported = getSupportedConstraintsSafe();
|
|
11741
|
+
const video = {
|
|
11742
|
+
deviceId: {
|
|
11743
|
+
exact: deviceId
|
|
11744
|
+
}
|
|
11745
|
+
};
|
|
11746
|
+
if (supported.width) {
|
|
11747
|
+
video.width = {
|
|
11748
|
+
ideal: 1920
|
|
11749
|
+
};
|
|
11750
|
+
}
|
|
11751
|
+
if (supported.height) {
|
|
11752
|
+
video.height = {
|
|
11753
|
+
ideal: 1080
|
|
11754
|
+
};
|
|
11755
|
+
}
|
|
11756
|
+
if (supported.aspectRatio) {
|
|
11757
|
+
video.aspectRatio = {
|
|
11758
|
+
ideal: 16 / 9
|
|
11759
|
+
};
|
|
11760
|
+
}
|
|
11761
|
+
return {
|
|
11762
|
+
video,
|
|
11763
|
+
audio: false
|
|
11764
|
+
};
|
|
11765
|
+
}
|
|
11766
|
+
async function enumerateVideoInputDevices() {
|
|
11767
|
+
if (!navigator.mediaDevices?.enumerateDevices) return [];
|
|
11768
|
+
try {
|
|
11769
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
11770
|
+
return devices.filter(d => d.kind === "videoinput");
|
|
11771
|
+
} catch (error) {
|
|
11772
|
+
console.warn("enumerateDevices failed:", error);
|
|
11773
|
+
return [];
|
|
11774
|
+
}
|
|
11775
|
+
}
|
|
11776
|
+
function normalizeLabel(label = "") {
|
|
11777
|
+
return String(label).trim().toLowerCase();
|
|
11778
|
+
}
|
|
11779
|
+
function isLikelyRearCamera(label = "") {
|
|
11780
|
+
const v = normalizeLabel(label);
|
|
11781
|
+
return v.includes("back") || v.includes("rear") || v.includes("environment") || v.includes("world");
|
|
11782
|
+
}
|
|
11783
|
+
function isLikelyUltraWideOrMacro(label = "") {
|
|
11784
|
+
const v = normalizeLabel(label);
|
|
11785
|
+
return v.includes("ultra") || v.includes("ultrawide") || v.includes("wide angle") || v.includes("0.5") || v.includes("macro") || v.includes("fisheye");
|
|
11786
|
+
}
|
|
11787
|
+
function scoreCameraCandidate({
|
|
11788
|
+
label,
|
|
11789
|
+
settings
|
|
11790
|
+
}) {
|
|
11791
|
+
const width = Number(settings?.width || 0);
|
|
11792
|
+
const height = Number(settings?.height || 0);
|
|
11793
|
+
const pixels = width * height;
|
|
11794
|
+
const aspectRatio = getAspectRatio(settings) || 0;
|
|
11795
|
+
let score = 0;
|
|
11796
|
+
score += pixels;
|
|
11797
|
+
if (isLikelyRearCamera(label)) {
|
|
11798
|
+
score += 5_000_000;
|
|
11799
|
+
}
|
|
11800
|
+
if (isLikelyUltraWideOrMacro(label)) {
|
|
11801
|
+
score -= 8_000_000;
|
|
11802
|
+
}
|
|
11803
|
+
if (aspectRatio > 1.65 && aspectRatio < 1.85) {
|
|
11804
|
+
score += 750_000;
|
|
11805
|
+
}
|
|
11806
|
+
if (aspectRatio > 2.1) {
|
|
11807
|
+
score -= 2_000_000;
|
|
11808
|
+
}
|
|
11809
|
+
if (width >= 1920) {
|
|
11810
|
+
score += 500_000;
|
|
11811
|
+
}
|
|
11812
|
+
if (width < 1280 || height < 720) {
|
|
11813
|
+
score -= 3_000_000;
|
|
11814
|
+
}
|
|
11815
|
+
return score;
|
|
11816
|
+
}
|
|
11817
|
+
async function probeCameraCandidate(device, videoElement) {
|
|
11818
|
+
let stream = null;
|
|
11819
|
+
try {
|
|
11820
|
+
stream = await navigator.mediaDevices.getUserMedia(buildDeviceConstraints(device.deviceId));
|
|
11821
|
+
await attachStreamToVideo(videoElement, stream);
|
|
11822
|
+
await waitForVideoReady(videoElement, 800);
|
|
11823
|
+
const track = stream.getVideoTracks?.()[0];
|
|
11824
|
+
const settings = track?.getSettings?.() || {};
|
|
11825
|
+
return {
|
|
11826
|
+
ok: true,
|
|
11827
|
+
device,
|
|
11828
|
+
stream,
|
|
11829
|
+
label: track?.label || device.label || "",
|
|
11830
|
+
deviceId: settings.deviceId || device.deviceId,
|
|
11831
|
+
settings,
|
|
11832
|
+
aspectRatio: getAspectRatio(settings),
|
|
11833
|
+
score: scoreCameraCandidate({
|
|
11834
|
+
label: track?.label || device.label || "",
|
|
11835
|
+
settings
|
|
11836
|
+
})
|
|
11837
|
+
};
|
|
11838
|
+
} catch (error) {
|
|
11839
|
+
if (stream) {
|
|
11840
|
+
await stopStream(stream);
|
|
11841
|
+
}
|
|
11842
|
+
return {
|
|
11843
|
+
ok: false,
|
|
11844
|
+
device,
|
|
11845
|
+
error,
|
|
11846
|
+
score: Number.NEGATIVE_INFINITY
|
|
11847
|
+
};
|
|
11848
|
+
}
|
|
11849
|
+
}
|
|
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
|
+
});
|
|
11874
|
+
}
|
|
11875
|
+
async function tryEnableDocumentFocus(stream) {
|
|
11876
|
+
const track = stream?.getVideoTracks?.()[0];
|
|
11877
|
+
if (!track) {
|
|
11878
|
+
return {
|
|
11879
|
+
ok: false,
|
|
11880
|
+
reason: "No video track found"
|
|
11881
|
+
};
|
|
11882
|
+
}
|
|
11883
|
+
if (!track.getCapabilities || !track.applyConstraints) {
|
|
11884
|
+
return {
|
|
11885
|
+
ok: false,
|
|
11886
|
+
reason: "Focus APIs unavailable on this browser/device",
|
|
11887
|
+
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11888
|
+
};
|
|
11889
|
+
}
|
|
11890
|
+
const caps = track.getCapabilities();
|
|
11891
|
+
const result = {
|
|
11892
|
+
ok: true,
|
|
11893
|
+
capabilities: caps,
|
|
11894
|
+
attempts: [],
|
|
11895
|
+
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11896
|
+
};
|
|
11897
|
+
try {
|
|
11898
|
+
let applied = false;
|
|
11899
|
+
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("continuous")) {
|
|
11900
|
+
await track.applyConstraints({
|
|
11901
|
+
advanced: [{
|
|
11902
|
+
focusMode: "continuous"
|
|
11903
|
+
}]
|
|
11904
|
+
});
|
|
11905
|
+
result.attempts.push("Applied focusMode=continuous");
|
|
11906
|
+
applied = true;
|
|
11907
|
+
}
|
|
11908
|
+
if (caps.focusDistance) {
|
|
11909
|
+
const min = caps.focusDistance.min ?? 0;
|
|
11910
|
+
const max = caps.focusDistance.max ?? 0;
|
|
11911
|
+
const docDistance = min + (max - min) * 0.3;
|
|
11912
|
+
const advanced = [];
|
|
11913
|
+
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("manual")) {
|
|
11914
|
+
advanced.push({
|
|
11915
|
+
focusMode: "manual"
|
|
11916
|
+
});
|
|
11917
|
+
}
|
|
11918
|
+
advanced.push({
|
|
11919
|
+
focusDistance: docDistance
|
|
11920
|
+
});
|
|
11921
|
+
await track.applyConstraints({
|
|
11922
|
+
advanced
|
|
11923
|
+
});
|
|
11924
|
+
result.attempts.push(`Applied focusDistance=${docDistance}`);
|
|
11925
|
+
applied = true;
|
|
11926
|
+
}
|
|
11927
|
+
if (!applied) {
|
|
11928
|
+
result.ok = false;
|
|
11929
|
+
result.reason = "No supported focus controls were exposed";
|
|
11930
|
+
}
|
|
11931
|
+
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11932
|
+
return result;
|
|
11933
|
+
} catch (err) {
|
|
11934
|
+
result.ok = false;
|
|
11935
|
+
result.error = err.message || String(err);
|
|
11936
|
+
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11937
|
+
return result;
|
|
11938
|
+
}
|
|
11939
|
+
}
|
|
11940
|
+
async function openBestDocumentCamera({
|
|
11941
|
+
videoElement,
|
|
11942
|
+
preferredFacingMode = "environment"
|
|
11943
|
+
}) {
|
|
11944
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
11945
|
+
throw new Error("getUserMedia is not available");
|
|
11946
|
+
}
|
|
11947
|
+
if (!window.isSecureContext) {
|
|
11948
|
+
throw new Error("Camera requires HTTPS / secure context");
|
|
11949
|
+
}
|
|
11950
|
+
let bootstrapStream = null;
|
|
11951
|
+
try {
|
|
11952
|
+
bootstrapStream = await navigator.mediaDevices.getUserMedia(buildDocumentScanConstraints(preferredFacingMode));
|
|
11953
|
+
await attachStreamToVideo(videoElement, bootstrapStream);
|
|
11954
|
+
await waitForVideoReady(videoElement, 800);
|
|
11955
|
+
} catch (preferredError) {
|
|
11956
|
+
console.warn("Preferred camera open failed, falling back:", preferredError);
|
|
11957
|
+
bootstrapStream = await navigator.mediaDevices.getUserMedia({
|
|
11958
|
+
video: true,
|
|
11959
|
+
audio: false
|
|
11960
|
+
});
|
|
11961
|
+
await attachStreamToVideo(videoElement, bootstrapStream);
|
|
11962
|
+
await waitForVideoReady(videoElement, 800);
|
|
11963
|
+
}
|
|
11964
|
+
const bootstrapTrack = bootstrapStream.getVideoTracks?.()[0];
|
|
11965
|
+
const bootstrapSettings = bootstrapTrack?.getSettings?.() || {};
|
|
11966
|
+
const bootstrapLabel = bootstrapTrack?.label || "";
|
|
11967
|
+
const bootstrapDeviceId = bootstrapSettings.deviceId || null;
|
|
11968
|
+
let best = {
|
|
11969
|
+
ok: true,
|
|
11970
|
+
device: null,
|
|
11971
|
+
stream: bootstrapStream,
|
|
11972
|
+
label: bootstrapLabel,
|
|
11973
|
+
deviceId: bootstrapDeviceId,
|
|
11974
|
+
settings: bootstrapSettings,
|
|
11975
|
+
aspectRatio: getAspectRatio(bootstrapSettings),
|
|
11976
|
+
score: scoreCameraCandidate({
|
|
11977
|
+
label: bootstrapLabel,
|
|
11978
|
+
settings: bootstrapSettings
|
|
11979
|
+
}),
|
|
11980
|
+
strategy: "bootstrap"
|
|
11981
|
+
};
|
|
11982
|
+
const devices = await enumerateVideoInputDevices();
|
|
11983
|
+
if (devices.length <= 1) {
|
|
11984
|
+
return best;
|
|
11985
|
+
}
|
|
11986
|
+
const rearDevices = devices.filter(d => isLikelyRearCamera(d.label));
|
|
11987
|
+
const candidates = rearDevices.length > 0 ? rearDevices : devices;
|
|
11988
|
+
for (const device of candidates) {
|
|
11989
|
+
if (!device.deviceId) continue;
|
|
11990
|
+
if (device.deviceId === bootstrapDeviceId) continue;
|
|
11991
|
+
const probed = await probeCameraCandidate(device, videoElement);
|
|
11992
|
+
if (!probed.ok) continue;
|
|
11993
|
+
if (probed.score > best.score) {
|
|
11994
|
+
await stopStream(best.stream);
|
|
11995
|
+
best = {
|
|
11996
|
+
...probed,
|
|
11997
|
+
strategy: "probed-better-camera"
|
|
11998
|
+
};
|
|
11999
|
+
} else {
|
|
12000
|
+
await stopStream(probed.stream);
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
12003
|
+
await attachStreamToVideo(videoElement, best.stream);
|
|
12004
|
+
return best;
|
|
12005
|
+
}
|
|
12014
12006
|
|
|
12015
12007
|
/***/ }),
|
|
12016
12008
|
|