@capgo/camera-preview 7.6.1 → 7.8.0

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 CHANGED
@@ -95,6 +95,53 @@ await CameraPreview.deleteFile({ path: filePath })
95
95
  ```
96
96
 
97
97
 
98
+ ## Exposure controls (iOS & Android)
99
+
100
+ This plugin exposes camera exposure controls on iOS and Android:
101
+
102
+ - Exposure modes: `"AUTO" | "LOCK" | "CONTINUOUS" | "CUSTOM"`
103
+ - Exposure compensation (EV bias): get range `{ min, max, step }`, read current value, and set new value
104
+
105
+ Platform notes:
106
+
107
+ - iOS: The camera starts in `CONTINUOUS` by default. Switching to `AUTO` or `CONTINUOUS` resets EV to 0. The `step` value is approximated to 0.1 since iOS does not expose the bias step.
108
+ - Android: AE lock/unlock and mode are handled via CameraX + Camera2 interop. The `step` value comes from CameraX `ExposureState` and may vary per device.
109
+
110
+ Example (TypeScript):
111
+
112
+ ```ts
113
+ import { CameraPreview } from '@capgo/camera-preview';
114
+
115
+ // Query supported modes
116
+ const { modes } = await CameraPreview.getExposureModes();
117
+ console.log('Supported exposure modes:', modes);
118
+
119
+ // Get current mode
120
+ const { mode } = await CameraPreview.getExposureMode();
121
+ console.log('Current exposure mode:', mode);
122
+
123
+ // Set mode (AUTO | LOCK | CONTINUOUS | CUSTOM)
124
+ await CameraPreview.setExposureMode({ mode: 'CONTINUOUS' });
125
+
126
+ // Get EV range (with step)
127
+ const { min, max, step } = await CameraPreview.getExposureCompensationRange();
128
+ console.log('EV range:', { min, max, step });
129
+
130
+ // Read current EV
131
+ const { value: currentEV } = await CameraPreview.getExposureCompensation();
132
+ console.log('Current EV:', currentEV);
133
+
134
+ // Increment EV by one step and clamp to range
135
+ const nextEV = Math.max(min, Math.min(max, currentEV + step));
136
+ await CameraPreview.setExposureCompensation({ value: nextEV });
137
+ ```
138
+
139
+ Example app (Ionic):
140
+
141
+ - Exposure mode toggle (sun icon) cycles through modes.
142
+ - EV controls (+/−) are placed in a top‑right floating action bar, outside the preview area.
143
+
144
+
98
145
  # Installation
99
146
 
100
147
  ```
