scandoc-ai-components 0.1.18 → 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 +471 -619
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11170,21 +11170,10 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
11170
11170
|
|
|
11171
11171
|
const DOCUMENT_DETECTOR = new _ai_detect_document__WEBPACK_IMPORTED_MODULE_0__["default"]();
|
|
11172
11172
|
class ExtractorVideo {
|
|
11173
|
-
static DEBUG = true;
|
|
11174
11173
|
static FREQUENCY_MS = 10;
|
|
11175
11174
|
static VALIDATION_BATCH_SIZE = 1;
|
|
11176
11175
|
static VALIDATION_IMG_WIDTH = 384;
|
|
11177
11176
|
static VALIDATION_IMG_HEIGHT = 384;
|
|
11178
|
-
static VIDEO_CANDIDATE_SETTINGS = Object.freeze([{
|
|
11179
|
-
width: 1920,
|
|
11180
|
-
height: 1080
|
|
11181
|
-
}, {
|
|
11182
|
-
width: 1280,
|
|
11183
|
-
height: 960
|
|
11184
|
-
}, {
|
|
11185
|
-
width: 1280,
|
|
11186
|
-
height: 720
|
|
11187
|
-
}]);
|
|
11188
11177
|
constructor(onExtractedResults) {
|
|
11189
11178
|
this.candidateImages = [];
|
|
11190
11179
|
this.extractionImages = {};
|
|
@@ -11198,47 +11187,8 @@ class ExtractorVideo {
|
|
|
11198
11187
|
this.waitingForSecondSide = false;
|
|
11199
11188
|
this.hasShownTurnMessage = false;
|
|
11200
11189
|
this.turnMessageTimer = null;
|
|
11201
|
-
this.
|
|
11202
|
-
this.
|
|
11203
|
-
}
|
|
11204
|
-
logDebug(event, data = {}) {
|
|
11205
|
-
if (!ExtractorVideo.DEBUG) return;
|
|
11206
|
-
const entry = {
|
|
11207
|
-
ts: new Date().toISOString(),
|
|
11208
|
-
event,
|
|
11209
|
-
data
|
|
11210
|
-
};
|
|
11211
|
-
this.debugLog.push(entry);
|
|
11212
|
-
try {
|
|
11213
|
-
console.log("[ScanDocAI]", event, data);
|
|
11214
|
-
} catch (_) {}
|
|
11215
|
-
}
|
|
11216
|
-
getDebugDump() {
|
|
11217
|
-
return {
|
|
11218
|
-
userAgent: navigator.userAgent,
|
|
11219
|
-
secureContext: window.isSecureContext,
|
|
11220
|
-
location: window.location.href,
|
|
11221
|
-
videoPresent: !!this.video,
|
|
11222
|
-
videoReadyState: this.video?.readyState,
|
|
11223
|
-
videoNetworkState: this.video?.networkState,
|
|
11224
|
-
videoPaused: this.video?.paused,
|
|
11225
|
-
videoMuted: this.video?.muted,
|
|
11226
|
-
videoAutoplay: this.video?.autoplay,
|
|
11227
|
-
videoPlaysInline: this.video?.playsInline,
|
|
11228
|
-
videoDimensions: {
|
|
11229
|
-
videoWidth: this.video?.videoWidth || 0,
|
|
11230
|
-
videoHeight: this.video?.videoHeight || 0,
|
|
11231
|
-
clientWidth: this.video?.clientWidth || 0,
|
|
11232
|
-
clientHeight: this.video?.clientHeight || 0
|
|
11233
|
-
},
|
|
11234
|
-
streamTracks: this.video?.srcObject ? this.video.srcObject.getTracks().map(t => ({
|
|
11235
|
-
kind: t.kind,
|
|
11236
|
-
label: t.label,
|
|
11237
|
-
readyState: t.readyState,
|
|
11238
|
-
enabled: t.enabled
|
|
11239
|
-
})) : [],
|
|
11240
|
-
log: this.debugLog
|
|
11241
|
-
};
|
|
11190
|
+
this.currentStream = null;
|
|
11191
|
+
this.lastCameraInfo = null;
|
|
11242
11192
|
}
|
|
11243
11193
|
reset() {
|
|
11244
11194
|
this.stopVideo();
|
|
@@ -11254,18 +11204,15 @@ class ExtractorVideo {
|
|
|
11254
11204
|
this.lastMessage = null;
|
|
11255
11205
|
this.waitingForSecondSide = false;
|
|
11256
11206
|
this.hasShownTurnMessage = false;
|
|
11207
|
+
this.lastCameraInfo = null;
|
|
11257
11208
|
clearTimeout(this.turnMessageTimer);
|
|
11258
11209
|
this.turnMessageTimer = null;
|
|
11259
|
-
this.debugLog = [];
|
|
11260
11210
|
}
|
|
11261
11211
|
async analyzeVideoStream() {
|
|
11262
11212
|
if (!this.isRunning) return;
|
|
11263
11213
|
const now = Date.now();
|
|
11264
11214
|
const cfgValues = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfigValues)();
|
|
11265
11215
|
if (this.scanStartTime && now - this.scanStartTime > cfgValues.MAX_SCAN_DURATION_MS) {
|
|
11266
|
-
this.logDebug("scan_timeout", {
|
|
11267
|
-
elapsedMs: now - this.scanStartTime
|
|
11268
|
-
});
|
|
11269
11216
|
this.stopVideo();
|
|
11270
11217
|
this.onExtractedResults({
|
|
11271
11218
|
success: false,
|
|
@@ -11276,16 +11223,7 @@ class ExtractorVideo {
|
|
|
11276
11223
|
}
|
|
11277
11224
|
const canvas = document.createElement("canvas");
|
|
11278
11225
|
const video = document.getElementById("ScanDocAIVideoElement");
|
|
11279
|
-
if (
|
|
11280
|
-
this.logDebug("analyze_no_video_element");
|
|
11281
|
-
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
11282
|
-
return;
|
|
11283
|
-
}
|
|
11284
|
-
if (video.videoWidth < video.videoHeight) {
|
|
11285
|
-
this.logDebug("portrait_video_detected", {
|
|
11286
|
-
videoWidth: video.videoWidth,
|
|
11287
|
-
videoHeight: video.videoHeight
|
|
11288
|
-
});
|
|
11226
|
+
if (video && video.videoWidth < video.videoHeight) {
|
|
11289
11227
|
this.showMessage("Please rotate your device to landscape mode.");
|
|
11290
11228
|
this.candidateImages = [];
|
|
11291
11229
|
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
@@ -11304,9 +11242,6 @@ class ExtractorVideo {
|
|
|
11304
11242
|
const [isValidationOk, response] = await (0,_requests_validation__WEBPACK_IMPORTED_MODULE_3__["default"])(images.map(e => e.validationImg), this.pastBlurValues, {});
|
|
11305
11243
|
if (!isValidationOk) {
|
|
11306
11244
|
const msg = Array.isArray(response) ? response.join(", ") : String(response || "");
|
|
11307
|
-
this.logDebug("validation_failed", {
|
|
11308
|
-
response: msg
|
|
11309
|
-
});
|
|
11310
11245
|
if (msg.toLowerCase().includes("token expired/invalid")) {
|
|
11311
11246
|
this.stopVideo();
|
|
11312
11247
|
this.showMessage("Token expired/invalid. Please provide a new token.", true);
|
|
@@ -11321,7 +11256,6 @@ class ExtractorVideo {
|
|
|
11321
11256
|
return;
|
|
11322
11257
|
}
|
|
11323
11258
|
if (response?.InfoCode === "1006" && response?.TransactionID) {
|
|
11324
|
-
this.logDebug("unsupported_document", response);
|
|
11325
11259
|
const frontImage = this.candidateImages[this.candidateImages.length - 1].fullImg;
|
|
11326
11260
|
await (0,_requests_error_logging__WEBPACK_IMPORTED_MODULE_4__["default"])(frontImage, "", response.TransactionID, response.OS, response.Device, response.Browser);
|
|
11327
11261
|
this.showMessage("Document not supported.", true);
|
|
@@ -11403,10 +11337,6 @@ class ExtractorVideo {
|
|
|
11403
11337
|
return;
|
|
11404
11338
|
}
|
|
11405
11339
|
this.candidateImages = [];
|
|
11406
|
-
}).catch(err => {
|
|
11407
|
-
this.logDebug("document_detector_error", {
|
|
11408
|
-
message: err?.message || String(err)
|
|
11409
|
-
});
|
|
11410
11340
|
}).finally(() => {
|
|
11411
11341
|
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
11412
11342
|
});
|
|
@@ -11427,7 +11357,7 @@ class ExtractorVideo {
|
|
|
11427
11357
|
this.turnMessageTimer = null;
|
|
11428
11358
|
this.stopVideo();
|
|
11429
11359
|
if (isExtractionOk) {
|
|
11430
|
-
this.showMessage("Success - data extracted",
|
|
11360
|
+
this.showMessage("Success - data extracted", true);
|
|
11431
11361
|
const shouldStop = this.onExtractedResults({
|
|
11432
11362
|
success: true,
|
|
11433
11363
|
code: "001",
|
|
@@ -11435,7 +11365,7 @@ class ExtractorVideo {
|
|
|
11435
11365
|
data: extractionData
|
|
11436
11366
|
});
|
|
11437
11367
|
if (shouldStop === true) {
|
|
11438
|
-
setTimeout(() => this.
|
|
11368
|
+
setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
|
|
11439
11369
|
}
|
|
11440
11370
|
} else {
|
|
11441
11371
|
this.onExtractedResults({
|
|
@@ -11443,527 +11373,114 @@ class ExtractorVideo {
|
|
|
11443
11373
|
code: "005",
|
|
11444
11374
|
info: "Document validation passed but extraction failed."
|
|
11445
11375
|
});
|
|
11446
|
-
setTimeout(() => this.
|
|
11447
|
-
}
|
|
11448
|
-
}
|
|
11449
|
-
getSupportedConstraintsSafe() {
|
|
11450
|
-
if (!navigator.mediaDevices?.getSupportedConstraints) return {};
|
|
11451
|
-
try {
|
|
11452
|
-
return navigator.mediaDevices.getSupportedConstraints();
|
|
11453
|
-
} catch (_) {
|
|
11454
|
-
return {};
|
|
11455
|
-
}
|
|
11456
|
-
}
|
|
11457
|
-
getAspectRatio(settings) {
|
|
11458
|
-
if (typeof settings?.aspectRatio === "number" && settings.aspectRatio > 0) {
|
|
11459
|
-
return settings.aspectRatio;
|
|
11460
|
-
}
|
|
11461
|
-
if (settings?.width && settings?.height) {
|
|
11462
|
-
return settings.width / settings.height;
|
|
11463
|
-
}
|
|
11464
|
-
return null;
|
|
11465
|
-
}
|
|
11466
|
-
isLandscape(settings) {
|
|
11467
|
-
return (settings?.width || 0) > (settings?.height || 0);
|
|
11468
|
-
}
|
|
11469
|
-
isFrontLabel(label) {
|
|
11470
|
-
const l = (label || "").toLowerCase();
|
|
11471
|
-
return l.includes("front") || l.includes("selfie") || l.includes("user facing") || l.includes("facing front");
|
|
11472
|
-
}
|
|
11473
|
-
isRearLikeLabel(label) {
|
|
11474
|
-
const l = (label || "").toLowerCase();
|
|
11475
|
-
return l.includes("rear") || l.includes("back") || l.includes("environment") || l.includes("facing back") || l.includes("facing rear");
|
|
11476
|
-
}
|
|
11477
|
-
scoreTrack({
|
|
11478
|
-
settings,
|
|
11479
|
-
label
|
|
11480
|
-
}) {
|
|
11481
|
-
const width = settings?.width || 0;
|
|
11482
|
-
const height = settings?.height || 0;
|
|
11483
|
-
const pixels = width * height;
|
|
11484
|
-
const aspectRatio = this.getAspectRatio(settings) || 0;
|
|
11485
|
-
let score = pixels;
|
|
11486
|
-
if (width > height) score += 1_000_000_000;
|
|
11487
|
-
if (aspectRatio >= 1.3) score += 100_000_000;
|
|
11488
|
-
if (this.isRearLikeLabel(label)) score += 10_000_000;
|
|
11489
|
-
if (this.isFrontLabel(label)) score -= 10_000_000_000;
|
|
11490
|
-
return score;
|
|
11491
|
-
}
|
|
11492
|
-
buildVideoConstraints({
|
|
11493
|
-
width,
|
|
11494
|
-
height,
|
|
11495
|
-
mode,
|
|
11496
|
-
deviceId
|
|
11497
|
-
}) {
|
|
11498
|
-
const supported = this.getSupportedConstraintsSafe();
|
|
11499
|
-
const video = {};
|
|
11500
|
-
if (deviceId) {
|
|
11501
|
-
video.deviceId = {
|
|
11502
|
-
exact: deviceId
|
|
11503
|
-
};
|
|
11504
|
-
} else if (supported.facingMode) {
|
|
11505
|
-
if (typeof mode === "string") {
|
|
11506
|
-
video.facingMode = {
|
|
11507
|
-
ideal: mode
|
|
11508
|
-
};
|
|
11509
|
-
} else if (mode && typeof mode === "object") {
|
|
11510
|
-
video.facingMode = mode;
|
|
11511
|
-
} else {
|
|
11512
|
-
video.facingMode = {
|
|
11513
|
-
ideal: "environment"
|
|
11514
|
-
};
|
|
11515
|
-
}
|
|
11516
|
-
}
|
|
11517
|
-
if (supported.width) video.width = {
|
|
11518
|
-
ideal: width
|
|
11519
|
-
};
|
|
11520
|
-
if (supported.height) video.height = {
|
|
11521
|
-
ideal: height
|
|
11522
|
-
};
|
|
11523
|
-
if (supported.aspectRatio) video.aspectRatio = {
|
|
11524
|
-
ideal: width / height
|
|
11525
|
-
};
|
|
11526
|
-
return {
|
|
11527
|
-
video,
|
|
11528
|
-
audio: false
|
|
11529
|
-
};
|
|
11530
|
-
}
|
|
11531
|
-
async pauseBetweenCameraOps(ms = 180) {
|
|
11532
|
-
await new Promise(resolve => setTimeout(resolve, ms));
|
|
11533
|
-
}
|
|
11534
|
-
async stopMediaStream(stream) {
|
|
11535
|
-
if (!stream) return;
|
|
11536
|
-
try {
|
|
11537
|
-
stream.getTracks().forEach(t => t.stop());
|
|
11538
|
-
} catch (_) {}
|
|
11539
|
-
await this.pauseBetweenCameraOps();
|
|
11540
|
-
}
|
|
11541
|
-
async tryEnableDocumentFocus(stream) {
|
|
11542
|
-
const track = stream?.getVideoTracks?.()[0];
|
|
11543
|
-
if (!track) {
|
|
11544
|
-
this.logDebug("focus_no_track");
|
|
11545
|
-
return {
|
|
11546
|
-
ok: false,
|
|
11547
|
-
reason: "No video track found"
|
|
11548
|
-
};
|
|
11549
|
-
}
|
|
11550
|
-
if (!track.getCapabilities || !track.applyConstraints) {
|
|
11551
|
-
this.logDebug("focus_api_unavailable");
|
|
11552
|
-
return {
|
|
11553
|
-
ok: false,
|
|
11554
|
-
reason: "Focus APIs unavailable on this browser/device",
|
|
11555
|
-
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11556
|
-
};
|
|
11557
|
-
}
|
|
11558
|
-
const caps = track.getCapabilities();
|
|
11559
|
-
const result = {
|
|
11560
|
-
ok: true,
|
|
11561
|
-
capabilities: caps,
|
|
11562
|
-
attempts: [],
|
|
11563
|
-
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11564
|
-
};
|
|
11565
|
-
try {
|
|
11566
|
-
let applied = false;
|
|
11567
|
-
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("continuous")) {
|
|
11568
|
-
await track.applyConstraints({
|
|
11569
|
-
advanced: [{
|
|
11570
|
-
focusMode: "continuous"
|
|
11571
|
-
}]
|
|
11572
|
-
});
|
|
11573
|
-
result.attempts.push("Applied focusMode=continuous");
|
|
11574
|
-
applied = true;
|
|
11575
|
-
}
|
|
11576
|
-
if (caps.focusDistance) {
|
|
11577
|
-
const min = caps.focusDistance.min;
|
|
11578
|
-
const max = caps.focusDistance.max;
|
|
11579
|
-
const docDistance = min + (max - min) * 0.30;
|
|
11580
|
-
const advanced = [];
|
|
11581
|
-
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("manual")) {
|
|
11582
|
-
advanced.push({
|
|
11583
|
-
focusMode: "manual"
|
|
11584
|
-
});
|
|
11585
|
-
}
|
|
11586
|
-
advanced.push({
|
|
11587
|
-
focusDistance: docDistance
|
|
11588
|
-
});
|
|
11589
|
-
await track.applyConstraints({
|
|
11590
|
-
advanced
|
|
11591
|
-
});
|
|
11592
|
-
result.attempts.push(`Applied focusDistance=${docDistance}`);
|
|
11593
|
-
applied = true;
|
|
11594
|
-
}
|
|
11595
|
-
if (!applied) {
|
|
11596
|
-
result.ok = false;
|
|
11597
|
-
result.reason = "No supported focus controls were exposed";
|
|
11598
|
-
}
|
|
11599
|
-
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11600
|
-
this.logDebug("focus_result", result);
|
|
11601
|
-
return result;
|
|
11602
|
-
} catch (err) {
|
|
11603
|
-
result.ok = false;
|
|
11604
|
-
result.error = err.message || String(err);
|
|
11605
|
-
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11606
|
-
this.logDebug("focus_error", result);
|
|
11607
|
-
return result;
|
|
11608
|
-
}
|
|
11609
|
-
}
|
|
11610
|
-
async openCandidateStream({
|
|
11611
|
-
mode,
|
|
11612
|
-
deviceId,
|
|
11613
|
-
width,
|
|
11614
|
-
height
|
|
11615
|
-
}) {
|
|
11616
|
-
const constraints = this.buildVideoConstraints({
|
|
11617
|
-
width,
|
|
11618
|
-
height,
|
|
11619
|
-
mode,
|
|
11620
|
-
deviceId
|
|
11621
|
-
});
|
|
11622
|
-
this.logDebug("getUserMedia_attempt", {
|
|
11623
|
-
constraints
|
|
11624
|
-
});
|
|
11625
|
-
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
11626
|
-
this.logDebug("getUserMedia_success", {
|
|
11627
|
-
deviceId,
|
|
11628
|
-
width,
|
|
11629
|
-
height,
|
|
11630
|
-
trackLabels: stream.getTracks().map(t => t.label)
|
|
11631
|
-
});
|
|
11632
|
-
return stream;
|
|
11633
|
-
}
|
|
11634
|
-
async testDeviceAtPreferredResolutions(device, mode) {
|
|
11635
|
-
const attempts = [];
|
|
11636
|
-
for (const size of ExtractorVideo.VIDEO_CANDIDATE_SETTINGS) {
|
|
11637
|
-
let stream = null;
|
|
11638
|
-
try {
|
|
11639
|
-
stream = await this.openCandidateStream({
|
|
11640
|
-
mode,
|
|
11641
|
-
deviceId: device?.deviceId,
|
|
11642
|
-
width: size.width,
|
|
11643
|
-
height: size.height
|
|
11644
|
-
});
|
|
11645
|
-
const track = stream.getVideoTracks()[0];
|
|
11646
|
-
const settings = track.getSettings();
|
|
11647
|
-
const label = track.label || device?.label || "";
|
|
11648
|
-
const aspectRatio = this.getAspectRatio(settings);
|
|
11649
|
-
const result = {
|
|
11650
|
-
stream,
|
|
11651
|
-
label,
|
|
11652
|
-
settings,
|
|
11653
|
-
aspectRatio,
|
|
11654
|
-
landscape: this.isLandscape(settings),
|
|
11655
|
-
score: this.scoreTrack({
|
|
11656
|
-
settings,
|
|
11657
|
-
label
|
|
11658
|
-
}),
|
|
11659
|
-
requested: size
|
|
11660
|
-
};
|
|
11661
|
-
attempts.push({
|
|
11662
|
-
ok: true,
|
|
11663
|
-
requested: size,
|
|
11664
|
-
returnedWidth: settings.width,
|
|
11665
|
-
returnedHeight: settings.height,
|
|
11666
|
-
aspectRatio,
|
|
11667
|
-
label
|
|
11668
|
-
});
|
|
11669
|
-
this.logDebug("device_test_success", {
|
|
11670
|
-
deviceLabel: device?.label,
|
|
11671
|
-
deviceId: device?.deviceId,
|
|
11672
|
-
requested: size,
|
|
11673
|
-
settings,
|
|
11674
|
-
label
|
|
11675
|
-
});
|
|
11676
|
-
return {
|
|
11677
|
-
result,
|
|
11678
|
-
attempts
|
|
11679
|
-
};
|
|
11680
|
-
} catch (err) {
|
|
11681
|
-
attempts.push({
|
|
11682
|
-
ok: false,
|
|
11683
|
-
requested: size,
|
|
11684
|
-
error: err.message || String(err)
|
|
11685
|
-
});
|
|
11686
|
-
this.logDebug("device_test_error", {
|
|
11687
|
-
deviceLabel: device?.label,
|
|
11688
|
-
deviceId: device?.deviceId,
|
|
11689
|
-
requested: size,
|
|
11690
|
-
error: err?.message || String(err),
|
|
11691
|
-
name: err?.name
|
|
11692
|
-
});
|
|
11693
|
-
await this.stopMediaStream(stream);
|
|
11694
|
-
}
|
|
11695
|
-
}
|
|
11696
|
-
return {
|
|
11697
|
-
result: null,
|
|
11698
|
-
attempts
|
|
11699
|
-
};
|
|
11700
|
-
}
|
|
11701
|
-
async pickBestPhoneCamera(mode = "environment") {
|
|
11702
|
-
const debug = {
|
|
11703
|
-
mode,
|
|
11704
|
-
tested: []
|
|
11705
|
-
};
|
|
11706
|
-
let warmupStream = null;
|
|
11707
|
-
let warmupResult = null;
|
|
11708
|
-
try {
|
|
11709
|
-
const warmupSize = ExtractorVideo.VIDEO_CANDIDATE_SETTINGS[2];
|
|
11710
|
-
warmupStream = await this.openCandidateStream({
|
|
11711
|
-
mode,
|
|
11712
|
-
width: warmupSize.width,
|
|
11713
|
-
height: warmupSize.height
|
|
11714
|
-
});
|
|
11715
|
-
const track = warmupStream.getVideoTracks()[0];
|
|
11716
|
-
const settings = track.getSettings();
|
|
11717
|
-
const label = track.label || "";
|
|
11718
|
-
warmupResult = {
|
|
11719
|
-
stream: warmupStream,
|
|
11720
|
-
label,
|
|
11721
|
-
settings,
|
|
11722
|
-
aspectRatio: this.getAspectRatio(settings),
|
|
11723
|
-
landscape: this.isLandscape(settings),
|
|
11724
|
-
score: this.scoreTrack({
|
|
11725
|
-
settings,
|
|
11726
|
-
label
|
|
11727
|
-
}),
|
|
11728
|
-
requested: warmupSize
|
|
11729
|
-
};
|
|
11730
|
-
this.logDebug("warmup_success", {
|
|
11731
|
-
label,
|
|
11732
|
-
settings
|
|
11733
|
-
});
|
|
11734
|
-
} catch (err) {
|
|
11735
|
-
this.logDebug("warmup_error", {
|
|
11736
|
-
name: err?.name,
|
|
11737
|
-
message: err?.message || String(err)
|
|
11738
|
-
});
|
|
11739
|
-
warmupStream = null;
|
|
11376
|
+
setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
|
|
11740
11377
|
}
|
|
11741
|
-
let devices = [];
|
|
11742
|
-
try {
|
|
11743
|
-
devices = await navigator.mediaDevices.enumerateDevices();
|
|
11744
|
-
this.logDebug("enumerate_devices", {
|
|
11745
|
-
devices: devices.map(d => ({
|
|
11746
|
-
kind: d.kind,
|
|
11747
|
-
label: d.label,
|
|
11748
|
-
deviceId: d.deviceId
|
|
11749
|
-
}))
|
|
11750
|
-
});
|
|
11751
|
-
} catch (err) {
|
|
11752
|
-
this.logDebug("enumerate_devices_error", {
|
|
11753
|
-
name: err?.name,
|
|
11754
|
-
message: err?.message || String(err)
|
|
11755
|
-
});
|
|
11756
|
-
devices = [];
|
|
11757
|
-
}
|
|
11758
|
-
const videoInputs = devices.filter(d => d.kind === "videoinput");
|
|
11759
|
-
const candidateDevices = mode === "environment" ? videoInputs.filter(d => !this.isFrontLabel(d.label)) : videoInputs;
|
|
11760
|
-
if (warmupStream) {
|
|
11761
|
-
await this.stopMediaStream(warmupStream);
|
|
11762
|
-
warmupStream = null;
|
|
11763
|
-
}
|
|
11764
|
-
const usableResults = [];
|
|
11765
|
-
for (let i = 0; i < candidateDevices.length; i += 1) {
|
|
11766
|
-
const device = candidateDevices[i];
|
|
11767
|
-
const {
|
|
11768
|
-
result,
|
|
11769
|
-
attempts
|
|
11770
|
-
} = await this.testDeviceAtPreferredResolutions(device, mode);
|
|
11771
|
-
debug.tested.push({
|
|
11772
|
-
deviceId: device.deviceId,
|
|
11773
|
-
label: device.label,
|
|
11774
|
-
attempts,
|
|
11775
|
-
final: result ? {
|
|
11776
|
-
label: result.label,
|
|
11777
|
-
settings: result.settings,
|
|
11778
|
-
aspectRatio: result.aspectRatio,
|
|
11779
|
-
landscape: result.landscape,
|
|
11780
|
-
score: result.score,
|
|
11781
|
-
requested: result.requested
|
|
11782
|
-
} : null
|
|
11783
|
-
});
|
|
11784
|
-
if (result) {
|
|
11785
|
-
const frontRejected = mode === "environment" && this.isFrontLabel(result.label);
|
|
11786
|
-
const geometryOk = result.landscape;
|
|
11787
|
-
if (!frontRejected && geometryOk) {
|
|
11788
|
-
usableResults.push(result);
|
|
11789
|
-
} else {
|
|
11790
|
-
await this.stopMediaStream(result.stream);
|
|
11791
|
-
}
|
|
11792
|
-
}
|
|
11793
|
-
}
|
|
11794
|
-
if (usableResults.length > 0) {
|
|
11795
|
-
usableResults.sort((a, b) => b.score - a.score);
|
|
11796
|
-
const chosen = usableResults[0];
|
|
11797
|
-
for (const candidate of usableResults) {
|
|
11798
|
-
if (candidate !== chosen) {
|
|
11799
|
-
await this.stopMediaStream(candidate.stream);
|
|
11800
|
-
}
|
|
11801
|
-
}
|
|
11802
|
-
debug.chosen = {
|
|
11803
|
-
label: chosen.label,
|
|
11804
|
-
settings: chosen.settings,
|
|
11805
|
-
aspectRatio: chosen.aspectRatio,
|
|
11806
|
-
requested: chosen.requested,
|
|
11807
|
-
source: "device-test"
|
|
11808
|
-
};
|
|
11809
|
-
this.logDebug("camera_chosen", debug.chosen);
|
|
11810
|
-
return {
|
|
11811
|
-
chosen,
|
|
11812
|
-
debug
|
|
11813
|
-
};
|
|
11814
|
-
}
|
|
11815
|
-
for (const size of ExtractorVideo.VIDEO_CANDIDATE_SETTINGS) {
|
|
11816
|
-
try {
|
|
11817
|
-
const stream = await this.openCandidateStream({
|
|
11818
|
-
mode,
|
|
11819
|
-
width: size.width,
|
|
11820
|
-
height: size.height
|
|
11821
|
-
});
|
|
11822
|
-
const track = stream.getVideoTracks()[0];
|
|
11823
|
-
const settings = track.getSettings();
|
|
11824
|
-
const label = track.label || "";
|
|
11825
|
-
const aspectRatio = this.getAspectRatio(settings);
|
|
11826
|
-
const landscape = this.isLandscape(settings);
|
|
11827
|
-
if (mode === "environment" && !landscape) {
|
|
11828
|
-
await this.stopMediaStream(stream);
|
|
11829
|
-
continue;
|
|
11830
|
-
}
|
|
11831
|
-
const chosen = {
|
|
11832
|
-
stream,
|
|
11833
|
-
label,
|
|
11834
|
-
settings,
|
|
11835
|
-
aspectRatio,
|
|
11836
|
-
landscape,
|
|
11837
|
-
score: this.scoreTrack({
|
|
11838
|
-
settings,
|
|
11839
|
-
label
|
|
11840
|
-
}),
|
|
11841
|
-
requested: size
|
|
11842
|
-
};
|
|
11843
|
-
debug.chosen = {
|
|
11844
|
-
label,
|
|
11845
|
-
settings,
|
|
11846
|
-
aspectRatio,
|
|
11847
|
-
requested: size,
|
|
11848
|
-
source: "fallback-facingMode"
|
|
11849
|
-
};
|
|
11850
|
-
this.logDebug("camera_chosen_fallback", debug.chosen);
|
|
11851
|
-
return {
|
|
11852
|
-
chosen,
|
|
11853
|
-
debug
|
|
11854
|
-
};
|
|
11855
|
-
} catch (err) {
|
|
11856
|
-
this.logDebug("fallback_open_error", {
|
|
11857
|
-
requested: size,
|
|
11858
|
-
name: err?.name,
|
|
11859
|
-
message: err?.message || String(err)
|
|
11860
|
-
});
|
|
11861
|
-
}
|
|
11862
|
-
}
|
|
11863
|
-
throw new Error("Could not start a suitable camera.");
|
|
11864
11378
|
}
|
|
11865
11379
|
async startVideo() {
|
|
11866
11380
|
try {
|
|
11867
|
-
this.logDebug("startVideo_begin");
|
|
11868
11381
|
const serviceConfig = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfig)();
|
|
11869
11382
|
await serviceConfig.getAccessToken(false);
|
|
11870
11383
|
const videoElem = document.getElementById("ScanDocAIVideoElement");
|
|
11871
11384
|
if (!videoElem) {
|
|
11872
11385
|
throw new Error("Video element not found.");
|
|
11873
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
|
+
}
|
|
11874
11393
|
this.video = videoElem;
|
|
11875
11394
|
this.isRunning = true;
|
|
11876
11395
|
this.scanStartTime = Date.now();
|
|
11877
11396
|
const cfgValues = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfigValues)();
|
|
11878
11397
|
const mode = cfgValues.VIDEO_FACING_MODE ?? "environment";
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
this.video.srcObject = chosen.stream;
|
|
11884
|
-
this.logDebug("srcObject_assigned", {
|
|
11885
|
-
label: chosen.label,
|
|
11886
|
-
settings: chosen.settings
|
|
11398
|
+
await this.stopCurrentStream();
|
|
11399
|
+
const cameraResult = await openBestDocumentCamera({
|
|
11400
|
+
videoElement: this.video,
|
|
11401
|
+
preferredFacingMode: mode
|
|
11887
11402
|
});
|
|
11403
|
+
this.currentStream = cameraResult.stream;
|
|
11404
|
+
this.lastCameraInfo = cameraResult;
|
|
11888
11405
|
try {
|
|
11889
|
-
await this.
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
this.
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
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);
|
|
11901
11418
|
}
|
|
11902
|
-
this.focusDebug = await this.tryEnableDocumentFocus(chosen.stream);
|
|
11903
|
-
this.cameraDebug = debug;
|
|
11904
|
-
this.video.addEventListener("loadeddata", () => {
|
|
11905
|
-
this.logDebug("video_loadeddata", {
|
|
11906
|
-
videoWidth: this.video.videoWidth,
|
|
11907
|
-
videoHeight: this.video.videoHeight
|
|
11908
|
-
});
|
|
11909
|
-
this.adjustOverlayPosition();
|
|
11910
|
-
});
|
|
11911
|
-
this.video.addEventListener("error", () => {
|
|
11912
|
-
this.logDebug("video_element_error", {
|
|
11913
|
-
error: this.video?.error ? {
|
|
11914
|
-
code: this.video.error.code,
|
|
11915
|
-
message: this.video.error.message
|
|
11916
|
-
} : null
|
|
11917
|
-
});
|
|
11918
|
-
});
|
|
11919
|
-
window.addEventListener("resize", () => this.adjustOverlayPosition());
|
|
11920
11419
|
setTimeout(() => this.adjustOverlayPosition(), 500);
|
|
11921
11420
|
this.scanStartTime = Date.now();
|
|
11922
11421
|
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
11923
11422
|
this.showMessage("Starting scanning");
|
|
11924
|
-
|
|
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
|
+
});
|
|
11925
11430
|
return true;
|
|
11926
11431
|
} catch (error) {
|
|
11927
11432
|
console.error("startVideo failed:", error);
|
|
11928
|
-
this.logDebug("startVideo_failed", {
|
|
11929
|
-
name: error?.name,
|
|
11930
|
-
message: error?.message || String(error),
|
|
11931
|
-
dump: this.getDebugDump()
|
|
11932
|
-
});
|
|
11933
11433
|
this.isRunning = false;
|
|
11934
11434
|
this.stopVideo();
|
|
11935
11435
|
this.onExtractedResults({
|
|
11936
11436
|
success: false,
|
|
11937
11437
|
code: error.status === 401 || error.status === 403 ? "004" : "005",
|
|
11938
|
-
info: error.status === 401 || error.status === 403 ? "Authentication failed: Invalid API key/token." : "Startup failed: " + (error.message || "Unknown error")
|
|
11939
|
-
debug: ExtractorVideo.DEBUG ? this.getDebugDump() : undefined
|
|
11438
|
+
info: error.status === 401 || error.status === 403 ? "Authentication failed: Invalid API key/token." : "Startup failed: " + (error.message || "Unknown error")
|
|
11940
11439
|
});
|
|
11941
11440
|
return false;
|
|
11942
11441
|
}
|
|
11943
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
|
+
}
|
|
11944
11451
|
stopVideo() {
|
|
11945
11452
|
this.isRunning = false;
|
|
11946
|
-
this.logDebug("stopVideo_called");
|
|
11947
11453
|
if (this.video) {
|
|
11948
|
-
|
|
11949
|
-
|
|
11454
|
+
try {
|
|
11455
|
+
this.video.pause();
|
|
11456
|
+
} catch (_) {}
|
|
11457
|
+
if (this.video.srcObject) {
|
|
11950
11458
|
try {
|
|
11951
11459
|
this.video.srcObject.getTracks().forEach(t => t.stop());
|
|
11952
11460
|
} catch (_) {}
|
|
11953
11461
|
}
|
|
11954
11462
|
this.video.srcObject = null;
|
|
11955
11463
|
}
|
|
11464
|
+
if (this.currentStream) {
|
|
11465
|
+
try {
|
|
11466
|
+
this.currentStream.getTracks().forEach(t => t.stop());
|
|
11467
|
+
} catch (_) {}
|
|
11468
|
+
}
|
|
11469
|
+
this.currentStream = null;
|
|
11956
11470
|
clearTimeout(this.turnMessageTimer);
|
|
11957
11471
|
this.turnMessageTimer = null;
|
|
11958
11472
|
}
|
|
11959
11473
|
adjustOverlayPosition() {
|
|
11960
11474
|
const video = this.video;
|
|
11961
11475
|
const dot = document.querySelector(".centerGuideDot");
|
|
11962
|
-
if (!video || !dot || video.videoWidth === 0 || video.videoHeight === 0)
|
|
11476
|
+
if (!video || !dot || video.videoWidth === 0 || video.videoHeight === 0) {
|
|
11477
|
+
return;
|
|
11478
|
+
}
|
|
11963
11479
|
const videoRatio = video.videoWidth / video.videoHeight;
|
|
11964
11480
|
const container = video.parentElement;
|
|
11965
11481
|
const containerRatio = container.clientWidth / container.clientHeight;
|
|
11966
|
-
let scaleWidth
|
|
11482
|
+
let scaleWidth;
|
|
11483
|
+
let scaleHeight;
|
|
11967
11484
|
if (videoRatio > containerRatio) {
|
|
11968
11485
|
scaleWidth = container.clientWidth;
|
|
11969
11486
|
scaleHeight = container.clientWidth / videoRatio;
|
|
@@ -12047,81 +11564,86 @@ class ExtractorVideo {
|
|
|
12047
11564
|
}
|
|
12048
11565
|
</style>
|
|
12049
11566
|
`;
|
|
12050
|
-
} else {
|
|
12051
|
-
return `
|
|
12052
|
-
<div class="desktopFeedback" id="ScanDocAIMessage"></div>
|
|
12053
|
-
<div class="desktopVideoArea">
|
|
12054
|
-
<div class="desktopVideoHolder">
|
|
12055
|
-
<video id="ScanDocAIVideoElement" class="desktopVideo" autoplay muted playsinline></video>
|
|
12056
|
-
<div class="backgroundOverlay"></div>
|
|
12057
|
-
<div class="centerGuideDot"></div>
|
|
12058
|
-
</div>
|
|
12059
|
-
</div>
|
|
12060
|
-
|
|
12061
|
-
<style>
|
|
12062
|
-
.desktopVideoArea {
|
|
12063
|
-
display: flex;
|
|
12064
|
-
flex-direction: column;
|
|
12065
|
-
gap: 8px;
|
|
12066
|
-
padding: 16px;
|
|
12067
|
-
background-color: rgba(255, 255, 255, 0.05);
|
|
12068
|
-
border: 1px solid ${borderColor};
|
|
12069
|
-
border-radius: 15px;
|
|
12070
|
-
margin-left: 20%;
|
|
12071
|
-
margin-right: 20%;
|
|
12072
|
-
}
|
|
12073
|
-
.desktopVideoHolder {
|
|
12074
|
-
position: relative;
|
|
12075
|
-
width: 100%;
|
|
12076
|
-
overflow: hidden;
|
|
12077
|
-
}
|
|
12078
|
-
.desktopVideo {
|
|
12079
|
-
width: 100%;
|
|
12080
|
-
height: auto;
|
|
12081
|
-
margin-left: auto;
|
|
12082
|
-
margin-right: auto;
|
|
12083
|
-
max-width: 100vw;
|
|
12084
|
-
max-height: 100vh;
|
|
12085
|
-
}
|
|
12086
|
-
.centerGuideDot {
|
|
12087
|
-
position: absolute;
|
|
12088
|
-
width: 16px;
|
|
12089
|
-
height: 16px;
|
|
12090
|
-
background-color: #00ff55;
|
|
12091
|
-
border-radius: 50%;
|
|
12092
|
-
box-shadow: 0 0 8px 2px rgba(0, 255, 100, 0.8);
|
|
12093
|
-
z-index: 4;
|
|
12094
|
-
animation: flicker 1s infinite;
|
|
12095
|
-
pointer-events: none;
|
|
12096
|
-
display: none;
|
|
12097
|
-
}
|
|
12098
|
-
@keyframes flicker {
|
|
12099
|
-
0%, 100% { opacity: 1; }
|
|
12100
|
-
50% { opacity: 0.3; }
|
|
12101
|
-
}
|
|
12102
|
-
.backgroundOverlay {
|
|
12103
|
-
position: absolute;
|
|
12104
|
-
top: 0;
|
|
12105
|
-
left: 0;
|
|
12106
|
-
width: 100%;
|
|
12107
|
-
height: 100%;
|
|
12108
|
-
background-color: rgba(0, 0, 0, 0.051);
|
|
12109
|
-
z-index: 2;
|
|
12110
|
-
pointer-events: none;
|
|
12111
|
-
}
|
|
12112
|
-
.desktopFeedback {
|
|
12113
|
-
position: relative;
|
|
12114
|
-
display: flex;
|
|
12115
|
-
font-size: 22px;
|
|
12116
|
-
font-weight: 600;
|
|
12117
|
-
min-height: 36px;
|
|
12118
|
-
justify-content: center;
|
|
12119
|
-
align-items: center;
|
|
12120
|
-
color: ${messageColor};
|
|
12121
|
-
}
|
|
12122
|
-
</style>
|
|
12123
|
-
`;
|
|
12124
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
|
+
`;
|
|
12125
11647
|
}
|
|
12126
11648
|
}
|
|
12127
11649
|
let EXTRACTION_VIDEO = undefined;
|
|
@@ -12151,6 +11673,336 @@ function getExtractionVideo(tokenOrCallback, maybeCallback) {
|
|
|
12151
11673
|
}
|
|
12152
11674
|
return EXTRACTION_VIDEO;
|
|
12153
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
|
+
}
|
|
12154
12006
|
|
|
12155
12007
|
/***/ }),
|
|
12156
12008
|
|