scandoc-ai-components 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +545 -197
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11170,18 +11170,21 @@ __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;
11173
11174
  static FREQUENCY_MS = 10;
11174
11175
  static VALIDATION_BATCH_SIZE = 1;
11175
11176
  static VALIDATION_IMG_WIDTH = 384;
11176
11177
  static VALIDATION_IMG_HEIGHT = 384;
11177
- static VIDEO_SETTINGS = Object.freeze({
11178
- width: {
11179
- ideal: 1920
11180
- },
11181
- height: {
11182
- ideal: 1080
11183
- }
11184
- });
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
+ }]);
11185
11188
  constructor(onExtractedResults) {
11186
11189
  this.candidateImages = [];
11187
11190
  this.extractionImages = {};
@@ -11195,10 +11198,48 @@ class ExtractorVideo {
11195
11198
  this.waitingForSecondSide = false;
11196
11199
  this.hasShownTurnMessage = false;
11197
11200
  this.turnMessageTimer = null;
11198
-
11199
- // initialize through setter so we see the first set too:
11201
+ this.debugLog = [];
11200
11202
  this.onExtractedResults = onExtractedResults;
11201
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
+ };
11242
+ }
11202
11243
  reset() {
11203
11244
  this.stopVideo();
11204
11245
  this.candidateImages = [];
@@ -11215,12 +11256,16 @@ class ExtractorVideo {
11215
11256
  this.hasShownTurnMessage = false;
11216
11257
  clearTimeout(this.turnMessageTimer);
11217
11258
  this.turnMessageTimer = null;
11259
+ this.debugLog = [];
11218
11260
  }
11219
11261
  async analyzeVideoStream() {
11220
11262
  if (!this.isRunning) return;
11221
11263
  const now = Date.now();
11222
11264
  const cfgValues = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfigValues)();
11223
11265
  if (this.scanStartTime && now - this.scanStartTime > cfgValues.MAX_SCAN_DURATION_MS) {
11266
+ this.logDebug("scan_timeout", {
11267
+ elapsedMs: now - this.scanStartTime
11268
+ });
11224
11269
  this.stopVideo();
11225
11270
  this.onExtractedResults({
11226
11271
  success: false,
@@ -11231,7 +11276,16 @@ class ExtractorVideo {
11231
11276
  }
11232
11277
  const canvas = document.createElement("canvas");
11233
11278
  const video = document.getElementById("ScanDocAIVideoElement");
11234
- if (video && video.videoWidth < video.videoHeight) {
11279
+ if (!video) {
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
+ });
11235
11289
  this.showMessage("Please rotate your device to landscape mode.");
11236
11290
  this.candidateImages = [];
11237
11291
  setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
@@ -11250,6 +11304,9 @@ class ExtractorVideo {
11250
11304
  const [isValidationOk, response] = await (0,_requests_validation__WEBPACK_IMPORTED_MODULE_3__["default"])(images.map(e => e.validationImg), this.pastBlurValues, {});
11251
11305
  if (!isValidationOk) {
11252
11306
  const msg = Array.isArray(response) ? response.join(", ") : String(response || "");
11307
+ this.logDebug("validation_failed", {
11308
+ response: msg
11309
+ });
11253
11310
  if (msg.toLowerCase().includes("token expired/invalid")) {
11254
11311
  this.stopVideo();
11255
11312
  this.showMessage("Token expired/invalid. Please provide a new token.", true);
@@ -11264,6 +11321,7 @@ class ExtractorVideo {
11264
11321
  return;
11265
11322
  }
11266
11323
  if (response?.InfoCode === "1006" && response?.TransactionID) {
11324
+ this.logDebug("unsupported_document", response);
11267
11325
  const frontImage = this.candidateImages[this.candidateImages.length - 1].fullImg;
11268
11326
  await (0,_requests_error_logging__WEBPACK_IMPORTED_MODULE_4__["default"])(frontImage, "", response.TransactionID, response.OS, response.Device, response.Browser);
11269
11327
  this.showMessage("Document not supported.", true);
@@ -11294,13 +11352,11 @@ class ExtractorVideo {
11294
11352
  this.hasShownTurnMessage = true;
11295
11353
  this.scanStartTime = Date.now();
11296
11354
  this.showMessage("Turn to the other side", true);
11297
-
11298
- // Clear lock after 3 seconds
11299
11355
  clearTimeout(this.turnMessageTimer);
11300
11356
  this.turnMessageTimer = setTimeout(() => {
11301
11357
  this.hasShownTurnMessage = false;
11302
11358
  this.waitingForSecondSide = false;
11303
- this.lastMessage = null; // allow same message again later
11359
+ this.lastMessage = null;
11304
11360
  }, 3000);
11305
11361
  } else if (Object.keys(this.extractionImages).length === 2) {
11306
11362
  this.waitingForSecondSide = false;
@@ -11347,6 +11403,10 @@ class ExtractorVideo {
11347
11403
  return;
11348
11404
  }
11349
11405
  this.candidateImages = [];
11406
+ }).catch(err => {
11407
+ this.logDebug("document_detector_error", {
11408
+ message: err?.message || String(err)
11409
+ });
11350
11410
  }).finally(() => {
11351
11411
  setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
11352
11412
  });
@@ -11367,7 +11427,7 @@ class ExtractorVideo {
11367
11427
  this.turnMessageTimer = null;
11368
11428
  this.stopVideo();
11369
11429
  if (isExtractionOk) {
11370
- this.showMessage("Success - data extracted", true);
11430
+ this.showMessage("Success - data extracted", "success");
11371
11431
  const shouldStop = this.onExtractedResults({
11372
11432
  success: true,
11373
11433
  code: "001",
@@ -11375,7 +11435,7 @@ class ExtractorVideo {
11375
11435
  data: extractionData
11376
11436
  });
11377
11437
  if (shouldStop === true) {
11378
- setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
11438
+ setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
11379
11439
  }
11380
11440
  } else {
11381
11441
  this.onExtractedResults({
@@ -11383,11 +11443,428 @@ class ExtractorVideo {
11383
11443
  code: "005",
11384
11444
  info: "Document validation passed but extraction failed."
11385
11445
  });
11386
- setTimeout(() => this.startVideo(), ExtractorVideo.FREQUENCY_MS);
11446
+ setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
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;
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
+ }
11387
11862
  }
11863
+ throw new Error("Could not start a suitable camera.");
11388
11864
  }
11389
11865
  async startVideo() {
11390
11866
  try {
11867
+ this.logDebug("startVideo_begin");
11391
11868
  const serviceConfig = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfig)();
11392
11869
  await serviceConfig.getAccessToken(false);
11393
11870
  const videoElem = document.getElementById("ScanDocAIVideoElement");
@@ -11399,63 +11876,80 @@ class ExtractorVideo {
11399
11876
  this.scanStartTime = Date.now();
11400
11877
  const cfgValues = (0,_config__WEBPACK_IMPORTED_MODULE_1__.getScanDocAIConfigValues)();
11401
11878
  const mode = cfgValues.VIDEO_FACING_MODE ?? "environment";
11402
- let stream;
11403
- try {
11404
- stream = await navigator.mediaDevices.getUserMedia(buildDocumentScanConstraints(mode));
11405
- } catch (e) {
11406
- console.warn("Preferred camera constraints failed, falling back:", e);
11407
- stream = await navigator.mediaDevices.getUserMedia({
11408
- video: true,
11409
- audio: false
11410
- });
11411
- }
11412
- this.video.srcObject = stream;
11879
+ const {
11880
+ chosen,
11881
+ debug
11882
+ } = await this.pickBestPhoneCamera(mode);
11883
+ this.video.srcObject = chosen.stream;
11884
+ this.logDebug("srcObject_assigned", {
11885
+ label: chosen.label,
11886
+ settings: chosen.settings
11887
+ });
11413
11888
  try {
11414
11889
  await this.video.play();
11890
+ this.logDebug("video_play_success", {
11891
+ readyState: this.video.readyState,
11892
+ videoWidth: this.video.videoWidth,
11893
+ videoHeight: this.video.videoHeight
11894
+ });
11415
11895
  } catch (e) {
11416
- console.warn(`Error on video play: ${e}`);
11417
- }
11418
-
11419
- // Try to improve document focus after stream is active
11420
- try {
11421
- const focusResult = await tryEnableDocumentFocus(stream);
11422
- console.log("Focus result:", focusResult);
11423
- } catch (focusError) {
11424
- console.warn("Could not apply focus settings:", focusError);
11425
- }
11426
-
11427
- // Avoid re-binding listeners every start
11428
- if (!this._boundLoadedDataHandler) {
11429
- this._boundLoadedDataHandler = () => this.adjustOverlayPosition();
11430
- this.video.addEventListener("loadeddata", this._boundLoadedDataHandler);
11431
- }
11432
- if (!this._boundResizeHandler) {
11433
- this._boundResizeHandler = () => this.adjustOverlayPosition();
11434
- window.addEventListener("resize", this._boundResizeHandler);
11896
+ this.logDebug("video_play_error", {
11897
+ name: e?.name,
11898
+ message: e?.message || String(e)
11899
+ });
11900
+ throw e;
11435
11901
  }
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());
11436
11920
  setTimeout(() => this.adjustOverlayPosition(), 500);
11437
11921
  this.scanStartTime = Date.now();
11438
11922
  setTimeout(() => this.analyzeVideoStream(), ExtractorVideo.FREQUENCY_MS);
11439
11923
  this.showMessage("Starting scanning");
11924
+ this.logDebug("startVideo_success", this.getDebugDump());
11440
11925
  return true;
11441
11926
  } catch (error) {
11442
11927
  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
+ });
11443
11933
  this.isRunning = false;
11444
11934
  this.stopVideo();
11445
11935
  this.onExtractedResults({
11446
11936
  success: false,
11447
11937
  code: error.status === 401 || error.status === 403 ? "004" : "005",
11448
- info: error.status === 401 || error.status === 403 ? "Authentication failed: Invalid API key/token." : "Startup failed: " + (error.message || "Unknown error")
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
11449
11940
  });
11450
11941
  return false;
11451
11942
  }
11452
11943
  }
11453
11944
  stopVideo() {
11454
11945
  this.isRunning = false;
11946
+ this.logDebug("stopVideo_called");
11455
11947
  if (this.video) {
11456
11948
  this.video.pause();
11457
11949
  if (this.video.srcObject !== undefined && this.video.srcObject !== null) {
11458
- this.video.srcObject.getTracks().forEach(t => t.stop());
11950
+ try {
11951
+ this.video.srcObject.getTracks().forEach(t => t.stop());
11952
+ } catch (_) {}
11459
11953
  }
11460
11954
  this.video.srcObject = null;
11461
11955
  }
@@ -11482,7 +11976,7 @@ class ExtractorVideo {
11482
11976
  const centerX = offsetX + scaleWidth / 2;
11483
11977
  const centerY = offsetY + scaleHeight / 2;
11484
11978
  dot.style.display = "block";
11485
- dot.style.left = `${centerX - 8}px`; // center minus half dot size
11979
+ dot.style.left = `${centerX - 8}px`;
11486
11980
  dot.style.top = `${centerY - 8}px`;
11487
11981
  }
11488
11982
  getHTML() {
@@ -11491,7 +11985,6 @@ class ExtractorVideo {
11491
11985
  const messageColor = cfgValues.VIDEO_COLORS?.messageColor;
11492
11986
  const isMobile = window.innerWidth < 768 || window.innerHeight > window.innerWidth && window.innerWidth < 1024;
11493
11987
  if (isMobile) {
11494
- // Mobile version with overlay feedback
11495
11988
  return `
11496
11989
  <div class="mobileVideoArea">
11497
11990
  <video id="ScanDocAIVideoElement" class="mobileVideo" autoplay muted playsinline></video>
@@ -11577,13 +12070,11 @@ class ExtractorVideo {
11577
12070
  margin-left: 20%;
11578
12071
  margin-right: 20%;
11579
12072
  }
11580
-
11581
12073
  .desktopVideoHolder {
11582
12074
  position: relative;
11583
12075
  width: 100%;
11584
12076
  overflow: hidden;
11585
12077
  }
11586
-
11587
12078
  .desktopVideo {
11588
12079
  width: 100%;
11589
12080
  height: auto;
@@ -11592,7 +12083,6 @@ class ExtractorVideo {
11592
12083
  max-width: 100vw;
11593
12084
  max-height: 100vh;
11594
12085
  }
11595
-
11596
12086
  .centerGuideDot {
11597
12087
  position: absolute;
11598
12088
  width: 16px;
@@ -11603,14 +12093,12 @@ class ExtractorVideo {
11603
12093
  z-index: 4;
11604
12094
  animation: flicker 1s infinite;
11605
12095
  pointer-events: none;
11606
- display: none; /* Hidden until video is ready */
12096
+ display: none;
11607
12097
  }
11608
-
11609
12098
  @keyframes flicker {
11610
12099
  0%, 100% { opacity: 1; }
11611
12100
  50% { opacity: 0.3; }
11612
12101
  }
11613
-
11614
12102
  .backgroundOverlay {
11615
12103
  position: absolute;
11616
12104
  top: 0;
@@ -11621,7 +12109,6 @@ class ExtractorVideo {
11621
12109
  z-index: 2;
11622
12110
  pointer-events: none;
11623
12111
  }
11624
-
11625
12112
  .desktopFeedback {
11626
12113
  position: relative;
11627
12114
  display: flex;
@@ -11657,8 +12144,6 @@ function getExtractionVideo(tokenOrCallback, maybeCallback) {
11657
12144
  EXTRACTION_VIDEO = new ExtractorVideo(cb);
11658
12145
  return EXTRACTION_VIDEO;
11659
12146
  }
11660
-
11661
- // If called again, update callback too
11662
12147
  if (typeof cb === "function") {
11663
12148
  EXTRACTION_VIDEO.onExtractedResults = cb;
11664
12149
  } else if (cb !== undefined) {
@@ -11666,143 +12151,6 @@ function getExtractionVideo(tokenOrCallback, maybeCallback) {
11666
12151
  }
11667
12152
  return EXTRACTION_VIDEO;
11668
12153
  }
11669
- function getSupportedConstraintsSafe() {
11670
- try {
11671
- return navigator.mediaDevices?.getSupportedConstraints?.() || {};
11672
- } catch (_) {
11673
- return {};
11674
- }
11675
- }
11676
- function buildDocumentScanConstraints(mode = "environment") {
11677
- const supported = getSupportedConstraintsSafe();
11678
- const video = {};
11679
- if (supported.facingMode) {
11680
- video.facingMode = typeof mode === "string" ? {
11681
- ideal: mode
11682
- } : mode || {
11683
- ideal: "environment"
11684
- };
11685
- }
11686
- if (supported.width) {
11687
- video.width = {
11688
- ideal: 1920
11689
- };
11690
- }
11691
- if (supported.height) {
11692
- video.height = {
11693
- ideal: 1080
11694
- };
11695
- }
11696
- if (supported.aspectRatio) {
11697
- video.aspectRatio = {
11698
- ideal: 16 / 9
11699
- };
11700
- }
11701
- return {
11702
- video,
11703
- audio: false
11704
- };
11705
- }
11706
- async function tryEnableDocumentFocus(stream) {
11707
- const track = stream?.getVideoTracks?.()[0];
11708
- if (!track) {
11709
- return {
11710
- ok: false,
11711
- reason: "No video track found"
11712
- };
11713
- }
11714
- if (!track.getCapabilities || !track.applyConstraints) {
11715
- return {
11716
- ok: false,
11717
- reason: "Focus APIs unavailable on this browser/device",
11718
- settingsBefore: track.getSettings ? track.getSettings() : null
11719
- };
11720
- }
11721
- const caps = track.getCapabilities();
11722
- const result = {
11723
- ok: true,
11724
- capabilities: caps,
11725
- attempts: [],
11726
- settingsBefore: track.getSettings ? track.getSettings() : null
11727
- };
11728
- try {
11729
- let applied = false;
11730
- if (Array.isArray(caps.focusMode) && caps.focusMode.includes("continuous")) {
11731
- await track.applyConstraints({
11732
- advanced: [{
11733
- focusMode: "continuous"
11734
- }]
11735
- });
11736
- result.attempts.push("Applied focusMode=continuous");
11737
- applied = true;
11738
- }
11739
- if (caps.focusDistance) {
11740
- const min = caps.focusDistance.min ?? 0;
11741
- const max = caps.focusDistance.max ?? 0;
11742
- const docDistance = min + (max - min) * 0.3;
11743
- const advanced = [];
11744
- if (Array.isArray(caps.focusMode) && caps.focusMode.includes("manual")) {
11745
- advanced.push({
11746
- focusMode: "manual"
11747
- });
11748
- }
11749
- advanced.push({
11750
- focusDistance: docDistance
11751
- });
11752
- await track.applyConstraints({
11753
- advanced
11754
- });
11755
- result.attempts.push(`Applied focusDistance=${docDistance}`);
11756
- applied = true;
11757
- }
11758
- if (!applied) {
11759
- result.ok = false;
11760
- result.reason = "No supported focus controls were exposed";
11761
- }
11762
- result.settingsAfter = track.getSettings ? track.getSettings() : null;
11763
- return result;
11764
- } catch (err) {
11765
- result.ok = false;
11766
- result.error = err.message || String(err);
11767
- result.settingsAfter = track.getSettings ? track.getSettings() : null;
11768
- return result;
11769
- }
11770
- }
11771
- async function checkAutofocusSupport(environmentCameras) {
11772
- try {
11773
- const camerasWithAutofocus = [];
11774
- for (const camera of environmentCameras) {
11775
- // console.log(camera.deviceId);
11776
- const constraints = {
11777
- video: {
11778
- deviceId: {
11779
- exact: camera.deviceId
11780
- }
11781
- }
11782
- };
11783
-
11784
- // Try to access camera stream with autofocus enabled
11785
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
11786
- const tracks = stream.getVideoTracks();
11787
- const track = tracks[0];
11788
- if (track) {
11789
- // Check if autofocus is supported
11790
- const capabilities = track.getCapabilities();
11791
- // console.log(capabilities);
11792
- if (capabilities.focusMode && capabilities.focusMode.includes("continuous")) {
11793
- camerasWithAutofocus.push(camera);
11794
- }
11795
- }
11796
-
11797
- // Cleanup
11798
- stream.getTracks().forEach(track => track.stop());
11799
- }
11800
- return camerasWithAutofocus;
11801
- } catch (error) {
11802
- console.log("Error checking autofocus support:", error);
11803
- return [];
11804
- }
11805
- }
11806
12154
 
11807
12155
  /***/ }),
11808
12156
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scandoc-ai-components",
3
3
  "author": "ScanDoc-AI",
4
- "version": "0.1.16",
4
+ "version": "0.1.18",
5
5
  "private": false,
6
6
  "description": "Pure JavaScript package for integrating ScanDoc-AI services.",
7
7
  "keywords": [