@scr2em/capacitor-scanner 6.0.29 → 6.0.31
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/README.md +1 -1
- package/android/src/main/java/com/leadliaion/capacitorscanner/CapacitorScannerPlugin.java +166 -126
- package/dist/docs.json +1 -1
- package/dist/esm/definitions.d.ts +7 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorScannerPlugin/CapacitorScannerPlugin.swift +33 -13
- package/ios/Sources/CapacitorScannerPlugin/RectangleDetector.swift +2 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ Set the camera zoom level.
|
|
|
262
262
|
|
|
263
263
|
#### StartOptions
|
|
264
264
|
|
|
265
|
-
<code>{ /** * Barcode formats to detect when barcodeDetection is enabled. */ formats?: BarcodeFormat[]; /** * Camera direction to use. Defaults to 'BACK'. */ cameraDirection?: 'BACK' | 'FRONT'; /** * Enable barcode detection on start. Defaults to false. * When enabled, the camera will automatically detect barcodes and emit 'barcodeScanned' events. */ barcodeDetection?: boolean; /** * Whether to show the highlight overlay for detected barcodes. Defaults to false. * Only applies when barcodeDetection is true. */ barcodeHighlight?: boolean; /** * Enable object detection (business card) on start. Defaults to false. * When enabled, the camera will automatically detect business cards and emit 'rectangleDetected' events. */ objectDetection?: boolean; /** * Whether to show the highlight overlay for detected business cards. Defaults to true. * Only applies when objectDetection is true. */ objectHighlight?: boolean; /** * Minimum interval in seconds between consecutive 'rectangleDetected' events. * Only applies when objectDetection is true. * Defaults to 0 (no throttling beyond initial stability check). */ rectangleEmitIntervalSeconds?: number;
|
|
265
|
+
<code>{ /** * Barcode formats to detect when barcodeDetection is enabled. */ formats?: BarcodeFormat[]; /** * Camera direction to use. Defaults to 'BACK'. */ cameraDirection?: 'BACK' | 'FRONT'; /** * Enable barcode detection on start. Defaults to false. * When enabled, the camera will automatically detect barcodes and emit 'barcodeScanned' events. */ barcodeDetection?: boolean; /** * Whether to show the highlight overlay for detected barcodes. Defaults to false. * Only applies when barcodeDetection is true. */ barcodeHighlight?: boolean; /** * Enable object detection (business card) on start. Defaults to false. * When enabled, the camera will automatically detect business cards and emit 'rectangleDetected' events. */ objectDetection?: boolean; /** * Whether to show the highlight overlay for detected business cards. Defaults to true. * Only applies when objectDetection is true. */ objectHighlight?: boolean; /** * Minimum interval in seconds between consecutive 'rectangleDetected' events. * Only applies when objectDetection is true. * Defaults to 0 (no throttling beyond initial stability check). */ rectangleEmitIntervalSeconds?: number; /** * Optional regex pattern to filter scanned barcodes. * If provided, only barcodes matching this pattern will be reported. */ regex?: string; /** * Optional regex flags (e.g., 'i' for case-insensitive, 'm' for multiline). */ regexFlags?: string; /** * Duration in seconds to pause rectangle detection after a barcode is successfully scanned. * When both barcode and object detection are enabled, this gives barcode scanning priority * by temporarily suppressing rectangle detection after a barcode is emitted. * Defaults to 0 (no throttle, both detections run simultaneously). */ detectionThrottleSeconds?: number; }</code>
|
|
266
266
|
|
|
267
267
|
|
|
268
268
|
#### CapturePhotoOptions
|
|
@@ -143,6 +143,10 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
143
143
|
// Configurable interval between rectangle detection event emissions (in milliseconds)
|
|
144
144
|
private long rectangleEmitIntervalMs = 0; // Default: 0 (no throttling)
|
|
145
145
|
|
|
146
|
+
// Detection throttle: pause rectangle detection after a successful barcode scan
|
|
147
|
+
private long detectionThrottleMs = 0; // Default: 0 (no throttle)
|
|
148
|
+
private long lastSuccessfulBarcodeScanTime = 0;
|
|
149
|
+
|
|
146
150
|
/**
|
|
147
151
|
* Calculates the processing interval in milliseconds based on the desired
|
|
148
152
|
* frequency per second.
|
|
@@ -230,6 +234,19 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
230
234
|
}
|
|
231
235
|
}
|
|
232
236
|
|
|
237
|
+
// Read detection throttle
|
|
238
|
+
if (call.getData().has("detectionThrottleSeconds")) {
|
|
239
|
+
try {
|
|
240
|
+
double throttleSeconds = call.getData().getDouble("detectionThrottleSeconds");
|
|
241
|
+
detectionThrottleMs = Math.max(0, (long) (throttleSeconds * 1000));
|
|
242
|
+
} catch (Exception e) {
|
|
243
|
+
detectionThrottleMs = 0;
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
detectionThrottleMs = 0;
|
|
247
|
+
}
|
|
248
|
+
lastSuccessfulBarcodeScanTime = 0;
|
|
249
|
+
|
|
233
250
|
try {
|
|
234
251
|
String cameraDirectionStr = call.getString("cameraDirection", "BACK");
|
|
235
252
|
int lensFacing;
|
|
@@ -449,152 +466,169 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
449
466
|
|
|
450
467
|
@Override
|
|
451
468
|
public void analyze(@NonNull ImageProxy imageProxy) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// Barcode detection - only process if barcode detection is enabled
|
|
460
|
-
if (scanner != null && enabledDetectionTypes.contains("barcode")) {
|
|
461
|
-
scanner.process(image)
|
|
462
|
-
.addOnSuccessListener(executor, barcodes -> {
|
|
463
|
-
processBarcodes(barcodes);
|
|
464
|
-
// Only display barcode overlay if highlight is enabled
|
|
465
|
-
if (enabledHighlightTypes.contains("barcode")) {
|
|
466
|
-
displayDetectedBarcodes(barcodes, imageProxy.getWidth(), imageProxy.getHeight(),
|
|
467
|
-
imageProxy.getImageInfo().getRotationDegrees());
|
|
468
|
-
}
|
|
469
|
-
})
|
|
470
|
-
.addOnFailureListener(executor, e -> {
|
|
471
|
-
echo("Failed to process image for barcode: " + e.getMessage());
|
|
472
|
-
});
|
|
473
|
-
}
|
|
469
|
+
@ExperimentalGetImage
|
|
470
|
+
android.media.Image mediaImage = imageProxy.getImage();
|
|
471
|
+
if (mediaImage == null) {
|
|
472
|
+
imageProxy.close();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
474
475
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
// Only perform detection if enough time has passed since last detection
|
|
480
|
-
// This throttles processing based on configured frequency
|
|
481
|
-
long currentTime = System.currentTimeMillis();
|
|
482
|
-
long processingIntervalMs = calculateProcessingInterval(processingFrequency);
|
|
483
|
-
if (currentTime - lastRectangleProcessingTime >= processingIntervalMs) {
|
|
484
|
-
// Perform new detection
|
|
485
|
-
echo("Starting rectangle detection for businessCard");
|
|
486
|
-
Bitmap bitmap = null;
|
|
487
|
-
try {
|
|
488
|
-
// Convert ImageProxy to Bitmap using the sample's toBitmap method
|
|
489
|
-
bitmap = imageProxyToBitmap(imageProxy);
|
|
476
|
+
// Capture metadata upfront so it's available after imageProxy is closed
|
|
477
|
+
final int imageWidth = imageProxy.getWidth();
|
|
478
|
+
final int imageHeight = imageProxy.getHeight();
|
|
479
|
+
final int rotationDegrees = imageProxy.getImageInfo().getRotationDegrees();
|
|
490
480
|
|
|
491
|
-
|
|
492
|
-
|
|
481
|
+
try {
|
|
482
|
+
InputImage image = InputImage.fromMediaImage(mediaImage, rotationDegrees);
|
|
483
|
+
|
|
484
|
+
boolean barcodeEnabled = scanner != null && enabledDetectionTypes.contains("barcode");
|
|
485
|
+
boolean isRectangleThrottled = detectionThrottleMs > 0
|
|
486
|
+
&& lastSuccessfulBarcodeScanTime > 0
|
|
487
|
+
&& (System.currentTimeMillis() - lastSuccessfulBarcodeScanTime) < detectionThrottleMs;
|
|
488
|
+
boolean rectangleEnabled = rectangleDetector != null
|
|
489
|
+
&& enabledDetectionTypes.contains("businessCard")
|
|
490
|
+
&& !isRectangleThrottled;
|
|
491
|
+
|
|
492
|
+
// Clear rectangle overlay when throttled
|
|
493
|
+
if (isRectangleThrottled && enabledDetectionTypes.contains("businessCard")) {
|
|
494
|
+
getActivity().runOnUiThread(() -> {
|
|
495
|
+
synchronized (overlayLock) {
|
|
496
|
+
currentRectangleRects.clear();
|
|
497
|
+
updateOverlayDetections(currentBarcodeRects, currentRectangleRects);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
493
501
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
502
|
+
// Rectangle detection (synchronous) - runs first while imageProxy is still open
|
|
503
|
+
if (rectangleEnabled) {
|
|
504
|
+
DetectionResult detectionResult = null;
|
|
505
|
+
|
|
506
|
+
// Only perform detection if enough time has passed since last detection
|
|
507
|
+
// This throttles processing based on configured frequency
|
|
508
|
+
long currentTime = System.currentTimeMillis();
|
|
509
|
+
long processingIntervalMs = calculateProcessingInterval(processingFrequency);
|
|
510
|
+
if (currentTime - lastRectangleProcessingTime >= processingIntervalMs) {
|
|
511
|
+
// Perform new detection
|
|
512
|
+
Bitmap bitmap = null;
|
|
513
|
+
try {
|
|
514
|
+
// Convert ImageProxy to Bitmap using the sample's toBitmap method
|
|
515
|
+
bitmap = imageProxyToBitmap(imageProxy);
|
|
497
516
|
|
|
498
|
-
|
|
499
|
-
List<Rectangle> filteredRectangles = filterRectangles(rawResult.getRectangles(),
|
|
500
|
-
rawResult.getImageSize());
|
|
517
|
+
DetectionResult rawResult = rectangleDetector.detectRectangles(bitmap);
|
|
501
518
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (detectionHistory.size() > DETECTION_WINDOW) {
|
|
506
|
-
detectionHistory.poll();
|
|
507
|
-
}
|
|
508
|
-
if (isHit) {
|
|
509
|
-
lastGoodRects = filteredRectangles; // update last known good
|
|
510
|
-
echo("✅ Hit - saved " + filteredRectangles.size() + " good rectangles");
|
|
511
|
-
} else {
|
|
512
|
-
echo("❌ Miss - keeping " + lastGoodRects.size() + " previous rectangles");
|
|
513
|
-
}
|
|
519
|
+
// Apply geometric filtering to eliminate false positives
|
|
520
|
+
List<Rectangle> filteredRectangles = filterRectangles(rawResult.getRectangles(),
|
|
521
|
+
rawResult.getImageSize());
|
|
514
522
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
boolean shouldShow = hits >= Math.ceil(PRESENCE_THRESHOLD * detectionHistory.size());
|
|
527
|
-
List<Rectangle> rectanglesToShow = shouldShow ? lastGoodRects : Collections.emptyList();
|
|
528
|
-
echo("Window: " + hits + "/" + detectionHistory.size() + " hits - shouldShow: "
|
|
529
|
-
+ shouldShow);
|
|
530
|
-
|
|
531
|
-
// Create detection result with the rectangles we decided to show
|
|
532
|
-
detectionResult = new DetectionResult(rawResult.getImageSize(), rectanglesToShow);
|
|
533
|
-
|
|
534
|
-
// Store the detection result for reuse on non-processing frames
|
|
535
|
-
if (!rectanglesToShow.isEmpty()) {
|
|
536
|
-
lastGoodDetectionResult = detectionResult;
|
|
537
|
-
lastRectangleDetectionTime = currentTime;
|
|
538
|
-
}
|
|
523
|
+
// 2) record hit/miss in sliding window
|
|
524
|
+
boolean isHit = !filteredRectangles.isEmpty();
|
|
525
|
+
detectionHistory.add(isHit);
|
|
526
|
+
if (detectionHistory.size() > DETECTION_WINDOW) {
|
|
527
|
+
detectionHistory.poll();
|
|
528
|
+
}
|
|
529
|
+
if (isHit) {
|
|
530
|
+
lastGoodRects = filteredRectangles; // update last known good
|
|
531
|
+
}
|
|
539
532
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
533
|
+
// 3) decide whether to show based on sliding window
|
|
534
|
+
long hits = 0;
|
|
535
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
536
|
+
hits = detectionHistory.stream().filter(b -> b).count();
|
|
537
|
+
} else {
|
|
538
|
+
// For older Android versions, count manually
|
|
539
|
+
for (Boolean hit : detectionHistory) {
|
|
540
|
+
if (hit)
|
|
541
|
+
hits++;
|
|
544
542
|
}
|
|
543
|
+
}
|
|
544
|
+
boolean shouldShow = hits >= Math.ceil(PRESENCE_THRESHOLD * detectionHistory.size());
|
|
545
|
+
List<Rectangle> rectanglesToShow = shouldShow ? lastGoodRects : Collections.emptyList();
|
|
545
546
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
} finally {
|
|
554
|
-
// Cleanup bitmap resource in all code paths
|
|
555
|
-
if (bitmap != null) {
|
|
556
|
-
bitmap.recycle();
|
|
557
|
-
}
|
|
547
|
+
// Create detection result with the rectangles we decided to show
|
|
548
|
+
detectionResult = new DetectionResult(rawResult.getImageSize(), rectanglesToShow);
|
|
549
|
+
|
|
550
|
+
// Store the detection result for reuse on non-processing frames
|
|
551
|
+
if (!rectanglesToShow.isEmpty()) {
|
|
552
|
+
lastGoodDetectionResult = detectionResult;
|
|
553
|
+
lastRectangleDetectionTime = currentTime;
|
|
558
554
|
}
|
|
559
|
-
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
(currentTime - lastRectangleDetectionTime) < CACHE_MS) {
|
|
565
|
-
detectionResult = lastGoodDetectionResult;
|
|
566
|
-
echo("Reusing last good detection result (age: "
|
|
567
|
-
+ (currentTime - lastRectangleProcessingTime) + "ms)");
|
|
568
|
-
} else {
|
|
569
|
-
// Create empty result if we have no recent good detection
|
|
570
|
-
detectionResult = new DetectionResult(
|
|
571
|
-
new Size(imageProxy.getWidth(), imageProxy.getHeight()), new ArrayList<>());
|
|
555
|
+
|
|
556
|
+
// Process rectangle detection results to emit events if we're showing
|
|
557
|
+
// rectangles
|
|
558
|
+
if (!rectanglesToShow.isEmpty()) {
|
|
559
|
+
processRectangles(detectionResult);
|
|
572
560
|
}
|
|
573
|
-
}
|
|
574
561
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
562
|
+
// Update last processing time
|
|
563
|
+
lastRectangleProcessingTime = currentTime;
|
|
564
|
+
} catch (Exception e) {
|
|
565
|
+
echo("Failed to process image for rectangle detection: " + e.getMessage());
|
|
566
|
+
e.printStackTrace();
|
|
567
|
+
detectionResult = new DetectionResult(
|
|
568
|
+
new Size(imageWidth, imageHeight), new ArrayList<>());
|
|
569
|
+
} finally {
|
|
570
|
+
// Cleanup bitmap resource in all code paths
|
|
571
|
+
if (bitmap != null) {
|
|
572
|
+
bitmap.recycle();
|
|
586
573
|
}
|
|
587
|
-
}
|
|
574
|
+
}
|
|
588
575
|
} else {
|
|
589
|
-
//
|
|
590
|
-
//
|
|
576
|
+
// For frames we're not processing, reuse the last good detection result
|
|
577
|
+
// This ensures the UI stays smooth even though we're only detecting 10
|
|
578
|
+
// times/second
|
|
579
|
+
if (lastGoodDetectionResult != null &&
|
|
580
|
+
(currentTime - lastRectangleDetectionTime) < CACHE_MS) {
|
|
581
|
+
detectionResult = lastGoodDetectionResult;
|
|
582
|
+
} else {
|
|
583
|
+
// Create empty result if we have no recent good detection
|
|
584
|
+
detectionResult = new DetectionResult(
|
|
585
|
+
new Size(imageWidth, imageHeight), new ArrayList<>());
|
|
586
|
+
}
|
|
591
587
|
}
|
|
588
|
+
|
|
589
|
+
// Always display detection results for every frame to keep UI smooth
|
|
590
|
+
// This runs for both freshly detected rectangles and reused detection results
|
|
591
|
+
final DetectionResult finalResult = detectionResult;
|
|
592
|
+
getActivity().runOnUiThread(() -> {
|
|
593
|
+
// Only display rectangle overlay if highlight is enabled
|
|
594
|
+
if (enabledHighlightTypes.contains("businessCard")) {
|
|
595
|
+
// Show the rectangles
|
|
596
|
+
displayDetectedRectangles(finalResult,
|
|
597
|
+
imageWidth,
|
|
598
|
+
imageHeight,
|
|
599
|
+
rotationDegrees);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Barcode detection (async) - close imageProxy only after ML Kit finishes
|
|
605
|
+
// processing. The imageProxy must stay open so ML Kit can read the underlying
|
|
606
|
+
// image buffer.
|
|
607
|
+
if (barcodeEnabled) {
|
|
608
|
+
scanner.process(image)
|
|
609
|
+
.addOnSuccessListener(executor, barcodes -> {
|
|
610
|
+
processBarcodes(barcodes);
|
|
611
|
+
// Only display barcode overlay if highlight is enabled
|
|
612
|
+
if (enabledHighlightTypes.contains("barcode")) {
|
|
613
|
+
displayDetectedBarcodes(barcodes, imageWidth, imageHeight,
|
|
614
|
+
rotationDegrees);
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
.addOnFailureListener(executor, e -> {
|
|
618
|
+
echo("Failed to process image for barcode: " + e.getMessage());
|
|
619
|
+
})
|
|
620
|
+
.addOnCompleteListener(executor, task -> {
|
|
621
|
+
imageProxy.close();
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
// No async work pending — safe to close now
|
|
625
|
+
imageProxy.close();
|
|
592
626
|
}
|
|
593
627
|
} catch (Exception e) {
|
|
594
628
|
echo("Unexpected error in image analysis: " + e.getMessage());
|
|
595
629
|
e.printStackTrace();
|
|
630
|
+
imageProxy.close();
|
|
596
631
|
}
|
|
597
|
-
// Always close the ImageProxy, in all code paths
|
|
598
632
|
}
|
|
599
633
|
}
|
|
600
634
|
|
|
@@ -874,6 +908,10 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
874
908
|
data.put("scannedCode", rawValue);
|
|
875
909
|
data.put("format", BarcodeFormatHelper.barcodeFormatToString(format));
|
|
876
910
|
notifyListeners("barcodeScanned", data, true);
|
|
911
|
+
|
|
912
|
+
// Record successful scan time for detection throttle
|
|
913
|
+
lastSuccessfulBarcodeScanTime = System.currentTimeMillis();
|
|
914
|
+
|
|
877
915
|
echo("Barcode " + rawValue + " scanned with format: "
|
|
878
916
|
+ BarcodeFormatHelper.barcodeFormatToString(format));
|
|
879
917
|
}
|
|
@@ -1148,6 +1186,8 @@ public class CapacitorScannerPlugin extends Plugin {
|
|
|
1148
1186
|
lastRectangleProcessingTime = 0;
|
|
1149
1187
|
lastRectangleDetectionTime = 0;
|
|
1150
1188
|
rectangleEmitIntervalMs = 0;
|
|
1189
|
+
detectionThrottleMs = 0;
|
|
1190
|
+
lastSuccessfulBarcodeScanTime = 0;
|
|
1151
1191
|
|
|
1152
1192
|
// Clear detection histories and caches
|
|
1153
1193
|
enabledDetectionTypes.clear();
|
package/dist/docs.json
CHANGED
|
@@ -344,7 +344,7 @@
|
|
|
344
344
|
"docs": "",
|
|
345
345
|
"types": [
|
|
346
346
|
{
|
|
347
|
-
"text": "{\n /**\n * Barcode formats to detect when barcodeDetection is enabled.\n */\n formats?: BarcodeFormat[];\n /**\n * Camera direction to use. Defaults to 'BACK'.\n */\n cameraDirection?: 'BACK' | 'FRONT';\n /**\n * Enable barcode detection on start. Defaults to false.\n * When enabled, the camera will automatically detect barcodes and emit 'barcodeScanned' events.\n */\n barcodeDetection?: boolean;\n /**\n * Whether to show the highlight overlay for detected barcodes. Defaults to false.\n * Only applies when barcodeDetection is true.\n */\n barcodeHighlight?: boolean;\n /**\n * Enable object detection (business card) on start. Defaults to false.\n * When enabled, the camera will automatically detect business cards and emit 'rectangleDetected' events.\n */\n objectDetection?: boolean;\n /**\n * Whether to show the highlight overlay for detected business cards. Defaults to true.\n * Only applies when objectDetection is true.\n */\n objectHighlight?: boolean;\n /**\n * Minimum interval in seconds between consecutive 'rectangleDetected' events.\n * Only applies when objectDetection is true.\n * Defaults to 0 (no throttling beyond initial stability check).\n */\n rectangleEmitIntervalSeconds?: number;\n
|
|
347
|
+
"text": "{\n /**\n * Barcode formats to detect when barcodeDetection is enabled.\n */\n formats?: BarcodeFormat[];\n /**\n * Camera direction to use. Defaults to 'BACK'.\n */\n cameraDirection?: 'BACK' | 'FRONT';\n /**\n * Enable barcode detection on start. Defaults to false.\n * When enabled, the camera will automatically detect barcodes and emit 'barcodeScanned' events.\n */\n barcodeDetection?: boolean;\n /**\n * Whether to show the highlight overlay for detected barcodes. Defaults to false.\n * Only applies when barcodeDetection is true.\n */\n barcodeHighlight?: boolean;\n /**\n * Enable object detection (business card) on start. Defaults to false.\n * When enabled, the camera will automatically detect business cards and emit 'rectangleDetected' events.\n */\n objectDetection?: boolean;\n /**\n * Whether to show the highlight overlay for detected business cards. Defaults to true.\n * Only applies when objectDetection is true.\n */\n objectHighlight?: boolean;\n /**\n * Minimum interval in seconds between consecutive 'rectangleDetected' events.\n * Only applies when objectDetection is true.\n * Defaults to 0 (no throttling beyond initial stability check).\n */\n rectangleEmitIntervalSeconds?: number;\n /**\n * Optional regex pattern to filter scanned barcodes.\n * If provided, only barcodes matching this pattern will be reported.\n */\n regex?: string;\n /**\n * Optional regex flags (e.g., 'i' for case-insensitive, 'm' for multiline).\n */\n regexFlags?: string;\n /**\n * Duration in seconds to pause rectangle detection after a barcode is successfully scanned.\n * When both barcode and object detection are enabled, this gives barcode scanning priority\n * by temporarily suppressing rectangle detection after a barcode is emitted.\n * Defaults to 0 (no throttle, both detections run simultaneously).\n */\n detectionThrottleSeconds?: number;\n}",
|
|
348
348
|
"complexTypes": [
|
|
349
349
|
"BarcodeFormat"
|
|
350
350
|
]
|
|
@@ -105,7 +105,6 @@ export declare type StartOptions = {
|
|
|
105
105
|
* Defaults to 0 (no throttling beyond initial stability check).
|
|
106
106
|
*/
|
|
107
107
|
rectangleEmitIntervalSeconds?: number;
|
|
108
|
-
debounceTimeInMilli?: number;
|
|
109
108
|
/**
|
|
110
109
|
* Optional regex pattern to filter scanned barcodes.
|
|
111
110
|
* If provided, only barcodes matching this pattern will be reported.
|
|
@@ -115,6 +114,13 @@ export declare type StartOptions = {
|
|
|
115
114
|
* Optional regex flags (e.g., 'i' for case-insensitive, 'm' for multiline).
|
|
116
115
|
*/
|
|
117
116
|
regexFlags?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Duration in seconds to pause rectangle detection after a barcode is successfully scanned.
|
|
119
|
+
* When both barcode and object detection are enabled, this gives barcode scanning priority
|
|
120
|
+
* by temporarily suppressing rectangle detection after a barcode is emitted.
|
|
121
|
+
* Defaults to 0 (no throttle, both detections run simultaneously).
|
|
122
|
+
*/
|
|
123
|
+
detectionThrottleSeconds?: number;
|
|
118
124
|
};
|
|
119
125
|
export declare type BarcodeScannedEvent = {
|
|
120
126
|
scannedCode: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAyKA,MAAM,CAAN,IAAY,aAYX;AAZD,WAAY,aAAa;IACvB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,qCAAoB,CAAA;IACpB,2CAA0B,CAAA;IAC1B,+BAAc,CAAA;IACd,iCAAgB,CAAA;IAChB,gCAAe,CAAA;IACf,mCAAkB,CAAA;IAClB,mCAAkB,CAAA;IAClB,+BAAc,CAAA;AAChB,CAAC,EAZW,aAAa,KAAb,aAAa,QAYxB","sourcesContent":["export interface CapacitorScannerPlugin {\n /**\n * Start the camera preview with optional barcode and object detection.\n * @param options - Configuration options for the camera and detection\n */\n start(options?: StartOptions): Promise<void>;\n\n /**\n * Stop the camera preview and all detection.\n */\n stop(): Promise<void>;\n\n openSettings(): Promise<void>;\n\n capturePhoto(options?: CapturePhotoOptions): Promise<CapturePhotoResult>;\n\n checkPermissions(): Promise<PermissionsResult>;\n\n requestPermissions(): Promise<PermissionsResult>;\n\n flipCamera(): Promise<void>;\n\n toggleFlash(): Promise<FlashResult>;\n\n addListener(event: 'barcodeScanned', listenerFunc: (result: BarcodeScannedEvent) => void): Promise<void>;\n\n addListener(event: 'rectangleDetected', listenerFunc: (result: RectangleDetectedEvent) => void): Promise<void>;\n\n removeAllListeners(): Promise<void>;\n\n /**\n * Enable object detection (business card). This will start detecting business cards.\n * @param options - Optional configuration for object detection\n */\n enableObjectDetection(options?: ObjectDetectionOptions): Promise<{ enabled: true }>;\n\n /**\n * Disable object detection. This will stop detecting business cards.\n */\n disableObjectDetection(): Promise<{ enabled: false }>;\n\n /**\n * Enable barcode detection. This will start detecting barcodes.\n * @param options - Optional configuration for barcode detection\n */\n enableBarcodeDetection(options?: BarcodeDetectionOptions): Promise<{ enabled: true }>;\n\n /**\n * Disable barcode detection. This will stop detecting barcodes and clear cached barcodes.\n */\n disableBarcodeDetection(): Promise<{ enabled: false }>;\n\n /**\n * Set the camera zoom level.\n * @param options - The zoom options containing the level (1, 2, or 3)\n */\n zoom(options: ZoomOptions): Promise<ZoomResult>;\n}\n\nexport type BarcodeDetectionOptions = {\n /**\n * Whether to show the highlight overlay for detected barcodes.\n * Defaults to false.\n */\n showHighlight?: boolean;\n}\n\nexport type ObjectDetectionOptions = {\n /**\n * Whether to show the highlight overlay for detected business cards.\n * Defaults to true.\n */\n showHighlight?: boolean,\n /**\n * Minimum interval in seconds between consecutive 'rectangleDetected' events.\n * After an event is emitted, no new events will be emitted until this interval passes.\n * Set to 0 to emit events as soon as stability is detected.\n * Defaults to 0 (no throttling beyond initial stability check).\n */\n emitIntervalSeconds?: number\n}\n\nexport type StartOptions = {\n /**\n * Barcode formats to detect when barcodeDetection is enabled.\n */\n formats?: BarcodeFormat[];\n /**\n * Camera direction to use. Defaults to 'BACK'.\n */\n cameraDirection?: 'BACK' | 'FRONT';\n /**\n * Enable barcode detection on start. Defaults to false.\n * When enabled, the camera will automatically detect barcodes and emit 'barcodeScanned' events.\n */\n barcodeDetection?: boolean;\n /**\n * Whether to show the highlight overlay for detected barcodes. Defaults to false.\n * Only applies when barcodeDetection is true.\n */\n barcodeHighlight?: boolean;\n /**\n * Enable object detection (business card) on start. Defaults to false.\n * When enabled, the camera will automatically detect business cards and emit 'rectangleDetected' events.\n */\n objectDetection?: boolean;\n /**\n * Whether to show the highlight overlay for detected business cards. Defaults to true.\n * Only applies when objectDetection is true.\n */\n objectHighlight?: boolean;\n /**\n * Minimum interval in seconds between consecutive 'rectangleDetected' events.\n * Only applies when objectDetection is true.\n * Defaults to 0 (no throttling beyond initial stability check).\n */\n rectangleEmitIntervalSeconds?: number;\n /**\n * Optional regex pattern to filter scanned barcodes.\n * If provided, only barcodes matching this pattern will be reported.\n */\n regex?: string;\n /**\n * Optional regex flags (e.g., 'i' for case-insensitive, 'm' for multiline).\n */\n regexFlags?: string;\n /**\n * Duration in seconds to pause rectangle detection after a barcode is successfully scanned.\n * When both barcode and object detection are enabled, this gives barcode scanning priority\n * by temporarily suppressing rectangle detection after a barcode is emitted.\n * Defaults to 0 (no throttle, both detections run simultaneously).\n */\n detectionThrottleSeconds?: number;\n};\n\nexport type BarcodeScannedEvent = { scannedCode: string; format: string };\n\n\nexport type RectangleDetectedEvent = {\n detected: true\n};\n\nexport type CapturePhotoOptions = {\n /**\n * The desired quality of the captured image, expressed as a value between 0.0 (lowest quality, smallest file size)\n * and 1.0 (highest quality, largest file size). Defaults to 1.0.\n * This parameter directly influences the compression level of the resulting JPEG image.\n */\n qualityRatio?: number;\n};\n\nexport type PermissionsResult = { camera: 'prompt' | 'denied' | 'granted' };\n\nexport type CapturePhotoResult = { imageBase64: string };\n\nexport type FlashResult = { enabled: boolean };\n\nexport type ZoomOptions = {\n /**\n * The zoom level to set. Must be 1, 2, or 3.\n * - 1 = 1.0x (no zoom)\n * - 2 = 2.0x\n * - 3 = 3.0x\n */\n level: 1 | 2 | 3;\n};\n\nexport type ZoomResult = { level: number };\n\nexport enum BarcodeFormat {\n Aztec = 'AZTEC',\n Code39 = 'CODE_39',\n Code93 = 'CODE_93',\n Code128 = 'CODE_128',\n DataMatrix = 'DATA_MATRIX',\n Ean8 = 'EAN_8',\n Ean13 = 'EAN_13',\n Itf14 = 'ITF14',\n Pdf417 = 'PDF_417',\n QrCode = 'QR_CODE',\n UpcE = 'UPC_E',\n}\n\ndeclare global {\n interface PluginRegistry {\n QRScanner: CapacitorScannerPlugin;\n }\n}"]}
|
|
@@ -51,6 +51,10 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
|
|
|
51
51
|
private var lastBarcodeDetectionTime: Date?
|
|
52
52
|
private let barcodeVisibilityTimeout: TimeInterval = 1.0
|
|
53
53
|
|
|
54
|
+
// Detection throttle: pause rectangle detection after a successful barcode scan
|
|
55
|
+
private var detectionThrottleSeconds: TimeInterval = 0
|
|
56
|
+
private var lastSuccessfulBarcodeScanTime: Date?
|
|
57
|
+
|
|
54
58
|
// Detection state (controls what detection runs)
|
|
55
59
|
private var enabledDetectionTypes: Set<String> = []
|
|
56
60
|
// Highlight state (controls what overlay is shown)
|
|
@@ -129,10 +133,8 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
|
|
|
129
133
|
private func setupOverlayView() {
|
|
130
134
|
guard let cameraView = self.cameraView,
|
|
131
135
|
let previewLayer = self.previewLayer else {
|
|
132
|
-
print("[CapacitorScanner] setupOverlayView: cameraView or previewLayer is nil")
|
|
133
136
|
return
|
|
134
137
|
}
|
|
135
|
-
print("[CapacitorScanner] setupOverlayView: setting up overlay")
|
|
136
138
|
|
|
137
139
|
// Remove existing overlay if present
|
|
138
140
|
self.overlayView?.removeFromSuperview()
|
|
@@ -466,6 +468,10 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
|
|
|
466
468
|
self.rectangleDetector?.setEmitInterval(emitInterval)
|
|
467
469
|
}
|
|
468
470
|
|
|
471
|
+
// Read detection throttle
|
|
472
|
+
self.detectionThrottleSeconds = call.getDouble("detectionThrottleSeconds") ?? 0
|
|
473
|
+
self.lastSuccessfulBarcodeScanTime = nil
|
|
474
|
+
|
|
469
475
|
// Handle Regex Filter
|
|
470
476
|
if let pattern = call.getString("regex") {
|
|
471
477
|
let flags = call.getString("regexFlags") ?? ""
|
|
@@ -598,6 +604,8 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
|
|
|
598
604
|
self.enabledDetectionTypes.removeAll()
|
|
599
605
|
self.enabledHighlightTypes.removeAll()
|
|
600
606
|
self.lastBarcodeDetectionTime = nil
|
|
607
|
+
self.lastSuccessfulBarcodeScanTime = nil
|
|
608
|
+
self.detectionThrottleSeconds = 0
|
|
601
609
|
self.removeOrientationChangeObserver()
|
|
602
610
|
}
|
|
603
611
|
|
|
@@ -813,6 +821,9 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
|
|
|
813
821
|
"format": CapacitorScannerHelpers.convertBarcodeScannerFormatToString(bestObservation.symbology),
|
|
814
822
|
])
|
|
815
823
|
|
|
824
|
+
// Record successful scan time for detection throttle
|
|
825
|
+
self.lastSuccessfulBarcodeScanTime = Date()
|
|
826
|
+
|
|
816
827
|
// Reset votes after successful scan
|
|
817
828
|
self.scannedCodesVotes.clear()
|
|
818
829
|
}
|
|
@@ -825,7 +836,6 @@ public class CapacitorScannerPlugin: CAPPlugin, CAPBridgedPlugin, AVCaptureMetad
|
|
|
825
836
|
// MARK: - RectangleDetectorDelegate
|
|
826
837
|
|
|
827
838
|
func rectangleDetector(_ detector: RectangleDetector, didDetect corners: RectangleCorners) {
|
|
828
|
-
print("[CapacitorScanner] rectangleDetector delegate called - emitting rectangleDetected event")
|
|
829
839
|
self.notifyListeners("rectangleDetected", data: [
|
|
830
840
|
"topLeft": ["x": corners.topLeft.x, "y": corners.topLeft.y],
|
|
831
841
|
"topRight": ["x": corners.topRight.x, "y": corners.topRight.y],
|
|
@@ -1027,9 +1037,15 @@ extension CapacitorScannerPlugin: AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
|
1027
1037
|
requests.append(self.barcodeDetectionRequest)
|
|
1028
1038
|
}
|
|
1029
1039
|
|
|
1030
|
-
|
|
1040
|
+
let isRectangleThrottled: Bool = {
|
|
1041
|
+
guard self.detectionThrottleSeconds > 0,
|
|
1042
|
+
let lastScan = self.lastSuccessfulBarcodeScanTime else { return false }
|
|
1043
|
+
return Date().timeIntervalSince(lastScan) < self.detectionThrottleSeconds
|
|
1044
|
+
}()
|
|
1045
|
+
|
|
1046
|
+
if !isRectangleThrottled,
|
|
1047
|
+
self.enabledDetectionTypes.contains("businessCard"),
|
|
1031
1048
|
let detector = self.rectangleDetector {
|
|
1032
|
-
print("[CapacitorScanner] Processing frame for businessCard detection")
|
|
1033
1049
|
detector.updatePixelBuffer(pixelBuffer)
|
|
1034
1050
|
requests.append(detector.visionRequest)
|
|
1035
1051
|
}
|
|
@@ -1041,15 +1057,19 @@ extension CapacitorScannerPlugin: AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
|
1041
1057
|
print("Failed to perform Vision request: \(error)")
|
|
1042
1058
|
}
|
|
1043
1059
|
|
|
1044
|
-
// Check overlay timeouts
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
self.
|
|
1060
|
+
// Check overlay timeouts only when there's something to clean up
|
|
1061
|
+
let needsBarcodeCheck = self.barcodeLayer != nil
|
|
1062
|
+
let needsRectCheck = self.rectangleDetector != nil
|
|
1063
|
+
if needsBarcodeCheck || needsRectCheck {
|
|
1064
|
+
DispatchQueue.main.async {
|
|
1065
|
+
let now = Date()
|
|
1066
|
+
if let t = self.lastBarcodeDetectionTime, now.timeIntervalSince(t) > self.barcodeVisibilityTimeout {
|
|
1067
|
+
self.barcodeLayer?.removeFromSuperlayer()
|
|
1068
|
+
self.barcodeLayer = nil
|
|
1069
|
+
self.prevBarcodeRect = nil
|
|
1070
|
+
}
|
|
1071
|
+
self.rectangleDetector?.checkTimeout()
|
|
1051
1072
|
}
|
|
1052
|
-
self.rectangleDetector?.checkTimeout()
|
|
1053
1073
|
}
|
|
1054
1074
|
}
|
|
1055
1075
|
}
|
|
@@ -41,6 +41,7 @@ class RectangleDetector {
|
|
|
41
41
|
private(set) var lastDetectedRectangle: DetectedRectangle?
|
|
42
42
|
private(set) var lastDetectionTime: Date?
|
|
43
43
|
private var currentPixelBuffer: CVPixelBuffer?
|
|
44
|
+
private let ciContext = CIContext(options: nil)
|
|
44
45
|
|
|
45
46
|
// Stability tracking
|
|
46
47
|
private var previousCorners: RectangleCorners?
|
|
@@ -73,7 +74,6 @@ class RectangleDetector {
|
|
|
73
74
|
|
|
74
75
|
/// Configure the detector with overlay support
|
|
75
76
|
func configure(overlayLayer: CALayer, previewLayer: AVCaptureVideoPreviewLayer) {
|
|
76
|
-
print("[RectangleDetector] configure called, isHighlightEnabled: \(isHighlightEnabled)")
|
|
77
77
|
self.overlayParentLayer = overlayLayer
|
|
78
78
|
self.previewLayer = previewLayer
|
|
79
79
|
|
|
@@ -132,41 +132,31 @@ class RectangleDetector {
|
|
|
132
132
|
|
|
133
133
|
private func setupSmoother() {
|
|
134
134
|
guard let parentLayer = overlayParentLayer, smoother == nil else {
|
|
135
|
-
print("[RectangleDetector] setupSmoother: skipped (parentLayer nil: \(overlayParentLayer == nil), smoother exists: \(smoother != nil))")
|
|
136
135
|
return
|
|
137
136
|
}
|
|
138
|
-
print("[RectangleDetector] setupSmoother: creating smoother")
|
|
139
137
|
smoother = RectangleSmoother()
|
|
140
138
|
smoother?.setup(in: parentLayer)
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
private func processDetection(_ request: VNRequest, error: Error?) {
|
|
144
|
-
print("[RectangleDetector] processDetection called")
|
|
145
|
-
|
|
146
142
|
if let error = error {
|
|
147
|
-
print("[RectangleDetector] Detection error: \(error)")
|
|
148
143
|
// Don't reset stability here - let checkTimeout() handle the grace period
|
|
149
144
|
return
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
guard let observations = request.results as? [VNRectangleObservation] else {
|
|
153
|
-
print("[RectangleDetector] No observations in results")
|
|
154
148
|
// Don't reset stability here - let checkTimeout() handle the grace period
|
|
155
149
|
return
|
|
156
150
|
}
|
|
157
151
|
|
|
158
152
|
// Find the rectangle with highest confidence
|
|
159
153
|
guard let bestCandidate = observations.max(by: { $0.confidence < $1.confidence }) else {
|
|
160
|
-
print("[RectangleDetector] No rectangles found (empty observations)")
|
|
161
154
|
// Don't reset stability here - let checkTimeout() handle the grace period
|
|
162
155
|
return
|
|
163
156
|
}
|
|
164
157
|
|
|
165
|
-
print("[RectangleDetector] Found rectangle with confidence: \(bestCandidate.confidence)")
|
|
166
|
-
|
|
167
158
|
// Check if all corners are inside the screen with buffer
|
|
168
159
|
guard isInsideScreen(bestCandidate) else {
|
|
169
|
-
print("[RectangleDetector] Rectangle outside screen bounds")
|
|
170
160
|
// Don't reset stability here - let checkTimeout() handle the grace period
|
|
171
161
|
return
|
|
172
162
|
}
|
|
@@ -204,11 +194,8 @@ class RectangleDetector {
|
|
|
204
194
|
// MARK: - Stability Tracking
|
|
205
195
|
|
|
206
196
|
private func updateStability(with corners: RectangleCorners) {
|
|
207
|
-
print("[RectangleDetector] updateStability - hasPrevious: \(previousCorners != nil), timerActive: \(stabilityStartTime != nil)")
|
|
208
|
-
|
|
209
197
|
// First detection - just store corners, don't start timer yet
|
|
210
198
|
guard let previous = previousCorners else {
|
|
211
|
-
print("[RectangleDetector] First detection - storing corners, no timer yet")
|
|
212
199
|
previousCorners = corners
|
|
213
200
|
stabilityStartTime = nil
|
|
214
201
|
return
|
|
@@ -219,11 +206,9 @@ class RectangleDetector {
|
|
|
219
206
|
|
|
220
207
|
// Check how much the rectangle moved
|
|
221
208
|
let movement = maxCornerMovement(from: previous, to: corners)
|
|
222
|
-
print("[RectangleDetector] Movement: \(movement), threshold: \(stabilityThreshold)")
|
|
223
209
|
|
|
224
210
|
// Too much movement - reset timer completely
|
|
225
211
|
if movement >= stabilityThreshold {
|
|
226
|
-
print("[RectangleDetector] Too much movement (\(movement) >= \(stabilityThreshold)) - resetting timer")
|
|
227
212
|
stabilityStartTime = nil
|
|
228
213
|
return
|
|
229
214
|
}
|
|
@@ -234,13 +219,11 @@ class RectangleDetector {
|
|
|
234
219
|
if stabilityStartTime == nil {
|
|
235
220
|
// Start the stability timer - but DON'T check duration yet
|
|
236
221
|
stabilityStartTime = now
|
|
237
|
-
print("[RectangleDetector] Stability timer STARTED")
|
|
238
222
|
return
|
|
239
223
|
}
|
|
240
224
|
|
|
241
225
|
// Timer was already running - check elapsed time
|
|
242
226
|
let elapsed = now.timeIntervalSince(stabilityStartTime!)
|
|
243
|
-
print("[RectangleDetector] Stability elapsed: \(String(format: "%.2f", elapsed))s / \(stabilityDuration)s required")
|
|
244
227
|
|
|
245
228
|
// Need to be stable for the required duration before any emission
|
|
246
229
|
guard elapsed >= stabilityDuration else { return }
|
|
@@ -250,13 +233,11 @@ class RectangleDetector {
|
|
|
250
233
|
if let lastEmit = lastEmitTime {
|
|
251
234
|
let timeSinceLastEmit = now.timeIntervalSince(lastEmit)
|
|
252
235
|
canEmit = timeSinceLastEmit >= emitIntervalSeconds
|
|
253
|
-
print("[RectangleDetector] Time since last emit: \(String(format: "%.2f", timeSinceLastEmit))s, interval: \(emitIntervalSeconds)s")
|
|
254
236
|
} else {
|
|
255
237
|
canEmit = true
|
|
256
238
|
}
|
|
257
239
|
|
|
258
240
|
if canEmit {
|
|
259
|
-
print("[RectangleDetector] STABLE for \(elapsed)s - emitting event")
|
|
260
241
|
lastEmitTime = now
|
|
261
242
|
delegate?.rectangleDetector(self, didDetect: corners)
|
|
262
243
|
}
|
|
@@ -264,9 +245,6 @@ class RectangleDetector {
|
|
|
264
245
|
|
|
265
246
|
/// Called when no valid rectangle is detected - resets all tracking state
|
|
266
247
|
private func handleNoDetection() {
|
|
267
|
-
if stabilityStartTime != nil {
|
|
268
|
-
print("[RectangleDetector] No detection - resetting stability timer")
|
|
269
|
-
}
|
|
270
248
|
stabilityStartTime = nil
|
|
271
249
|
previousCorners = nil
|
|
272
250
|
}
|
|
@@ -311,9 +289,8 @@ class RectangleDetector {
|
|
|
311
289
|
}
|
|
312
290
|
|
|
313
291
|
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
|
|
314
|
-
let context = CIContext(options: nil)
|
|
315
292
|
|
|
316
|
-
guard let cgImage =
|
|
293
|
+
guard let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) else {
|
|
317
294
|
lastDetectedRectangle = DetectedRectangle(observation: observation, snapshot: nil)
|
|
318
295
|
return
|
|
319
296
|
}
|