@capgo/camera-preview 8.4.2 → 8.4.3

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.
@@ -83,21 +83,20 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
83
83
  protected void handleOnResume() {
84
84
  super.handleOnResume();
85
85
  if (lastSessionConfig != null) {
86
- // Set to black to avoid flicker, transparent set later
87
- if (lastSessionConfig.isToBack()) {
88
- try {
89
- getBridge()
90
- .getActivity()
91
- .getWindow()
92
- .setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK));
93
- getBridge().getWebView().setBackgroundColor(android.graphics.Color.BLACK);
94
- } catch (Exception ignored) {}
95
- }
96
86
  // Recreate camera with last known configuration
97
- if (cameraXView == null) {
87
+ if (cameraXView == null || !cameraXView.isRunning() || cameraXView.isStopping()) {
98
88
  cameraXView = new CameraXView(getContext(), getBridge().getWebView());
99
89
  cameraXView.setListener(this);
100
90
  }
91
+ if (lastSessionConfig.isToBack()) {
92
+ if (usesFullStackTransparentBackgroundWorkaround()) {
93
+ activateTransparentBackgroundsForToBack(cameraXView);
94
+ } else {
95
+ prepareTransparentBackgroundsForToBack(cameraXView);
96
+ }
97
+ } else {
98
+ toBackVisualStateActive = false;
99
+ }
101
100
  cameraXView.startSession(lastSessionConfig);
102
101
  }
103
102
  }
@@ -110,12 +109,16 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
110
109
  cameraXView = null;
111
110
  }
112
111
  lastSessionConfig = null;
112
+ toBackVisualStateActive = false;
113
+ restoreOriginalWindowBackground(getBridge().getActivity());
114
+ restoreWebViewVisualState();
113
115
  restoreSystemUiForToBackMode(getBridge().getActivity());
114
116
  }
115
117
 
116
118
  private CameraSessionConfiguration lastSessionConfig;
117
119
 
118
120
  private static final String TAG = "CameraPreview CameraXView";
121
+ private static final int DEFAULT_WEB_VIEW_BACKGROUND = Color.WHITE;
119
122
 
120
123
  static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
121
124
  static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
@@ -138,11 +141,16 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
138
141
  private boolean lastDisableAudio = true;
139
142
  private boolean lastIncludeSafeAreaInsets = false;
140
143
  private Drawable originalWindowBackground;
144
+ private boolean originalWindowBackgroundCaptured = false;
145
+ private Drawable originalWebViewBackground;
146
+ private boolean originalWebViewBackgroundCaptured = false;
141
147
  private Float originalWebViewAlpha;
142
148
  private Drawable originalWebViewParentBackground;
149
+ private boolean originalWebViewParentBackgroundCaptured = false;
143
150
  private Integer originalStatusBarColor;
144
151
  private Integer originalNavigationBarColor;
145
152
  private Boolean originalNavigationBarContrastEnforced;
153
+ private volatile boolean toBackVisualStateActive = false;
146
154
  private boolean isCameraPermissionDialogShowing = false;
147
155
  private boolean pendingStartBarcodeScanner = false;
148
156
  private List<String> pendingStartBarcodeFormats = new ArrayList<>();
@@ -524,13 +532,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
524
532
  }
525
533
  // Manual stops should not trigger automatic resume with stale config
526
534
  lastSessionConfig = null;
527
- // Restore original window background if modified earlier
528
- if (originalWindowBackground != null) {
529
- try {
530
- getBridge().getActivity().getWindow().setBackgroundDrawable(originalWindowBackground);
531
- } catch (Exception ignored) {}
532
- originalWindowBackground = null;
533
- }
535
+ toBackVisualStateActive = false;
536
+ restoreOriginalWindowBackground(getBridge().getActivity());
534
537
  restoreWebViewVisualState();
535
538
  restoreSystemUiForToBackMode(getBridge().getActivity());
536
539
  call.resolve();
@@ -1051,16 +1054,14 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1051
1054
  .getActivity()
