@capgo/camera-preview 8.3.7 → 8.3.8-beta.pr356.31.1

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.
@@ -22,9 +22,11 @@ import android.hardware.camera2.CameraCharacteristics;
22
22
  import android.hardware.camera2.CameraManager;
23
23
  import android.hardware.camera2.CaptureRequest;
24
24
  import android.location.Location;
25
+ import android.media.Image;
25
26
  import android.media.MediaScannerConnection;
26
27
  import android.os.Build;
27
28
  import android.os.Environment;
29
+ import android.os.SystemClock;
28
30
  import android.util.Base64;
29
31
  import android.util.DisplayMetrics;
30
32
  import android.util.Log;
@@ -47,9 +49,11 @@ import androidx.camera.core.AspectRatio;
47
49
  import androidx.camera.core.Camera;
48
50
  import androidx.camera.core.CameraInfo;
49
51
  import androidx.camera.core.CameraSelector;
52
+ import androidx.camera.core.ExperimentalGetImage;
50
53
  import androidx.camera.core.ExposureState;
51
54
  import androidx.camera.core.FocusMeteringAction;
52
55
  import androidx.camera.core.FocusMeteringResult;
56
+ import androidx.camera.core.ImageAnalysis;
53
57
  import androidx.camera.core.ImageCapture;
54
58
  import androidx.camera.core.ImageCaptureException;
55
59
  import androidx.camera.core.ImageProxy;
@@ -83,6 +87,11 @@ import app.capgo.capacitor.camera.preview.model.CameraSessionConfiguration;
83
87
  import app.capgo.capacitor.camera.preview.model.LensInfo;
84
88
  import app.capgo.capacitor.camera.preview.model.ZoomFactors;
85
89
  import com.google.common.util.concurrent.ListenableFuture;
90
+ import com.google.mlkit.vision.barcode.BarcodeScanner;
91
+ import com.google.mlkit.vision.barcode.BarcodeScannerOptions;
92
+ import com.google.mlkit.vision.barcode.BarcodeScanning;
93
+ import com.google.mlkit.vision.barcode.common.Barcode;
94
+ import com.google.mlkit.vision.common.InputImage;
86
95
  import java.io.ByteArrayInputStream;
87
96
  import java.io.ByteArrayOutputStream;
88
97
  import java.io.File;
@@ -104,6 +113,7 @@ import java.util.concurrent.Executor;
104
113
  import java.util.concurrent.ExecutorService;
105
114
  import java.util.concurrent.Executors;
106
115
  import java.util.concurrent.RejectedExecutionException;
116
+ import org.json.JSONArray;
107
117
  import org.json.JSONObject;
108
118
 
109
119
  public class CameraXView implements LifecycleOwner, LifecycleObserver {
@@ -116,11 +126,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
116
126
  void onPictureTakenError(String message);
117
127
  void onSampleTaken(String result);
118
128
  void onSampleTakenError(String message);
129
+ void onBarcodesScanned(JSONArray barcodes);
130
+ void onBarcodeScanError(String message);
119
131
  void onCameraStarted(int width, int height, int x, int y);
120
132
  void onCameraStartError(String message);
121
133
  void onCameraStopped(CameraXView source);
122
134
  }
123
135
 
