scandoc-ai-components 0.1.18 → 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 +505 -618
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11170,25 +11170,15 @@ __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 = {};
|
|
11191
11180
|
this.onExtractedResults = onExtractedResults;
|
|
11181
|
+
this.onCameraSelected = null;
|
|
11192
11182
|
this.pastBlurValues = [];
|
|
11193
11183
|
this.isRunning = false;
|
|
11194
11184
|
this.scanStartTime = null;
|
|
@@ -11198,47 +11188,8 @@ class ExtractorVideo {
|
|
|
11198
11188
|
this.waitingForSecondSide = false;
|
|
11199
11189
|
this.hasShownTurnMessage = false;
|
|
11200
11190
|
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
|
-
};
|
|
11191
|
+
this.currentStream = null;
|
|
11192
|
+
this.lastCameraInfo = null;
|
|
11242
11193
|
}
|
|
11243
11194
|
reset() {
|
|
11244
11195
|
this.stopVideo();
|
|
@@ -11254,18 +11205,32 @@ class ExtractorVideo {
|
|
|
11254
11205
|
this.lastMessage = null;
|
|
11255
11206
|
this.waitingForSecondSide = false;
|
|
11256
11207
|
this.hasShownTurnMessage = false;
|
|
11208
|
+
this.lastCameraInfo = null;
|
|
11257
11209
|
clearTimeout(this.turnMessageTimer);
|
|
11258
11210
|
this.turnMessageTimer = null;
|
|
11259
|
-
|
|
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
|
+
}
|
|
11260
11228
|
}
|
|
11261
11229
|
async analyzeVideoStream() {
|
|
11262
11230
|
if (!this.isRunning) return;
|
|
11263
11231
|
const now = Date.now();
|
|
11264
11232
|
const cfgValues = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfigValues)();
|
|
11265
11233
|
if (this.scanStartTime && now - this.scanStartTime > cfgValues.MAX_SCAN_DURATION_MS) {
|
|
11266
|
-
this.logDebug("scan_timeout", {
|
|
11267
|
-
elapsedMs: now - this.scanStartTime
|
|
11268
|
-
});
|
|
11269
11234
|
this.stopVideo();
|
|
11270
11235
|
this.onExtractedResults({
|
|
11271
11236
|
success: false,
|
|
@@ -11276,16 +11241,7 @@ class ExtractorVideo {
|
|
|
11276
11241
|
}
|
|
11277
11242
|
const canvas = document.createElement("canvas");
|
|
11278
11243
|
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
|
-
});
|
|
11244
|
+
if (video && video.videoWidth < video.videoHeight) {
|
|
11289
11245
|
this.showMessage("Please rotate your device to landscape mode.");
|
|
11290
11246
|
this.candidateImages = [];
|
|
11291
11247
|
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
@@ -11304,9 +11260,6 @@ class ExtractorVideo {
|
|
|
11304
11260
|
const [isValidationOk, response] = await (0,_requests_validation__WEBPACK_IMPORTED_MODULE_3__["default"])(images.map(e => e.validationImg), this.pastBlurValues, {});
|
|
11305
11261
|
if (!isValidationOk) {
|
|
11306
11262
|
const msg = Array.isArray(response) ? response.join(", ") : String(response || "");
|
|
11307
|
-
this.logDebug("validation_failed", {
|
|
11308
|
-
response: msg
|
|
11309
|
-
});
|
|
11310
11263
|
if (msg.toLowerCase().includes("token expired/invalid")) {
|
|
11311
11264
|
this.stopVideo();
|
|
11312
11265
|
this.showMessage("Token expired/invalid. Please provide a new token.", true);
|
|
@@ -11321,7 +11274,6 @@ class ExtractorVideo {
|
|
|
11321
11274
|
return;
|
|
11322
11275
|
}
|
|
11323
11276
|
if (response?.InfoCode === "1006" && response?.TransactionID) {
|
|
11324
|
-
this.logDebug("unsupported_document", response);
|
|
11325
11277
|
const frontImage = this.candidateImages[this.candidateImages.length - 1].fullImg;
|
|
11326
11278
|
await (0,_requests_error_logging__WEBPACK_IMPORTED_MODULE_4__["default"])(frontImage, "", response.TransactionID, response.OS, response.Device, response.Browser);
|
|
11327
11279
|
this.showMessage("Document not supported.", true);
|
|
@@ -11403,10 +11355,6 @@ class ExtractorVideo {
|
|
|
11403
11355
|
return;
|
|
11404
11356
|
}
|
|
11405
11357
|
this.candidateImages = [];
|
|
11406
|
-
}).catch(err => {
|
|
11407
|
-
this.logDebug("document_detector_error", {
|
|
11408
|
-
message: err?.message || String(err)
|
|
11409
|
-
});
|
|
11410
11358
|
}).finally(() => {
|
|
11411
11359
|
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
11412
11360
|
});
|
|
@@ -11427,7 +11375,7 @@ class ExtractorVideo {
|
|
|
11427
11375
|
this.turnMessageTimer = null;
|
|
11428
11376
|
this.stopVideo();
|
|
11429
11377
|
if (isExtractionOk) {
|
|
11430
|
-
this.showMessage("Success - data extracted",
|
|
11378
|
+
this.showMessage("Success - data extracted", true);
|
|
11431
11379
|
const shouldStop = this.onExtractedResults({
|
|
11432
11380
|
success: true,
|
|
11433
11381
|
code: "001",
|
|
@@ -11435,7 +11383,7 @@ class ExtractorVideo {
|
|
|
11435
11383
|
data: extractionData
|
|
11436
11384
|
});
|
|
11437
11385
|
if (shouldStop === true) {
|
|
11438
|
-
setTimeout(() => this.
|
|
11386
|
+
setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
|
|
11439
11387
|
}
|
|
11440
11388
|
} else {
|
|
11441
11389
|
this.onExtractedResults({
|
|
@@ -11443,509 +11391,89 @@ class ExtractorVideo {
|
|
|
11443
11391
|
code: "005",
|
|
11444
11392
|
info: "Document validation passed but extraction failed."
|
|
11445
11393
|
});
|
|
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
|
-
};
|
|
11394
|
+
setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
|
|
11557
11395
|
}
|
|
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;
|
|
11740
|
-
}
|
|
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
11396
|
}
|
|
11865
11397
|
async startVideo() {
|
|
11866
11398
|
try {
|
|
11867
|
-
this.logDebug("startVideo_begin");
|
|
11868
11399
|
const serviceConfig = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfig)();
|
|
11869
11400
|
await serviceConfig.getAccessToken(false);
|
|
11870
11401
|
const videoElem = document.getElementById("ScanDocAIVideoElement");
|
|
11871
11402
|
if (!videoElem) {
|
|
11872
11403
|
throw new Error("Video element not found.");
|
|
11873
11404
|
}
|
|
11405
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
11406
|
+
throw new Error("getUserMedia is not available");
|
|
11407
|
+
}
|
|
11408
|
+
if (!window.isSecureContext) {
|
|
11409
|
+
throw new Error("Camera requires HTTPS / secure context");
|
|
11410
|
+
}
|
|
11874
11411
|
this.video = videoElem;
|
|
11875
11412
|
this.isRunning = true;
|
|
11876
11413
|
this.scanStartTime = Date.now();
|
|
11877
11414
|
const cfgValues = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfigValues)();
|
|
11878
11415
|
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
|
|
11416
|
+
await this.stopCurrentStream();
|
|
11417
|
+
const cameraResult = await openBestDocumentCamera({
|
|
11418
|
+
videoElement: this.video,
|
|
11419
|
+
preferredFacingMode: mode
|
|
11887
11420
|
});
|
|
11421
|
+
this.currentStream = cameraResult.stream;
|
|
11422
|
+
this.lastCameraInfo = cameraResult;
|
|
11423
|
+
this.emitCameraInfo(cameraResult);
|
|
11888
11424
|
try {
|
|
11889
|
-
await this.
|
|
11890
|
-
|
|
11891
|
-
|
|
11892
|
-
|
|
11893
|
-
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
this.
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11425
|
+
const focusResult = await tryEnableDocumentFocus(this.currentStream);
|
|
11426
|
+
console.log("Focus result:", focusResult);
|
|
11427
|
+
} catch (focusError) {
|
|
11428
|
+
console.warn("Could not apply focus settings:", focusError);
|
|
11429
|
+
}
|
|
11430
|
+
if (!this._boundLoadedDataHandler) {
|
|
11431
|
+
this._boundLoadedDataHandler = () => this.adjustOverlayPosition();
|
|
11432
|
+
this.video.addEventListener("loadeddata", this._boundLoadedDataHandler);
|
|
11433
|
+
}
|
|
11434
|
+
if (!this._boundResizeHandler) {
|
|
11435
|
+
this._boundResizeHandler = () => this.adjustOverlayPosition();
|
|
11436
|
+
window.addEventListener("resize", this._boundResizeHandler);
|
|
11901
11437
|
}
|
|
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
11438
|
setTimeout(() => this.adjustOverlayPosition(), 500);
|
|
11921
11439
|
this.scanStartTime = Date.now();
|
|
11922
11440
|
setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
|
|
11923
11441
|
this.showMessage("Starting scanning");
|
|
11924
|
-
|
|
11442
|
+
console.log("Chosen camera:", {
|
|
11443
|
+
label: cameraResult.label,
|
|
11444
|
+
deviceId: cameraResult.deviceId,
|
|
11445
|
+
strategy: cameraResult.strategy,
|
|
11446
|
+
settings: cameraResult.settings,
|
|
11447
|
+
score: cameraResult.score,
|
|
11448
|
+
tested: cameraResult.tested
|
|
11449
|
+
});
|
|
11925
11450
|
return true;
|
|
11926
11451
|
} catch (error) {
|
|
11927
11452
|
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
11453
|
this.isRunning = false;
|
|
11934
11454
|
this.stopVideo();
|
|
11935
11455
|
this.onExtractedResults({
|
|
11936
11456
|
success: false,
|
|
11937
11457
|
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
|
|
11458
|
+
info: error.status === 401 || error.status === 403 ? "Authentication failed: Invalid API key/token." : "Startup failed: " + (error.message || "Unknown error")
|
|
11940
11459
|
});
|
|
11941
11460
|
return false;
|
|
11942
11461
|
}
|
|
11943
11462
|
}
|
|
11463
|
+
async stopCurrentStream() {
|
|
11464
|
+
await stopStream(this.currentStream);
|
|
11465
|
+
this.currentStream = null;
|
|
11466
|
+
if (this.video) {
|
|
11467
|
+
this.video.srcObject = null;
|
|
11468
|
+
}
|
|
11469
|
+
await delay(200);
|
|
11470
|
+
}
|
|
11944
11471
|
stopVideo() {
|
|
11945
11472
|
this.isRunning = false;
|
|
11946
|
-
this.logDebug("stopVideo_called");
|
|
11947
11473
|
if (this.video) {
|
|
11948
|
-
|
|
11474
|
+
try {
|
|
11475
|
+
this.video.pause();
|
|
11476
|
+
} catch (_) {}
|
|
11949
11477
|
if (this.video.srcObject !== undefined && this.video.srcObject !== null) {
|
|
11950
11478
|
try {
|
|
11951
11479
|
this.video.srcObject.getTracks().forEach(t => t.stop());
|
|
@@ -11953,17 +11481,26 @@ class ExtractorVideo {
|
|
|
11953
11481
|
}
|
|
11954
11482
|
this.video.srcObject = null;
|
|
11955
11483
|
}
|
|
11484
|
+
if (this.currentStream) {
|
|
11485
|
+
try {
|
|
11486
|
+
this.currentStream.getTracks().forEach(t => t.stop());
|
|
11487
|
+
} catch (_) {}
|
|
11488
|
+
}
|
|
11489
|
+
this.currentStream = null;
|
|
11956
11490
|
clearTimeout(this.turnMessageTimer);
|
|
11957
11491
|
this.turnMessageTimer = null;
|
|
11958
11492
|
}
|
|
11959
11493
|
adjustOverlayPosition() {
|
|
11960
11494
|
const video = this.video;
|
|
11961
11495
|
const dot = document.querySelector(".centerGuideDot");
|
|
11962
|
-
if (!video || !dot || video.videoWidth === 0 || video.videoHeight === 0)
|
|
11496
|
+
if (!video || !dot || video.videoWidth === 0 || video.videoHeight === 0) {
|
|
11497
|
+
return;
|
|
11498
|
+
}
|
|
11963
11499
|
const videoRatio = video.videoWidth / video.videoHeight;
|
|
11964
11500
|
const container = video.parentElement;
|
|
11965
11501
|
const containerRatio = container.clientWidth / container.clientHeight;
|
|
11966
|
-
let scaleWidth
|
|
11502
|
+
let scaleWidth;
|
|
11503
|
+
let scaleHeight;
|
|
11967
11504
|
if (videoRatio > containerRatio) {
|
|
11968
11505
|
scaleWidth = container.clientWidth;
|
|
11969
11506
|
scaleHeight = container.clientWidth / videoRatio;
|
|
@@ -12047,81 +11584,98 @@ class ExtractorVideo {
|
|
|
12047
11584
|
}
|
|
12048
11585
|
</style>
|
|
12049
11586
|
`;
|
|
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
11587
|
}
|
|
11588
|
+
return `
|
|
11589
|
+
<div class="desktopFeedback" id="ScanDocAIMessage"></div>
|
|
11590
|
+
<div class="desktopVideoArea">
|
|
11591
|
+
<div class="desktopVideoHolder">
|
|
11592
|
+
<video id="ScanDocAIVideoElement" class="desktopVideo" autoplay muted playsinline></video>
|
|
11593
|
+
<div class="backgroundOverlay"></div>
|
|
11594
|
+
<div class="centerGuideDot"></div>
|
|
11595
|
+
</div>
|
|
11596
|
+
</div>
|
|
11597
|
+
|
|
11598
|
+
<style>
|
|
11599
|
+
.desktopVideoArea {
|
|
11600
|
+
display: flex;
|
|
11601
|
+
flex-direction: column;
|
|
11602
|
+
gap: 8px;
|
|
11603
|
+
padding: 16px;
|
|
11604
|
+
background-color: rgba(255, 255, 255, 0.05);
|
|
11605
|
+
border: 1px solid ${borderColor};
|
|
11606
|
+
border-radius: 15px;
|
|
11607
|
+
margin-left: 20%;
|
|
11608
|
+
margin-right: 20%;
|
|
11609
|
+
}
|
|
11610
|
+
|
|
11611
|
+
.desktopVideoHolder {
|
|
11612
|
+
position: relative;
|
|
11613
|
+
width: 100%;
|
|
11614
|
+
overflow: hidden;
|
|
11615
|
+
}
|
|
11616
|
+
|
|
11617
|
+
.desktopVideo {
|
|
11618
|
+
width: 100%;
|
|
11619
|
+
height: auto;
|
|
11620
|
+
margin-left: auto;
|
|
11621
|
+
margin-right: auto;
|
|
11622
|
+
max-width: 100vw;
|
|
11623
|
+
max-height: 100vh;
|
|
11624
|
+
}
|
|
11625
|
+
|
|
11626
|
+
.centerGuideDot {
|
|
11627
|
+
position: absolute;
|
|
11628
|
+
width: 16px;
|
|
11629
|
+
height: 16px;
|
|
11630
|
+
background-color: #00ff55;
|
|
11631
|
+
border-radius: 50%;
|
|
11632
|
+
box-shadow: 0 0 8px 2px rgba(0, 255, 100, 0.8);
|
|
11633
|
+
z-index: 4;
|
|
11634
|
+
animation: flicker 1s infinite;
|
|
11635
|
+
pointer-events: none;
|
|
11636
|
+
display: none;
|
|
11637
|
+
}
|
|
11638
|
+
|
|
11639
|
+
@keyframes flicker {
|
|
11640
|
+
0%, 100% { opacity: 1; }
|
|
11641
|
+
50% { opacity: 0.3; }
|
|
11642
|
+
}
|
|
11643
|
+
|
|
11644
|
+
.backgroundOverlay {
|
|
11645
|
+
position: absolute;
|
|
11646
|
+
top: 0;
|
|
11647
|
+
left: 0;
|
|
11648
|
+
width: 100%;
|
|
11649
|
+
height: 100%;
|
|
11650
|
+
background-color: rgba(0, 0, 0, 0.051);
|
|
11651
|
+
z-index: 2;
|
|
11652
|
+
pointer-events: none;
|
|
11653
|
+
}
|
|
11654
|
+
|
|
11655
|
+
.desktopFeedback {
|
|
11656
|
+
position: relative;
|
|
11657
|
+
display: flex;
|
|
11658
|
+
font-size: 22px;
|
|
11659
|
+
font-weight: 600;
|
|
11660
|
+
min-height: 36px;
|
|
11661
|
+
justify-content: center;
|
|
11662
|
+
align-items: center;
|
|
11663
|
+
color: ${messageColor};
|
|
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
|
+
}
|
|
11677
|
+
</style>
|
|
11678
|
+
`;
|
|
12125
11679
|
}
|
|
12126
11680
|
}
|
|
12127
11681
|
let EXTRACTION_VIDEO = undefined;
|
|
@@ -12151,6 +11705,339 @@ function getExtractionVideo(tokenOrCallback, maybeCallback) {
|
|
|
12151
11705
|
}
|
|
12152
11706
|
return EXTRACTION_VIDEO;
|
|
12153
11707
|
}
|
|
11708
|
+
function delay(ms) {
|
|
11709
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
11710
|
+
}
|
|
11711
|
+
async function stopStream(stream) {
|
|
11712
|
+
if (!stream) return;
|
|
11713
|
+
for (const track of stream.getTracks()) {
|
|
11714
|
+
try {
|
|
11715
|
+
track.stop();
|
|
11716
|
+
} catch (_) {}
|
|
11717
|
+
}
|
|
11718
|
+
}
|
|
11719
|
+
function getAspectRatio(settings = {}) {
|
|
11720
|
+
if (typeof settings.aspectRatio === "number" && settings.aspectRatio > 0) {
|
|
11721
|
+
return settings.aspectRatio;
|
|
11722
|
+
}
|
|
11723
|
+
if (settings.width && settings.height) {
|
|
11724
|
+
return settings.width / settings.height;
|
|
11725
|
+
}
|
|
11726
|
+
return null;
|
|
11727
|
+
}
|
|
11728
|
+
async function attachStreamToVideo(videoEl, stream) {
|
|
11729
|
+
if (!videoEl) return;
|
|
11730
|
+
videoEl.srcObject = stream;
|
|
11731
|
+
try {
|
|
11732
|
+
await videoEl.play();
|
|
11733
|
+
} catch (_) {}
|
|
11734
|
+
}
|
|
11735
|
+
function getSupportedConstraintsSafe() {
|
|
11736
|
+
try {
|
|
11737
|
+
return navigator.mediaDevices?.getSupportedConstraints?.() || {};
|
|
11738
|
+
} catch (_) {
|
|
11739
|
+
return {};
|
|
11740
|
+
}
|
|
11741
|
+
}
|
|
11742
|
+
function buildDocumentScanConstraints(mode = "environment") {
|
|
11743
|
+
const supported = getSupportedConstraintsSafe();
|
|
11744
|
+
const video = {};
|
|
11745
|
+
if (supported.facingMode) {
|
|
11746
|
+
video.facingMode = typeof mode === "string" ? {
|
|
11747
|
+
ideal: mode
|
|
11748
|
+
} : mode || {
|
|
11749
|
+
ideal: "environment"
|
|
11750
|
+
};
|
|
11751
|
+
}
|
|
11752
|
+
if (supported.width) {
|
|
11753
|
+
video.width = {
|
|
11754
|
+
ideal: 1920
|
|
11755
|
+
};
|
|
11756
|
+
}
|
|
11757
|
+
if (supported.height) {
|
|
11758
|
+
video.height = {
|
|
11759
|
+
ideal: 1080
|
|
11760
|
+
};
|
|
11761
|
+
}
|
|
11762
|
+
if (supported.aspectRatio) {
|
|
11763
|
+
video.aspectRatio = {
|
|
11764
|
+
ideal: 16 / 9
|
|
11765
|
+
};
|
|
11766
|
+
}
|
|
11767
|
+
return {
|
|
11768
|
+
video,
|
|
11769
|
+
audio: false
|
|
11770
|
+
};
|
|
11771
|
+
}
|
|
11772
|
+
function buildDeviceConstraints(deviceId) {
|
|
11773
|
+
const supported = getSupportedConstraintsSafe();
|
|
11774
|
+
const video = {
|
|
11775
|
+
deviceId: {
|
|
11776
|
+
exact: deviceId
|
|
11777
|
+
}
|
|
11778
|
+
};
|
|
11779
|
+
if (supported.width) {
|
|
11780
|
+
video.width = {
|
|
11781
|
+
ideal: 1920
|
|
11782
|
+
};
|
|
11783
|
+
}
|
|
11784
|
+
if (supported.height) {
|
|
11785
|
+
video.height = {
|
|
11786
|
+
ideal: 1080
|
|
11787
|
+
};
|
|
11788
|
+
}
|
|
11789
|
+
if (supported.aspectRatio) {
|
|
11790
|
+
video.aspectRatio = {
|
|
11791
|
+
ideal: 16 / 9
|
|
11792
|
+
};
|
|
11793
|
+
}
|
|
11794
|
+
return {
|
|
11795
|
+
video,
|
|
11796
|
+
audio: false
|
|
11797
|
+
};
|
|
11798
|
+
}
|
|
11799
|
+
async function enumerateVideoInputDevices() {
|
|
11800
|
+
if (!navigator.mediaDevices?.enumerateDevices) return [];
|
|
11801
|
+
try {
|
|
11802
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
11803
|
+
return devices.filter(d => d.kind === "videoinput");
|
|
11804
|
+
} catch (error) {
|
|
11805
|
+
console.warn("enumerateDevices failed:", error);
|
|
11806
|
+
return [];
|
|
11807
|
+
}
|
|
11808
|
+
}
|
|
11809
|
+
function normalizeLabel(label = "") {
|
|
11810
|
+
return String(label).trim().toLowerCase();
|
|
11811
|
+
}
|
|
11812
|
+
function isLikelyRearCamera(label = "") {
|
|
11813
|
+
const v = normalizeLabel(label);
|
|
11814
|
+
return v.includes("back") || v.includes("rear") || v.includes("environment") || v.includes("world");
|
|
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
|
+
}
|
|
11820
|
+
function isLikelyUltraWideOrMacro(label = "") {
|
|
11821
|
+
const v = normalizeLabel(label);
|
|
11822
|
+
return v.includes("ultra") || v.includes("ultrawide") || v.includes("wide angle") || v.includes("0.5") || v.includes("macro") || v.includes("fisheye");
|
|
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
|
+
}
|
|
11831
|
+
function scoreCameraCandidate({
|
|
11832
|
+
label,
|
|
11833
|
+
settings,
|
|
11834
|
+
isBootstrap = false
|
|
11835
|
+
}) {
|
|
11836
|
+
const width = Number(settings?.width || 0);
|
|
11837
|
+
const height = Number(settings?.height || 0);
|
|
11838
|
+
const pixels = getPixelCount(settings);
|
|
11839
|
+
const aspectRatio = getAspectRatio(settings) || 0;
|
|
11840
|
+
let score = 0;
|
|
11841
|
+
score += pixels;
|
|
11842
|
+
if (isBootstrap) {
|
|
11843
|
+
score += 400000;
|
|
11844
|
+
}
|
|
11845
|
+
if (isLikelyRearCamera(label)) {
|
|
11846
|
+
score += 4000000;
|
|
11847
|
+
}
|
|
11848
|
+
if (isLikelyFrontCamera(label)) {
|
|
11849
|
+
score -= 12000000;
|
|
11850
|
+
}
|
|
11851
|
+
if (isLikelyUltraWideOrMacro(label)) {
|
|
11852
|
+
score -= 10000000;
|
|
11853
|
+
}
|
|
11854
|
+
if (isLikelyTelephoto(label)) {
|
|
11855
|
+
score -= 2500000;
|
|
11856
|
+
}
|
|
11857
|
+
if (aspectRatio >= 1.6 && aspectRatio <= 1.9) {
|
|
11858
|
+
score += 700000;
|
|
11859
|
+
}
|
|
11860
|
+
if (aspectRatio > 2.05) {
|
|
11861
|
+
score -= 1500000;
|
|
11862
|
+
}
|
|
11863
|
+
if (width >= 1920) {
|
|
11864
|
+
score += 500000;
|
|
11865
|
+
}
|
|
11866
|
+
if (width < 1280 || height < 720) {
|
|
11867
|
+
score -= 4000000;
|
|
11868
|
+
}
|
|
11869
|
+
return score;
|
|
11870
|
+
}
|
|
11871
|
+
async function probeCameraCandidate(device) {
|
|
11872
|
+
let stream = null;
|
|
11873
|
+
try {
|
|
11874
|
+
stream = await navigator.mediaDevices.getUserMedia(buildDeviceConstraints(device.deviceId));
|
|
11875
|
+
const track = stream.getVideoTracks?.()[0];
|
|
11876
|
+
const settings = track?.getSettings?.() || {};
|
|
11877
|
+
const label = track?.label || device.label || "";
|
|
11878
|
+
const score = scoreCameraCandidate({
|
|
11879
|
+
label,
|
|
11880
|
+
settings
|
|
11881
|
+
});
|
|
11882
|
+
return {
|
|
11883
|
+
ok: true,
|
|
11884
|
+
device,
|
|
11885
|
+
stream,
|
|
11886
|
+
label,
|
|
11887
|
+
deviceId: settings.deviceId || device.deviceId,
|
|
11888
|
+
settings,
|
|
11889
|
+
aspectRatio: getAspectRatio(settings),
|
|
11890
|
+
score
|
|
11891
|
+
};
|
|
11892
|
+
} catch (error) {
|
|
11893
|
+
if (stream) {
|
|
11894
|
+
await stopStream(stream);
|
|
11895
|
+
}
|
|
11896
|
+
return {
|
|
11897
|
+
ok: false,
|
|
11898
|
+
device,
|
|
11899
|
+
error,
|
|
11900
|
+
score: Number.NEGATIVE_INFINITY
|
|
11901
|
+
};
|
|
11902
|
+
}
|
|
11903
|
+
}
|
|
11904
|
+
function isIOSLike() {
|
|
11905
|
+
const ua = navigator.userAgent || "";
|
|
11906
|
+
return /iPhone|iPad|iPod/i.test(ua);
|
|
11907
|
+
}
|
|
11908
|
+
async function tryEnableDocumentFocus(stream) {
|
|
11909
|
+
const track = stream?.getVideoTracks?.()[0];
|
|
11910
|
+
if (!track) {
|
|
11911
|
+
return {
|
|
11912
|
+
ok: false,
|
|
11913
|
+
reason: "No video track found"
|
|
11914
|
+
};
|
|
11915
|
+
}
|
|
11916
|
+
if (!track.getCapabilities || !track.applyConstraints) {
|
|
11917
|
+
return {
|
|
11918
|
+
ok: false,
|
|
11919
|
+
reason: "Focus APIs unavailable on this browser/device",
|
|
11920
|
+
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11921
|
+
};
|
|
11922
|
+
}
|
|
11923
|
+
const caps = track.getCapabilities();
|
|
11924
|
+
const result = {
|
|
11925
|
+
ok: true,
|
|
11926
|
+
capabilities: caps,
|
|
11927
|
+
attempts: [],
|
|
11928
|
+
settingsBefore: track.getSettings ? track.getSettings() : null
|
|
11929
|
+
};
|
|
11930
|
+
try {
|
|
11931
|
+
let applied = false;
|
|
11932
|
+
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("continuous")) {
|
|
11933
|
+
await track.applyConstraints({
|
|
11934
|
+
advanced: [{
|
|
11935
|
+
focusMode: "continuous"
|
|
11936
|
+
}]
|
|
11937
|
+
});
|
|
11938
|
+
result.attempts.push("Applied focusMode=continuous");
|
|
11939
|
+
applied = true;
|
|
11940
|
+
}
|
|
11941
|
+
if (caps.focusDistance) {
|
|
11942
|
+
const min = caps.focusDistance.min ?? 0;
|
|
11943
|
+
const max = caps.focusDistance.max ?? 0;
|
|
11944
|
+
const docDistance = min + (max - min) * 0.3;
|
|
11945
|
+
const advanced = [];
|
|
11946
|
+
if (Array.isArray(caps.focusMode) && caps.focusMode.includes("manual")) {
|
|
11947
|
+
advanced.push({
|
|
11948
|
+
focusMode: "manual"
|
|
11949
|
+
});
|
|
11950
|
+
}
|
|
11951
|
+
advanced.push({
|
|
11952
|
+
focusDistance: docDistance
|
|
11953
|
+
});
|
|
11954
|
+
await track.applyConstraints({
|
|
11955
|
+
advanced
|
|
11956
|
+
});
|
|
11957
|
+
result.attempts.push(`Applied focusDistance=${docDistance}`);
|
|
11958
|
+
applied = true;
|
|
11959
|
+
}
|
|
11960
|
+
if (!applied) {
|
|
11961
|
+
result.ok = false;
|
|
11962
|
+
result.reason = "No supported focus controls were exposed";
|
|
11963
|
+
}
|
|
11964
|
+
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11965
|
+
return result;
|
|
11966
|
+
} catch (err) {
|
|
11967
|
+
result.ok = false;
|
|
11968
|
+
result.error = err.message || String(err);
|
|
11969
|
+
result.settingsAfter = track.getSettings ? track.getSettings() : null;
|
|
11970
|
+
return result;
|
|
11971
|
+
}
|
|
11972
|
+
}
|
|
11973
|
+
async function openBestDocumentCamera({
|
|
11974
|
+
videoElement,
|
|
11975
|
+
preferredFacingMode = "environment"
|
|
11976
|
+
}) {
|
|
11977
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
11978
|
+
throw new Error("getUserMedia is not available");
|
|
11979
|
+
}
|
|
11980
|
+
if (!window.isSecureContext) {
|
|
11981
|
+
throw new Error("Camera requires HTTPS / secure context");
|
|
11982
|
+
}
|
|
11983
|
+
let bootstrapStream;
|
|
11984
|
+
try {
|
|
11985
|
+
bootstrapStream = await navigator.mediaDevices.getUserMedia(buildDocumentScanConstraints(preferredFacingMode));
|
|
11986
|
+
} catch (preferredError) {
|
|
11987
|
+
console.warn("Preferred camera open failed, falling back:", preferredError);
|
|
11988
|
+
bootstrapStream = await navigator.mediaDevices.getUserMedia({
|
|
11989
|
+
video: true,
|
|
11990
|
+
audio: false
|
|
11991
|
+
});
|
|
11992
|
+
}
|
|
11993
|
+
const bootstrapTrack = bootstrapStream.getVideoTracks?.()[0];
|
|
11994
|
+
const bootstrapSettings = bootstrapTrack?.getSettings?.() || {};
|
|
11995
|
+
const bootstrapLabel = bootstrapTrack?.label || "";
|
|
11996
|
+
const bootstrapDeviceId = bootstrapSettings.deviceId || null;
|
|
11997
|
+
let best = {
|
|
11998
|
+
stream: bootstrapStream,
|
|
11999
|
+
label: bootstrapLabel,
|
|
12000
|
+
deviceId: bootstrapDeviceId,
|
|
12001
|
+
settings: bootstrapSettings,
|
|
12002
|
+
aspectRatio: getAspectRatio(bootstrapSettings),
|
|
12003
|
+
score: scoreCameraCandidate({
|
|
12004
|
+
label: bootstrapLabel,
|
|
12005
|
+
settings: bootstrapSettings,
|
|
12006
|
+
isBootstrap: true
|
|
12007
|
+
}),
|
|
12008
|
+
strategy: "bootstrap-environment",
|
|
12009
|
+
tested: []
|
|
12010
|
+
};
|
|
12011
|
+
const devices = await enumerateVideoInputDevices();
|
|
12012
|
+
const rearDevices = devices.filter(d => isLikelyRearCamera(d.label));
|
|
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);
|
|
12015
|
+
for (const device of candidates) {
|
|
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
|
+
});
|
|
12025
|
+
if (!probed.ok) continue;
|
|
12026
|
+
const clearlyBetter = probed.score > best.score + 1000000;
|
|
12027
|
+
if (clearlyBetter) {
|
|
12028
|
+
await stopStream(best.stream);
|
|
12029
|
+
best = {
|
|
12030
|
+
...probed,
|
|
12031
|
+
strategy: "switched-after-ranking",
|
|
12032
|
+
tested: best.tested
|
|
12033
|
+
};
|
|
12034
|
+
} else {
|
|
12035
|
+
await stopStream(probed.stream);
|
|
12036
|
+
}
|
|
12037
|
+
}
|
|
12038
|
+
await attachStreamToVideo(videoElement, best.stream);
|
|
12039
|
+
return best;
|
|
12040
|
+
}
|
|
12154
12041
|
|
|
12155
12042
|
/***/ }),
|
|
12156
12043
|
|