1052
1055
  .runOnUiThread(() -> {
1053
1056
  lockSystemUiForToBackMode(getBridge().getActivity(), toBack);
1054
- // Ensure transparent background when preview is behind the WebView (Android 10 fix)
1055
1057
  if (toBack) {
1056
- try {
1057
- if (originalWindowBackground == null) {
1058
- originalWindowBackground = getBridge().getActivity().getWindow().getDecorView().getBackground();
1059
- }
1060
- // Set to solid black first to prevent flickering during transition
1061
- // This provides a stable base before camera preview is ready
1062
- getBridge().getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
1063
- } catch (Exception ignored) {}
1058
+ if (usesFullStackTransparentBackgroundWorkaround()) {
1059
+ activateTransparentBackgroundsForToBack(cameraXView);
1060
+ } else {
1061
+ prepareTransparentBackgroundsForToBack(cameraXView);
1062
+ }
1063
+ } else {
1064
+ toBackVisualStateActive = false;
1064
1065
  }
1065
1066
  DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
1066
1067
  if (lockOrientation) {
@@ -1337,6 +1338,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1337
1338
  config.setTargetZoom(finalTargetZoom);
1338
1339
  config.setCentered(isCentered);
1339
1340
  config.setEnablePhysicalDeviceSelection(enablePhysicalDeviceSelection);
1341
+ config.setBarcodeScannerEnabled(barcodeScannerOptions != null);
1340
1342
  setPendingStartBarcodeScanner(barcodeScannerOptions);
1341
1343
 
1342
1344
  bridge.saveCall(call);
@@ -1626,10 +1628,12 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1626
1628
  return;
1627
1629
  }
1628
1630
  // Ensure reference is cleared once the originating CameraXView has fully stopped
1629
- if (cameraXView == source) {
1631
+ if (source != null && cameraXView == source) {
1630
1632
  cameraXView = null;
1631
1633
  }
1632
- restoreWebViewVisualState();
1634
+ if (!toBackVisualStateActive) {
1635
+ restoreWebViewVisualState();
1636
+ }
1633
1637
 
1634
1638
  PluginCall queuedCall = null;
1635
1639
  synchronized (pendingStartLock) {
@@ -1676,6 +1680,72 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1676
1680
  return manufacturer.contains("xiaomi") || brand.contains("xiaomi") || brand.contains("redmi") || brand.contains("poco");
1677
1681
  }
1678
1682
 
1683
+ private boolean usesFullStackTransparentBackgroundWorkaround() {
1684
+ String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase(Locale.US) : "";
1685
+ String brand = Build.BRAND != null ? Build.BRAND.toLowerCase(Locale.US) : "";
1686
+ return (
1687
+ isMiuiDevice() ||
1688
+ manufacturer.contains("huawei") ||
1689
+ manufacturer.contains("honor") ||
1690
+ brand.contains("huawei") ||
1691
+ brand.contains("honor")
1692
+ );
1693
+ }
1694
+
1695
+ private void captureOriginalWindowBackground(Activity activity) {
1696
+ if (activity == null) {
1697
+ return;
1698
+ }
1699
+ synchronized (this) {
1700
+ if (!originalWindowBackgroundCaptured) {
1701
+ originalWindowBackground = activity.getWindow().getDecorView().getBackground();
1702
+ originalWindowBackgroundCaptured = true;
1703
+ }
1704
+ }
1705
+ }
1706
+
1707
+ private void captureOriginalWebViewVisualState(WebView webView, ViewGroup webViewParent) {
1708
+ if (webView == null) {
1709
+ return;
1710
+ }
1711
+ synchronized (this) {
1712
+ if (!originalWebViewBackgroundCaptured) {
1713
+ originalWebViewBackground = webView.getBackground();
1714
+ originalWebViewBackgroundCaptured = true;
1715
+ }
1716
+ if (originalWebViewAlpha == null) {
1717
+ originalWebViewAlpha = webView.getAlpha();
1718
+ }
1719
+ if (webViewParent != null && !originalWebViewParentBackgroundCaptured) {
1720
+ originalWebViewParentBackground = webViewParent.getBackground();
1721
+ originalWebViewParentBackgroundCaptured = true;
1722
+ }
1723
+ }
1724
+ }
1725
+
1726
+ private void restoreOriginalWindowBackground(Activity activity) {
1727
+ final Drawable backgroundToRestore;
1728
+ final boolean captured;
1729
+ synchronized (this) {
1730
+ backgroundToRestore = originalWindowBackground;
1731
+ captured = originalWindowBackgroundCaptured;
1732
+ originalWindowBackground = null;
1733
+ originalWindowBackgroundCaptured = false;
1734
+ }
1735
+
1736
+ if (!captured || activity == null) {
1737
+ return;
1738
+ }
1739
+
1740
+ activity.runOnUiThread(() -> {
1741
+ try {
1742
+ activity.getWindow().setBackgroundDrawable(backgroundToRestore);
1743
+ } catch (Exception e) {
1744
+ Log.w(TAG, "Failed to restore window background", e);
1745
+ }
1746
+ });
1747
+ }
1748
+
1679
1749
  private int toOpaqueColor(int color) {
1680
1750
  return Color.argb(255, Color.red(color), Color.green(color), Color.blue(color));
1681
1751
  }
@@ -1741,35 +1811,46 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1741
1811
  });
1742
1812
  }