@@ -267,6 +314,12 @@ Documentation for the [uploader](https://github.com/Cap-go/capacitor-uploader)
267
314
  * [`deleteFile(...)`](#deletefile)
268
315
  * [`getSafeAreaInsets()`](#getsafeareainsets)
269
316
  * [`getOrientation()`](#getorientation)
317
+ * [`getExposureModes()`](#getexposuremodes)
318
+ * [`getExposureMode()`](#getexposuremode)
319
+ * [`setExposureMode(...)`](#setexposuremode)
320
+ * [`getExposureCompensationRange()`](#getexposurecompensationrange)
321
+ * [`getExposureCompensation()`](#getexposurecompensation)
322
+ * [`setExposureCompensation(...)`](#setexposurecompensation)
270
323
  * [Interfaces](#interfaces)
271
324
  * [Type Aliases](#type-aliases)
272
325
  * [Enums](#enums)
@@ -828,6 +881,89 @@ Gets the current device orientation in a cross-platform format.
828
881
  --------------------
829
882
 
830
883
 
884
+ ### getExposureModes()
885
+
886
+ ```typescript
887
+ getExposureModes() => Promise<{ modes: ExposureMode[]; }>
888
+ ```
889
+
890
+ Returns the exposure modes supported by the active camera.
891
+ Modes can include: 'locked', 'auto', 'continuous', 'custom'.
892
+
893
+ **Returns:** <code>Promise&lt;{ modes: ExposureMode[]; }&gt;</code>
894
+
895
+ --------------------
896
+
897
+
898
+ ### getExposureMode()
899
+
900
+ ```typescript
901
+ getExposureMode() => Promise<{ mode: ExposureMode; }>
902
+ ```
903
+
904
+ Returns the current exposure mode.
905
+
906
+ **Returns:** <code>Promise&lt;{ mode: <a href="#exposuremode">ExposureMode</a>; }&gt;</code>
907
+
908
+ --------------------
909
+
910
+
911
+ ### setExposureMode(...)
912
+
913
+ ```typescript
914
+ setExposureMode(options: { mode: ExposureMode; }) => Promise<void>
915
+ ```
916
+
917
+ Sets the exposure mode.
918
+
919
+ | Param | Type |
920
+ | ------------- | ---------------------------------------------------------------- |
921
+ | **`options`** | <code>{ mode: <a href="#exposuremode">ExposureMode</a>; }</code> |
922
+
923
+ --------------------
924
+
925
+
926
+ ### getExposureCompensationRange()
927
+
928
+ ```typescript
929
+ getExposureCompensationRange() => Promise<{ min: number; max: number; step: number; }>
930
+ ```
931
+
932
+ Returns the exposure compensation (EV bias) supported range.
933
+
934
+ **Returns:** <code>Promise&lt;{ min: number; max: number; step: number; }&gt;</code>
935
+
936
+ --------------------
937
+
938
+
939
+ ### getExposureCompensation()
940
+
941
+ ```typescript
942
+ getExposureCompensation() => Promise<{ value: number; }>
943
+ ```
944
+
945
+ Returns the current exposure compensation (EV bias).
946
+
947
+ **Returns:** <code>Promise&lt;{ value: number; }&gt;</code>
948
+
949
+ --------------------
950
+
951
+
952
+ ### setExposureCompensation(...)
953
+
954
+ ```typescript
955
+ setExposureCompensation(options: { value: number; }) => Promise<void>
956
+ ```
957
+
958
+ Sets the exposure compensation (EV bias). Value will be clamped to range.
959
+
960
+ | Param | Type |
961
+ | ------------- | ------------------------------- |
962
+ | **`options`** | <code>{ value: number; }</code> |
963
+
964
+ --------------------
965
+
966
+
831
967
  ### Interfaces
832
968
 
833
969
 
@@ -1023,6 +1159,13 @@ Canonical device orientation values across platforms.
1023
1159
  <code>"portrait" | "landscape" | "landscape-left" | "landscape-right" | "portrait-upside-down" | "unknown"</code>
1024
1160
 
1025
1161
 
1162
+ #### ExposureMode
1163
+
1164
+ Reusable exposure mode type for cross-platform support.
1165
+
1166
+ <code>"AUTO" | "LOCK" | "CONTINUOUS" | "CUSTOM"</code>
1167
+
1168
+
1026
1169
  ### Enums
1027
1170
 
1028
1171
 
@@ -53,13 +53,13 @@ dependencies {
53
53
  implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
54
54
 
55
55
  // CameraX dependencies
56
- def camerax_version = "1.5.0-beta01"
56
+ def camerax_version = "1.5.0-rc01"
57
57
  implementation "androidx.camera:camera-core:${camerax_version}"
58
58
  implementation "androidx.camera:camera-camera2:${camerax_version}"
59
59
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
60
60
  implementation "androidx.camera:camera-view:${camerax_version}"
61
61
  implementation "androidx.camera:camera-extensions:${camerax_version}"
62
-
62
+
63
63
  testImplementation "junit:junit:$junitVersion"
64
64
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
65
65
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -80,8 +80,106 @@ public class CameraPreview
80
80
  private Location lastLocation;
81
81
  private OrientationEventListener orientationListener;
82
82
  private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
83
+ private boolean lastDisableAudio = true;
83
84
 
84
85
  @PluginMethod
86
+ public void getExposureModes(PluginCall call) {
87
+ if (cameraXView == null || !cameraXView.isRunning()) {
88
+ call.reject("Camera is not running");
89
+ return;
90
+ }
91
+ JSArray arr = new JSArray();
92
+ for (String m : cameraXView.getExposureModes()) arr.put(m);
93
+ JSObject ret = new JSObject();
94
+ ret.put("modes", arr);
95
+ call.resolve(ret);
96
+ }
97
+
98
+ @PluginMethod
99
+ public void getExposureMode(PluginCall call) {
100
+ if (cameraXView == null || !cameraXView.isRunning()) {
101
+ call.reject("Camera is not running");
102
+ return;
103
+ }
104
+ JSObject ret = new JSObject();
105
+ ret.put("mode", cameraXView.getExposureMode());
106
+ call.resolve(ret);
107
+ }
108
+
109
+ @PluginMethod
110
+ public void setExposureMode(PluginCall call) {
111
+ if (cameraXView == null || !cameraXView.isRunning()) {
112
+ call.reject("Camera is not running");
113
+ return;
114
+ }
115
+ String mode = call.getString("mode");
116
+ if (mode == null || mode.isEmpty()) {
117
+ call.reject("mode parameter is required");
118
+ return;
119
+ }
120
+ try {
121
+ cameraXView.setExposureMode(mode);
122
+ call.resolve();
123
+ } catch (Exception e) {
124
+ call.reject("Failed to set exposure mode: " + e.getMessage());
125
+ }
126
+ }
127
+
128
+ @PluginMethod
129
+ public void getExposureCompensationRange(PluginCall call) {
130
+ if (cameraXView == null || !cameraXView.isRunning()) {
131
+ call.reject("Camera is not running");
132
+ return;
133
+ }
134
+ try {
135
+ float[] range = cameraXView.getExposureCompensationRange();
136
+ JSObject ret = new JSObject();
137
+ ret.put("min", range[0]);
138
+ ret.put("max", range[1]);
139
+ ret.put("step", range.length > 2 ? range[2] : 0.1);
140
+ call.resolve(ret);
141
+ } catch (Exception e) {
142
+ call.reject(
143
+ "Failed to get exposure compensation range: " + e.getMessage()
144
+ );
145
+ }
146
+ }
147
+
148
+ @PluginMethod
149
+ public void getExposureCompensation(PluginCall call) {
150
+ if (cameraXView == null || !cameraXView.isRunning()) {
151
+ call.reject("Camera is not running");
152
+ return;
153
+ }
154
+ try {
155
+ float value = cameraXView.getExposureCompensation();
156
+ JSObject ret = new JSObject();
157
+ ret.put("value", value);
158
+ call.resolve(ret);
159
+ } catch (Exception e) {
160
+ call.reject("Failed to get exposure compensation: " + e.getMessage());
161
+ }
162
+ }
163
+
164
+ @PluginMethod
165
+ public void setExposureCompensation(PluginCall call) {
166
+ if (cameraXView == null || !cameraXView.isRunning()) {
167
+ call.reject("Camera is not running");
168
+ return;
169
+ }
170
+ Float value = call.getFloat("value");
171
+ if (value == null) {
172
+ call.reject("value parameter is required");
173
+ return;
174
+ }
175
+ try {
176
+ cameraXView.setExposureCompensation(value);
177
+ call.resolve();
178
+ } catch (Exception e) {
179
+ call.reject("Failed to set exposure compensation: " + e.getMessage());
180
+ }
181
+ }
182
+
85
183
  public void getOrientation(PluginCall call) {
86
184
  int orientation = getContext()
87
185
  .getResources()
@@ -592,6 +690,7 @@ public class CameraPreview
592
690
  final boolean disableAudio = Boolean.TRUE.equals(
593
691
  call.getBoolean("disableAudio", true)
594
692
  );
693
+ this.lastDisableAudio = disableAudio;
595
694
  final String aspectRatio = call.getString("aspectRatio", "4:3");
596
695
  final String gridMode = call.getString("gridMode", "none");
597
696
  final String positioning = call.getString("positioning", "top");
@@ -1669,6 +1768,101 @@ public class CameraPreview
1669
1768
  }
1670
1769
  }
1671
1770
 
1771
+ @PluginMethod
1772
+ public void startRecordVideo(PluginCall call) {
1773
+ if (cameraXView == null || !cameraXView.isRunning()) {
1774
+ call.reject("Camera is not running");
1775
+ return;
1776
+ }
1777
+
1778
+ boolean disableAudio = call.getBoolean("disableAudio") != null
1779
+ ? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
1780
+ : this.lastDisableAudio;
1781
+ String permissionAlias = disableAudio
1782
+ ? CAMERA_ONLY_PERMISSION_ALIAS
1783
+ : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
1784
+
1785
+ if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
1786
+ try {
1787
+ cameraXView.startRecordVideo();
1788
+ call.resolve();
1789
+ } catch (Exception e) {
1790
+ call.reject("Failed to start video recording: " + e.getMessage());
1791
+ }
1792
+ } else {
1793
+ requestPermissionForAlias(
1794
+ permissionAlias,
1795
+ call,
1796
+ "handleVideoRecordingPermissionResult"
1797
+ );
1798
+ }
1799
+ }
1800
+
1801
+ @PluginMethod
1802
+ public void stopRecordVideo(PluginCall call) {
1803
+ if (cameraXView == null || !cameraXView.isRunning()) {
1804
+ call.reject("Camera is not running");
1805
+ return;
1806
+ }
1807
+
1808
+ try {
1809
+ bridge.saveCall(call);
1810
+ final String cbId = call.getCallbackId();
1811
+ cameraXView.stopRecordVideo(
1812
+ new CameraXView.VideoRecordingCallback() {
1813
+ @Override
1814
+ public void onSuccess(String filePath) {
1815
+ PluginCall saved = bridge.getSavedCall(cbId);
1816
+ if (saved != null) {
1817
+ JSObject result = new JSObject();
1818
+ result.put("videoFilePath", filePath);
1819
+ saved.resolve(result);
1820
+ bridge.releaseCall(saved);
1821
+ }
1822
+ }
1823
+
1824
+ @Override
1825
+ public void onError(String message) {
1826
+ PluginCall saved = bridge.getSavedCall(cbId);
1827
+ if (saved != null) {
1828
+ saved.reject("Failed to stop video recording: " + message);
1829
+ bridge.releaseCall(saved);
1830
+ }
1831
+ }
1832
+ }
1833
+ );
1834
+ } catch (Exception e) {
1835
+ call.reject("Failed to stop video recording: " + e.getMessage());
1836
+ }
1837
+ }
1838
+
1839
+ @PermissionCallback
1840
+ private void handleVideoRecordingPermissionResult(PluginCall call) {
1841
+ // Use the persisted session value to determine which permission we requested
1842
+ String permissionAlias = this.lastDisableAudio
1843
+ ? CAMERA_ONLY_PERMISSION_ALIAS
1844
+ : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
1845
+
1846
+ // Check if either permission is granted (mirroring handleCameraPermissionResult)
1847
+ if (
1848
+ PermissionState.GRANTED.equals(
1849
+ getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
1850
+ ) ||
1851
+ PermissionState.GRANTED.equals(
1852
+ getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
1853
+ )
1854
+ ) {
1855
+ try {
1856
+ cameraXView.startRecordVideo();
1857
+ call.resolve();
1858
+ } catch (Exception e) {
1859
+ call.reject("Failed to start video recording: " + e.getMessage());
1860
+ }
1861
+ } else {
1862
+ call.reject("Permission denied for video recording");
1863
+ }
1864
+ }
1865
+
1672
1866
  @PluginMethod
1673
1867
  public void getSafeAreaInsets(PluginCall call) {
1674
1868
  JSObject ret = new JSObject();