136
+ public interface BarcodeScannerStartCallback {
137
+ void onStarted();
138
+ void onError(String message);
139
+ }
140
+
124
141
  public interface VideoRecordingCallback {
125
142
  void onSuccess(String filePath);
126
143
  void onError(String message);
@@ -130,6 +147,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
130
147
  private Camera camera;
131
148
  private ImageCapture imageCapture;
132
149
  private ImageCapture sampleImageCapture;
150
+ private ImageAnalysis barcodeAnalysis;
151
+ private BarcodeScanner barcodeScanner;
133
152
  private VideoCapture<Recorder> videoCapture;
134
153
  private Recording currentRecording;
135
154
  private File currentVideoFile;
@@ -168,6 +187,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
168
187
  private volatile boolean isCapturingPhoto = false;
169
188
  private volatile boolean stopRequested = false;
170
189
  private volatile boolean previewDetachedOnDeferredStop = false;
190
+ private volatile boolean isBarcodeScannerActive = false;
191
+ private volatile boolean isBarcodeFrameProcessing = false;
192
+ private volatile long lastBarcodeFrameAtMs = 0L;
193
+ private volatile long barcodeDetectionIntervalMs = 500L;
171
194
 
172
195
  // Operation coordination (acts like a semaphore to prevent stop during active ops)
173
196
  private final Object operationLock = new Object();
@@ -601,6 +624,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
601
624
 
602
625
  mainExecutor.execute(() -> {
603
626
  try {
627
+ stopBarcodeScannerInternal(false);
604
628
  lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
605
629
  if (cameraProvider != null) {
606
630
  cameraProvider.unbindAll();
@@ -1519,6 +1543,262 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1519
1543
  );
1520
1544
  }
1521
1545
 
1546
+ public void startBarcodeScanner(List<String> formats, int detectionIntervalMs, BarcodeScannerStartCallback callback) {
1547
+ if (!isRunning || cameraProvider == null || currentCameraSelector == null || cameraExecutor == null) {
1548
+ callback.onError("Camera is not running");
1549
+ return;
1550
+ }
1551
+
1552
+ mainExecutor.execute(() -> {
1553
+ try {
1554
+ stopBarcodeScannerInternal(true);
1555
+ barcodeScanner = createBarcodeScanner(formats);
1556
+ barcodeDetectionIntervalMs = Math.max(100L, detectionIntervalMs);
1557
+ lastBarcodeFrameAtMs = 0L;
1558
+ isBarcodeFrameProcessing = false;
1559
+ isBarcodeScannerActive = true;
1560
+
1561
+ ResolutionSelector barcodeResolutionSelector = new ResolutionSelector.Builder()
1562
+ .setResolutionStrategy(
1563
+ new ResolutionStrategy(new Size(1280, 720), ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER)
1564
+ )
1565
+ .build();
1566
+
1567
+ barcodeAnalysis = new ImageAnalysis.Builder()
1568
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
1569
+ .setResolutionSelector(barcodeResolutionSelector)
1570
+ .build();
1571
+ barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
1572
+
1573
+ cameraProvider.bindToLifecycle(this, currentCameraSelector, barcodeAnalysis);
1574
+ callback.onStarted();
1575
+ } catch (Exception e) {
1576
+ stopBarcodeScannerInternal(true);
1577
+ callback.onError("Failed to start barcode scanner: " + e.getMessage());
1578
+ }
1579
+ });
1580
+ }
1581
+
1582
+ public void stopBarcodeScanner() {
1583
+ mainExecutor.execute(() -> stopBarcodeScannerInternal(true));
1584
+ }
1585
+
1586
+ private void stopBarcodeScannerInternal(boolean unbindAnalysis) {
1587
+ isBarcodeScannerActive = false;
1588
+ isBarcodeFrameProcessing = false;
1589
+ lastBarcodeFrameAtMs = 0L;
1590
+
1591
+ if (barcodeAnalysis != null) {
1592
+ barcodeAnalysis.clearAnalyzer();
1593
+ if (unbindAnalysis && cameraProvider != null) {
1594
+ try {
1595
+ cameraProvider.unbind(barcodeAnalysis);
1596
+ } catch (Exception e) {
1597
+ Log.w(TAG, "stopBarcodeScannerInternal: failed to unbind barcode analysis", e);
1598
+ }
1599
+ }
1600
+ barcodeAnalysis = null;
1601
+ }
1602
+
1603
+ if (barcodeScanner != null) {
1604
+ try {
1605
+ barcodeScanner.close();
1606
+ } catch (Exception e) {
1607
+ Log.w(TAG, "stopBarcodeScannerInternal: failed to close scanner", e);
1608
+ }
1609
+ barcodeScanner = null;
1610
+ }
1611
+ }
1612
+
1613
+ private BarcodeScanner createBarcodeScanner(List<String> formats) {
1614
+ if (formats == null || formats.isEmpty()) {
1615
+ return BarcodeScanning.getClient();
1616
+ }
1617
+
1618
+ int[] mlKitFormats = toMlKitBarcodeFormats(formats);
1619
+ if (mlKitFormats.length == 0) {
1620
+ throw new IllegalArgumentException("No supported barcode formats requested");
1621
+ }
1622
+
1623
+ BarcodeScannerOptions.Builder builder = new BarcodeScannerOptions.Builder();
1624
+ int[] extraFormats = Arrays.copyOfRange(mlKitFormats, 1, mlKitFormats.length);
1625
+ builder.setBarcodeFormats(mlKitFormats[0], extraFormats);
1626
+ return BarcodeScanning.getClient(builder.build());
1627
+ }
1628
+
1629
+ private int[] toMlKitBarcodeFormats(List<String> formats) {
1630
+ if (formats == null || formats.isEmpty()) {
1631
+ return new int[0];
1632
+ }
1633
+
1634
+ List<Integer> mappedFormats = new ArrayList<>();
1635
+ for (String format : formats) {
1636
+ int mappedFormat = toMlKitBarcodeFormat(format);
1637
+ if (mappedFormat != -1 && !mappedFormats.contains(mappedFormat)) {
1638
+ mappedFormats.add(mappedFormat);
1639
+ }
1640
+ }
1641
+
1642
+ int[] result = new int[mappedFormats.size()];
1643
+ for (int i = 0; i < mappedFormats.size(); i++) {
1644
+ result[i] = mappedFormats.get(i);
1645
+ }
1646
+ return result;
1647
+ }
1648
+
1649
+ private int toMlKitBarcodeFormat(String format) {
1650
+ if (format == null) {
1651
+ return -1;
1652
+ }
1653
+
1654
+ switch (format) {
1655
+ case "aztec":
1656
+ return Barcode.FORMAT_AZTEC;
1657
+ case "codabar":
1658
+ return Barcode.FORMAT_CODABAR;
1659
+ case "code_39":
1660
+ return Barcode.FORMAT_CODE_39;
1661
+ case "code_93":
1662
+ return Barcode.FORMAT_CODE_93;
1663
+ case "code_128":
1664
+ return Barcode.FORMAT_CODE_128;
1665
+ case "data_matrix":
1666
+ return Barcode.FORMAT_DATA_MATRIX;
1667
+ case "ean_8":
1668
+ return Barcode.FORMAT_EAN_8;
1669
+ case "ean_13":
1670
+ return Barcode.FORMAT_EAN_13;
1671
+ case "itf":
1672
+ return Barcode.FORMAT_ITF;
1673
+ case "pdf417":
1674
+ return Barcode.FORMAT_PDF417;
1675
+ case "qr_code":
1676
+ return Barcode.FORMAT_QR_CODE;
1677
+ case "upc_a":
1678
+ return Barcode.FORMAT_UPC_A;
1679
+ case "upc_e":
1680
+ return Barcode.FORMAT_UPC_E;
1681
+ default:
1682
+ return -1;
1683
+ }
1684
+ }
1685
+
1686
+ @OptIn(markerClass = ExperimentalGetImage.class)
1687
+ private void analyzeBarcodeImage(@NonNull ImageProxy imageProxy) {
1688
+ BarcodeScanner scanner = barcodeScanner;
1689
+ long now = SystemClock.elapsedRealtime();
1690
+
1691
+ if (
1692
+ !isBarcodeScannerActive ||
1693
+ scanner == null ||
1694
+ isBarcodeFrameProcessing ||
1695
+ now - lastBarcodeFrameAtMs < barcodeDetectionIntervalMs
1696
+ ) {
1697
+ imageProxy.close();
1698
+ return;
1699
+ }
1700
+
1701
+ Image mediaImage = imageProxy.getImage();
1702
+ if (mediaImage == null) {
1703
+ imageProxy.close();
1704
+ return;
1705
+ }
1706
+
1707
+ isBarcodeFrameProcessing = true;
1708
+ lastBarcodeFrameAtMs = now;
1709
+
1710
+ InputImage image = InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());
1711
+ scanner
1712
+ .process(image)
1713
+ .addOnSuccessListener((barcodes) -> {
1714
+ if (!isBarcodeScannerActive || barcodes.isEmpty() || listener == null) {
1715
+ return;
1716
+ }
1717
+
1718
+ JSONArray result = new JSONArray();
1719
+ for (Barcode barcode : barcodes) {
1720
+ JSONObject barcodeJson = barcodeToJson(barcode);
1721
+ if (barcodeJson != null) {
1722
+ result.put(barcodeJson);
1723
+ }
1724
+ }
1725
+
1726
+ if (result.length() > 0) {
1727
+ listener.onBarcodesScanned(result);
1728
+ }
1729
+ })
1730
+ .addOnFailureListener((e) -> {
1731
+ if (isBarcodeScannerActive && listener != null) {
1732
+ listener.onBarcodeScanError("Barcode scan failed: " + e.getMessage());
1733
+ }
1734
+ })
1735
+ .addOnCompleteListener((task) -> {
1736
+ isBarcodeFrameProcessing = false;
1737
+ imageProxy.close();
1738
+ });
1739
+ }
1740
+
1741
+ private JSONObject barcodeToJson(Barcode barcode) {
1742
+ String value = barcode.getRawValue();
1743
+ if (value == null || value.isEmpty()) {
1744
+ return null;
1745
+ }
1746
+
1747
+ JSONObject barcodeJson = new JSONObject();
1748
+ try {
1749
+ barcodeJson.put("value", value);
1750
+ barcodeJson.put("format", fromMlKitBarcodeFormat(barcode.getFormat()));
1751
+
1752
+ String displayValue = barcode.getDisplayValue();
1753
+ if (displayValue != null) {
1754
+ barcodeJson.put("displayValue", displayValue);
1755
+ }
1756
+
1757
+ byte[] rawBytes = barcode.getRawBytes();
1758
+ if (rawBytes != null && rawBytes.length > 0) {
1759
+ barcodeJson.put("rawBytes", Base64.encodeToString(rawBytes, Base64.NO_WRAP));
1760
+ }
1761
+ } catch (Exception e) {
1762
+ Log.w(TAG, "barcodeToJson: failed to serialize barcode", e);
1763
+ return null;
1764
+ }
1765
+
1766
+ return barcodeJson;
1767
+ }
1768
+
1769
+ private String fromMlKitBarcodeFormat(int format) {
1770
+ switch (format) {
1771
+ case Barcode.FORMAT_AZTEC:
1772
+ return "aztec";
1773
+ case Barcode.FORMAT_CODABAR:
1774
+ return "codabar";
1775
+ case Barcode.FORMAT_CODE_39:
1776
+ return "code_39";
1777
+ case Barcode.FORMAT_CODE_93:
1778
+ return "code_93";
1779
+ case Barcode.FORMAT_CODE_128:
1780
+ return "code_128";
1781
+ case Barcode.FORMAT_DATA_MATRIX:
1782
+ return "data_matrix";
1783
+ case Barcode.FORMAT_EAN_8:
1784
+ return "ean_8";
1785
+ case Barcode.FORMAT_EAN_13:
1786
+ return "ean_13";
1787
+ case Barcode.FORMAT_ITF:
1788
+ return "itf";
1789
+ case Barcode.FORMAT_PDF417:
1790
+ return "pdf417";
1791
+ case Barcode.FORMAT_QR_CODE:
1792
+ return "qr_code";
1793
+ case Barcode.FORMAT_UPC_A:
1794
+ return "upc_a";
1795
+ case Barcode.FORMAT_UPC_E:
1796
+ return "upc_e";
1797
+ default:
1798
+ return "unknown";
1799
+ }
1800
+ }
1801
+
1522
1802
  private void copyMutableSessionConfigState(CameraSessionConfiguration source, CameraSessionConfiguration target) {
1523
1803
  target.setCentered(source.isCentered());
1524
1804
  target.setTargetZoom(source.getTargetZoom());