1743
1813
 
1744
- private void applyTransparentBackgroundsForToBack() {
1745
- if (!isToBackMode()) {
1746
- return;
1747
- }
1814
+ private void prepareTransparentBackgroundsForToBack(CameraXView visualStateOwner) {
1748
1815
  Activity activity = getActivity();
1749
1816
  WebView webView = getBridge().getWebView();
1750
1817
  if (activity == null || webView == null) {
1751
1818
  return;
1752
1819
  }
1753
1820
 
1754
- if (originalWebViewAlpha == null) {
1755
- originalWebViewAlpha = webView.getAlpha();
1821
+ toBackVisualStateActive = true;
1822
+ final ViewGroup webViewParent = (ViewGroup) webView.getParent();
1823
+ captureOriginalWindowBackground(activity);
1824
+ captureOriginalWebViewVisualState(webView, webViewParent);
1825
+ }
1826
+
1827
+ private void activateTransparentBackgroundsForToBack(CameraXView visualStateOwner) {
1828
+ prepareTransparentBackgroundsForToBack(visualStateOwner);
1829
+ Activity activity = getActivity();
1830
+ WebView webView = getBridge().getWebView();
1831
+ if (activity == null || webView == null) {
1832
+ return;
1756
1833
  }
1757
1834
 
1758
1835
  final ViewGroup webViewParent = (ViewGroup) webView.getParent();
1759
- if (webViewParent != null && originalWebViewParentBackground == null) {
1760
- originalWebViewParentBackground = webViewParent.getBackground();
1761
- }
1762
1836
 
1763
1837
  Runnable apply = () -> {
1764
1838
  try {
1765
- activity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1766
- if (webViewParent != null) {
1839
+ if (!toBackVisualStateActive || visualStateOwner == null || cameraXView != visualStateOwner) {
1840
+ return;
1841
+ }
1842
+ boolean fullStackWorkaround = usesFullStackTransparentBackgroundWorkaround();
1843
+ if (fullStackWorkaround) {
1844
+ activity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1845
+ }
1846
+ if (webViewParent != null && fullStackWorkaround) {
1767
1847
  webViewParent.setBackgroundColor(Color.TRANSPARENT);
1768
1848
  }
1769
- // Keep a tiny alpha on MIUI/Xiaomi devices to avoid compositor bugs that treat
1770
- // fully transparent layers as black.
1771
- webView.setBackgroundColor(Color.argb(1, 255, 255, 255));
1849
+ webView.setBackgroundColor(isMiuiDevice() ? Color.argb(1, 255, 255, 255) : Color.TRANSPARENT);
1772
1850
  webView.setAlpha(isMiuiDevice() ? 0.99f : (originalWebViewAlpha != null ? originalWebViewAlpha : 1f));
1851
+ if (webViewParent != null) {
1852
+ webViewParent.requestTransparentRegion(webView);
1853
+ }
1773
1854
  } catch (Exception e) {
1774
1855
  Log.w(TAG, "Failed to set backgrounds to transparent", e);
1775
1856
  }
@@ -1784,13 +1865,41 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1784
1865
  });
1785
1866
  }
1786
1867
 
1868
+ private void applyTransparentBackgroundsForToBack() {
1869
+ if (!isToBackMode()) {
1870
+ return;
1871
+ }
1872
+ activateTransparentBackgroundsForToBack(cameraXView);
1873
+ }
1874
+
1787
1875
  private void restoreWebViewVisualState() {
1876
+ if (toBackVisualStateActive) {
1877
+ return;
1878
+ }
1879
+
1788
1880
  Activity activity = getActivity();
1789
1881
  WebView webView = getBridge().getWebView();
1790
- final Float alphaToRestore = originalWebViewAlpha;
1791
- final Drawable parentBackground = originalWebViewParentBackground;
1792
- originalWebViewAlpha = null;
1793
- originalWebViewParentBackground = null;
1882
+ final Float alphaToRestore;
1883
+ final Drawable webViewBackground;
1884
+ final boolean webViewBackgroundCaptured;
1885
+ final Drawable parentBackground;
1886
+ final boolean parentBackgroundCaptured;
1887
+ synchronized (this) {
1888
+ alphaToRestore = originalWebViewAlpha;
1889
+ webViewBackground = originalWebViewBackground;
1890
+ webViewBackgroundCaptured = originalWebViewBackgroundCaptured;
1891
+ parentBackground = originalWebViewParentBackground;
1892
+ parentBackgroundCaptured = originalWebViewParentBackgroundCaptured;
1893
+ originalWebViewAlpha = null;
1894
+ originalWebViewBackground = null;
1895
+ originalWebViewBackgroundCaptured = false;
1896
+ originalWebViewParentBackground = null;
1897
+ originalWebViewParentBackgroundCaptured = false;
1898
+ }
1899
+
1900
+ if (alphaToRestore == null && !webViewBackgroundCaptured && !parentBackgroundCaptured) {
1901
+ return;
1902
+ }
1794
1903
 
1795
1904
  if (activity == null || webView == null) {
1796
1905
  return;
@@ -1802,7 +1911,12 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1802
1911
  if (alphaToRestore != null) {
1803
1912
  webView.setAlpha(alphaToRestore);
1804
1913
  }
1805
- if (webViewParent != null && parentBackground != null) {
1914
+ if (webViewBackgroundCaptured) {
1915
+ webView.setBackground(webViewBackground);
1916
+ } else {
1917
+ webView.setBackgroundColor(DEFAULT_WEB_VIEW_BACKGROUND);
1918
+ }
1919
+ if (webViewParent != null && parentBackgroundCaptured) {
1806
1920
  webViewParent.setBackground(parentBackground);
1807
1921
  }
1808
1922
  } catch (Exception ignored) {}
@@ -2042,7 +2156,22 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2042
2156
  }
2043
2157
 
2044
2158
  @Override
2045
- public void onCameraStartError(String message) {
2159
+ public void onCameraStartError(CameraXView source, String message) {
2160
+ if (cameraXView != null && cameraXView != source) {
2161
+ Log.d(TAG, "onCameraStartError: ignoring callback from stale instance");
2162
+ return;
2163
+ }
2164
+ toBackVisualStateActive = false;
2165
+ if (cameraXView == source) {
2166
+ try {
2167
+ // Keep the reference until onCameraStopped clears it after native cleanup.
2168
+ source.stopSession();
2169
+ } catch (Exception e) {
2170
+ Log.w(TAG, "onCameraStartError: failed to stop failed camera session", e);
2171
+ cameraXView = null;
2172
+ }
2173
+ }
2174
+
2046
2175
  PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
2047
2176
  if (call != null) {
2048
2177
  call.reject(message);
@@ -2051,24 +2180,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2051
2180
  resetPendingStartBarcodeScanner();
2052
2181
  }
2053
2182
 
2054
- // Restore original window background on error to prevent black screen
2055
- // Use synchronized block to ensure only one thread captures and clears the background.
2056
- // Even if multiple errors occur, only the first will have a non-null background to restore.
2057
- synchronized (this) {
2058
- final Drawable backgroundToRestore = originalWindowBackground;
2059
- if (backgroundToRestore != null) {
2060
- originalWindowBackground = null; // Clear immediately so other threads won't restore
2061
- getBridge()
2062
- .getActivity()
2063
- .runOnUiThread(() -> {
2064
- try {
2065
- getBridge().getActivity().getWindow().setBackgroundDrawable(backgroundToRestore);
2066
- } catch (Exception e) {
2067
- Log.w(TAG, "Failed to restore window background on error", e);
2068
- }
2069
- });
2070
- }
2071
- }
2183
+ restoreOriginalWindowBackground(getBridge().getActivity());
2072
2184
  restoreWebViewVisualState();
2073
2185
  restoreSystemUiForToBackMode(getBridge().getActivity());
2074
2186
  }
@@ -129,7 +129,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
129
129
  void onBarcodesScanned(JSONArray barcodes);
130
130
  void onBarcodeScanError(String message);
131
131
  void onCameraStarted(int width, int height, int x, int y);
132
- void onCameraStartError(String message);
132
+ void onCameraStartError(CameraXView source, String message);
133
133
  void onCameraStopped(CameraXView source);
134
134
  }
135
135
 
@@ -191,6 +191,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
191
191
  private volatile boolean isBarcodeFrameProcessing = false;
192
192
  private volatile long lastBarcodeFrameAtMs = 0L;
193
193
  private volatile long barcodeDetectionIntervalMs = 500L;
194
+ private boolean cameraStartedCallbackSent = false;
194
195
 
195
196
  // Operation coordination (acts like a semaphore to prevent stop during active ops)
196
197
  private final Object operationLock = new Object();
@@ -481,13 +482,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
481
482
  // runnable is still queued — never transition backward from DESTROYED.
482
483
  if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
483
484
  if (listener != null) {
484
- listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
485
+ listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
485
486
  }
486
487
  return;
487
488
  }
488
489
  if (stopRequested) {
489
490
  if (listener != null) {
490
- listener.onCameraStartError("Camera start aborted: stop requested");
491
+ listener.onCameraStartError(this, "Camera start aborted: stop requested");
491
492
  }
492
493
  return;
493
494
  }
@@ -496,44 +497,45 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
496
497
  lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
497
498
  if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
498
499
  if (listener != null) {
499
- listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
500
+ listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
500
501
  }
501
502
  return;
502
503
  }
503
504
  if (stopRequested) {
504
505
  if (listener != null) {
505
- listener.onCameraStartError("Camera start aborted: stop requested");
506
+ listener.onCameraStartError(this, "Camera start aborted: stop requested");
506
507
  }
507
508
  return;
508
509
  }
509
510
  }
510
511
  if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
511
512
  if (listener != null) {
512
- listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
513
+ listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
513
514
  }
514
515
  return;
515
516
  }
516
517
  if (stopRequested) {
517
518
  if (listener != null) {
518
- listener.onCameraStartError("Camera start aborted: stop requested");
519
+ listener.onCameraStartError(this, "Camera start aborted: stop requested");
519
520
  }
520
521
  return;
521
522
  }
522
523
  lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
523
524
  if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
524
525
  if (listener != null) {
525
- listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
526
+ listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
526
527
  }
527
528
  return;
528
529
  }
529
530
  if (stopRequested) {
530
531
  if (listener != null) {
531
- listener.onCameraStartError("Camera start aborted: stop requested");
532
+ listener.onCameraStartError(this, "Camera start aborted: stop requested");
532
533
  }
533
534
  return;
534
535
  }
535
536
 
536
537
  this.sessionConfig = config;
538
+ cameraStartedCallbackSent = false;
537
539
  cameraExecutor = Executors.newSingleThreadExecutor();
538
540
  requestEnumeratedDeviceCacheRefresh();
539
541
 
@@ -629,6 +631,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
629
631
  if (cameraProvider != null) {
630
632
  cameraProvider.unbindAll();
631
633
  }
634
+ barcodeAnalysis = null;
632
635
  if (cameraExecutor != null) {
633
636
  cameraExecutor.shutdown();
634
637
  }
@@ -654,7 +657,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
654
657
  private void restoreWebViewBackground() {
655
658
  // Capture sessionConfig reference once to avoid race conditions
656
659
  CameraSessionConfiguration config = sessionConfig;
657
- boolean shouldRestore = config != null && config.isToBack();
660
+ boolean shouldRestore = config == null || !config.isToBack();
658
661
  if (shouldRestore) {
659
662
  // Capture background color before posting to UI thread
660
663
  final int backgroundColorToRestore = originalWebViewBackground;
@@ -674,26 +677,26 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
674
677
  try {
675
678
  if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
676
679
  if (listener != null) {
677
- listener.onCameraStartError("Camera binding cancelled: lifecycle destroyed (before provider)");
680
+ listener.onCameraStartError(this, "Camera binding cancelled: lifecycle destroyed (before provider)");
678
681
  }
679
682
  return;
680
683
  }
681
684
  if (stopRequested) {
682
685
  if (listener != null) {
683
- listener.onCameraStartError("Camera binding cancelled: stop requested (before provider)");
686
+ listener.onCameraStartError(this, "Camera binding cancelled: stop requested (before provider)");
684
687
  }
685
688
  return;
686
689
  }
687
690
  cameraProvider = cameraProviderFuture.get();
688
691
  if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
689
692
  if (listener != null) {
690
- listener.onCameraStartError("Camera binding cancelled: lifecycle destroyed (after provider)");
693
+ listener.onCameraStartError(this, "Camera binding cancelled: lifecycle destroyed (after provider)");
691
694
  }
692
695
  return;
693
696
  }
694
697
  if (stopRequested) {
695
698
  if (listener != null) {
696
- listener.onCameraStartError("Camera binding cancelled: stop requested (after provider)");
699
+ listener.onCameraStartError(this, "Camera binding cancelled: stop requested (after provider)");
697
700
  }
698
701
  return;
699
702
  }
@@ -703,7 +706,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
703
706
  // Restore webView background on error
704
707
  restoreWebViewBackground();
705
708
  if (listener != null) {
706
- listener.onCameraStartError("Error initializing camera: " + e.getMessage());
709
+ listener.onCameraStartError(this, "Error initializing camera: " + e.getMessage());
707
710
  }
708
711
  }
709
712
  },
@@ -715,13 +718,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
715
718
  if (previewView != null) {
716
719
  removePreviewView();
717
720
  }
718
- if (sessionConfig.isToBack()) {
719
- // Set to black initially to prevent flickering, will be transparent after camera starts
720
- webView.setBackgroundColor(android.graphics.Color.BLACK);
721
- }
722
-
723
721
  // Create a container to hold both the preview and grid overlay
724
722
  previewContainer = new FrameLayout(context);
723
+ if (sessionConfig != null && sessionConfig.isToBack()) {
724
+ previewContainer.setBackgroundColor(Color.TRANSPARENT);
725
+ }
725
726
  // Ensure container can receive touch events
726
727
  previewContainer.setClickable(true);
727
728
  previewContainer.setFocusable(true);
@@ -735,9 +736,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
735
736
 
736
737
  // Create and setup the preview view
737
738
  previewView = new PreviewView(context);
738
- // Use TextureView-backed implementation for broader device compatibility when overlaying with WebView
739
- // This avoids SurfaceView z-order issues seen on some MIUI/EMUI devices.
740
- previewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
739
+ if (sessionConfig != null && sessionConfig.isToBack()) {
740
+ previewView.setBackgroundColor(Color.TRANSPARENT);
741
+ }
742
+ PreviewView.ImplementationMode implementationMode = choosePreviewImplementationMode();
743
+ previewView.setImplementationMode(implementationMode);
741
744
  // Set scale type based on aspectMode: 'contain' uses FIT, 'cover' uses FILL
742
745
  String aspectMode = sessionConfig != null ? sessionConfig.getAspectMode() : "contain";
743
746
  previewView.setScaleType("cover".equals(aspectMode) ? PreviewView.ScaleType.FILL_CENTER : PreviewView.ScaleType.FIT_CENTER);
@@ -753,6 +756,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
753
756
  new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
754
757
  );
755
758
 
759
+ previewView
760
+ .getPreviewStreamState()
761
+ .observe(this, (streamState) -> {
762
+ if (sessionConfig != null && sessionConfig.isToBack() && streamState == PreviewView.StreamState.STREAMING) {
763
+ notifyCameraStartedIfNeeded("streaming");
764
+ }
765
+ });
766
+
756
767
  // Create and setup the grid overlay
757
768
  gridOverlayView = new GridOverlayView(context);
758
769
  // Make grid overlay not intercept touch events
@@ -781,7 +792,17 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
781
792
  if (parent != null) {
782
793
  FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
783
794
  parent.addView(previewContainer, layoutParams);
784
- if (sessionConfig.isToBack()) webView.bringToFront();
795
+ if (sessionConfig.isToBack()) {
796
+ webView.bringToFront();
797
+ parent.requestTransparentRegion(webView);
798
+ webView.post(() -> {
799
+ webView.bringToFront();
800
+ ViewGroup currentParent = (ViewGroup) webView.getParent();
801
+ if (currentParent != null) {
802
+ currentParent.requestTransparentRegion(webView);
803
+ }
804
+ });
805
+ }
785
806
 
786
807
  // Log the actual position after layout
787
808
  previewContainer.post(() -> {
@@ -811,6 +832,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
811
832
  }
812
833
  }
813
834
 
835
+ private PreviewView.ImplementationMode choosePreviewImplementationMode() {
836
+ return PreviewView.ImplementationMode.COMPATIBLE;
837
+ }
838
+
814
839
  /**
815
840
  * Compute layout parameters for the camera preview container based on the current session configuration,
816
841
  * device screen size, WebView/parent geometry, and optional aspect-ratio centering.
@@ -1000,7 +1025,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1000
1025
  if (focusIndicatorView != null) {
1001
1026
  focusIndicatorView = null;
1002
1027
  }
1003
- webView.setBackgroundColor(originalWebViewBackground);
1028
+ if (sessionConfig == null || !sessionConfig.isToBack()) {
1029
+ webView.setBackgroundColor(originalWebViewBackground);
1030
+ }
1004
1031
  }
1005
1032
 
1006
1033
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
@@ -1050,6 +1077,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1050
1077
  .setTargetRotation(rotation)
1051
1078
  .build();
1052
1079
  sampleImageCapture = imageCapture;
1080
+ barcodeAnalysis = null;
1081
+
1082
+ if (!sessionConfig.isVideoModeEnabled() && sessionConfig.isBarcodeScannerEnabled()) {
1083
+ barcodeAnalysis = createBarcodeAnalysisUseCase();
1084
+ if (isBarcodeScannerActive && barcodeScanner != null && cameraExecutor != null) {
1085
+ barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
1086
+ }
1087
+ }
1053
1088
 
1054
1089
  // Only setup VideoCapture if enableVideoMode is true
1055
1090
  if (sessionConfig.isVideoModeEnabled()) {
@@ -1214,6 +1249,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1214
1249
  if (initialZoom < minZoom || initialZoom > maxZoom) {
1215
1250
  if (listener != null) {
1216
1251
  listener.onCameraStartError(
1252
+ this,
1217
1253
  "Initial zoom level " +
1218
1254
  initialZoom +
1219
1255
  " is not available. " +
@@ -1233,43 +1269,78 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1233
1269
  isRunning = true;
1234
1270
  Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
1235
1271
  if (listener != null) {
1236
- // Post the callback to ensure layout is complete
1237
- previewContainer.post(() -> {
1238
- // Return actual preview container dimensions instead of requested dimensions
1239
- // Get the actual camera dimensions and position
1240
- int actualWidth = getPreviewWidth();
1241
- int actualHeight = getPreviewHeight();
1242
- int actualX = getPreviewX();
1243
- int actualY = getPreviewY();
1244
-
1245
- Log.d(
1246
- TAG,
1247
- "onCameraStarted callback - actualX=" +
1248
- actualX +
1249
- ", actualY=" +
1250
- actualY +
1251
- ", actualWidth=" +
1252
- actualWidth +
1253
- ", actualHeight=" +
1254
- actualHeight
1255
- );
1256
-
1257
- // Update grid overlay bounds after camera is started
1258
- updateGridOverlayBounds();
1259
-
1260
- // Notify listener that camera is started
1261
- // The listener (CameraPreview) will handle setting both window and webview to transparent
1262
- listener.onCameraStarted(actualWidth, actualHeight, actualX, actualY);
1263
- });
1272
+ if (sessionConfig != null && sessionConfig.isToBack()) {
1273
+ PreviewView.StreamState streamState = previewView != null ? previewView.getPreviewStreamState().getValue() : null;
1274
+ if (streamState == PreviewView.StreamState.STREAMING) {
1275
+ notifyCameraStartedIfNeeded("already-streaming");
1276
+ } else if (previewContainer != null) {
1277
+ previewContainer.postDelayed(
1278
+ () -> {
1279
+ PreviewView.StreamState latestState = previewView != null
1280
+ ? previewView.getPreviewStreamState().getValue()
1281
+ : null;
1282
+ if (latestState == PreviewView.StreamState.STREAMING) {
1283
+ notifyCameraStartedIfNeeded("watchdog-streaming");
1284
+ }
1285
+ },
1286
+ 300
1287
+ );
1288
+ previewContainer.postDelayed(
1289
+ () -> {
1290
+ PreviewView.StreamState latestState = previewView != null
1291
+ ? previewView.getPreviewStreamState().getValue()
1292
+ : null;
1293
+ if (!cameraStartedCallbackSent && latestState == PreviewView.StreamState.STREAMING) {
1294
+ notifyCameraStartedIfNeeded("fallback-streaming");
1295
+ }
1296
+ },
1297
+ 1500
1298
+ );
1299
+ }
1300
+ } else {
1301
+ notifyCameraStartedIfNeeded("bound");
1302
+ }
1264
1303
  }
1265
1304
  } catch (Exception e) {
1266
1305
  // Restore webView background on error
1267
1306
  restoreWebViewBackground();
1268
- if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
1307
+ if (listener != null) listener.onCameraStartError(this, "Error binding camera: " + e.getMessage());
1269
1308
  }
1270
1309
  });
1271
1310
  }
1272
1311
 
1312
+ private void notifyCameraStartedIfNeeded(String reason) {
1313
+ if (cameraStartedCallbackSent || listener == null || previewContainer == null) {
1314
+ return;
1315
+ }
1316
+ cameraStartedCallbackSent = true;
1317
+ previewContainer.post(() -> {
1318
+ if (listener == null || previewContainer == null) {
1319
+ return;
1320
+ }
1321
+
1322
+ int actualWidth = getPreviewWidth();
1323
+ int actualHeight = getPreviewHeight();
1324
+ int actualX = getPreviewX();
1325
+ int actualY = getPreviewY();
1326
+
1327
+ Log.d(
1328
+ TAG,
1329
+ "onCameraStarted callback - actualX=" +
1330
+ actualX +
1331
+ ", actualY=" +
1332
+ actualY +
1333
+ ", actualWidth=" +
1334
+ actualWidth +
1335
+ ", actualHeight=" +
1336
+ actualHeight
1337
+ );
1338
+
1339
+ updateGridOverlayBounds();
1340
+ listener.onCameraStarted(actualWidth, actualHeight, actualX, actualY);
1341
+ });
1342
+ }
1343
+
1273
1344
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
1274
1345
  private CameraSelector buildCameraSelector() {
1275
1346
  return buildCameraBindingPlan(sessionConfig).selector;
@@ -1523,6 +1594,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1523
1594
  private void bindConfiguredUseCases(CameraBindingPlan bindingPlan, Preview preview) {
1524
1595
  if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
1525
1596
  camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, videoCapture);
1597
+ } else if (barcodeAnalysis != null) {
1598
+ camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, barcodeAnalysis);
1526
1599
  } else {
1527
1600
  camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture);
1528
1601
  }
@@ -1551,26 +1624,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1551
1624
 
1552
1625
  mainExecutor.execute(() -> {
1553
1626
  try {
1554
- stopBarcodeScannerInternal(true);
1627
+ stopBarcodeScannerInternal(false);
1555
1628
  barcodeScanner = createBarcodeScanner(formats);
1556
1629
  barcodeDetectionIntervalMs = Math.max(100L, detectionIntervalMs);
1557
1630
  lastBarcodeFrameAtMs = 0L;
1558
1631
  isBarcodeFrameProcessing = false;
1559
1632
  isBarcodeScannerActive = true;
1560
1633
 
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);
1634
+ if (barcodeAnalysis == null) {
1635
+ barcodeAnalysis = createBarcodeAnalysisUseCase();
1636
+ barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
1637
+ cameraProvider.bindToLifecycle(this, currentCameraSelector, barcodeAnalysis);
1638
+ } else {
1639
+ barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
1640
+ }
1574
1641
  callback.onStarted();
1575
1642
  } catch (Exception e) {
1576
1643
  stopBarcodeScannerInternal(true);
@@ -1583,6 +1650,17 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1583
1650
  mainExecutor.execute(() -> stopBarcodeScannerInternal(true));
1584
1651
  }
1585
1652
 
1653
+ private ImageAnalysis createBarcodeAnalysisUseCase() {
1654
+ ResolutionSelector barcodeResolutionSelector = new ResolutionSelector.Builder()
1655
+ .setResolutionStrategy(new ResolutionStrategy(new Size(1280, 720), ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER))
1656
+ .build();
1657
+
1658
+ return new ImageAnalysis.Builder()
1659
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
1660
+ .setResolutionSelector(barcodeResolutionSelector)
1661
+ .build();
1662
+ }
1663
+
1586
1664
  private void stopBarcodeScannerInternal(boolean unbindAnalysis) {
1587
1665
  isBarcodeScannerActive = false;
1588
1666
  isBarcodeFrameProcessing = false;
@@ -1595,9 +1673,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1595
1673
  cameraProvider.unbind(barcodeAnalysis);
1596
1674
  } catch (Exception e) {
1597
1675
  Log.w(TAG, "stopBarcodeScannerInternal: failed to unbind barcode analysis", e);
1676
+ } finally {
1677
+ barcodeAnalysis = null;
1598
1678
  }
1599
1679
  }
1600
- barcodeAnalysis = null;
1601
1680
  }
1602
1681
 
1603
1682
  if (barcodeScanner != null) {
@@ -1803,6 +1882,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1803
1882
  target.setCentered(source.isCentered());
1804
1883
  target.setTargetZoom(source.getTargetZoom());
1805
1884
  target.setEnablePhysicalDeviceSelection(source.isPhysicalDeviceSelectionEnabled());
1885
+ target.setBarcodeScannerEnabled(source.isBarcodeScannerEnabled());
1806
1886
  }
1807
1887
 
1808
1888
  private void requestEnumeratedDeviceCacheRefresh() {
@@ -27,6 +27,7 @@ public class CameraSessionConfiguration {
27
27
  private boolean isCentered = false;
28
28
  private final String videoQuality;
29
29
  private boolean enablePhysicalDeviceSelection = false;
30
+ private boolean barcodeScannerEnabled = false;
30
31
 
31
32
  public CameraSessionConfiguration(
32
33
  String deviceId,
@@ -158,6 +159,14 @@ public class CameraSessionConfiguration {
158
159
  this.enablePhysicalDeviceSelection = enablePhysicalDeviceSelection;
159
160
  }
160
161
 
162
+ public boolean isBarcodeScannerEnabled() {
163
+ return barcodeScannerEnabled;
164
+ }
165
+
166
+ public void setBarcodeScannerEnabled(boolean barcodeScannerEnabled) {
167
+ this.barcodeScannerEnabled = barcodeScannerEnabled;
168
+ }
169
+
161
170
  // Additional getters with "get" prefix for compatibility
162
171
  public boolean getToBack() {
163
172
  return toBack;
@@ -35,7 +35,7 @@ extension UIWindow {
35
35
  */
36
36
  @objc(CameraPreview)
37
37
  public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
38
- private let pluginVersion: String = "8.4.2"
38
+ private let pluginVersion: String = "8.4.3"
39
39
  public let identifier = "CameraPreviewPlugin"
40
40
  public let jsName = "CameraPreview"
41
41
  public let pluginMethods: [CAPPluginMethod] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.4.2",
3
+ "version": "8.4.3",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {