@capgo/camera-preview 8.8.1 → 8.10.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
@@ -427,6 +427,9 @@ Documentation for the [uploader](https://github.com/Cap-go/capacitor-uploader)
427
427
  * [`getExposureCompensationRange()`](#getexposurecompensationrange)
428
428
  * [`getExposureCompensation()`](#getexposurecompensation)
429
429
  * [`setExposureCompensation(...)`](#setexposurecompensation)
430
+ * [`getWhiteBalanceModes()`](#getwhitebalancemodes)
431
+ * [`getWhiteBalanceMode()`](#getwhitebalancemode)
432
+ * [`setWhiteBalanceMode(...)`](#setwhitebalancemode)
430
433
  * [`getSupportedVideoFrameRates()`](#getsupportedvideoframerates)
431
434
  * [`getVideoFrameRate()`](#getvideoframerate)
432
435
  * [`setVideoFrameRate(...)`](#setvideoframerate)
@@ -1379,6 +1382,52 @@ Sets the exposure compensation (EV bias). Value will be clamped to range.
1379
1382
  --------------------
1380
1383
 
1381
1384
 
1385
+ ### getWhiteBalanceModes()
1386
+
1387
+ ```typescript
1388
+ getWhiteBalanceModes() => Promise<{ modes: WhiteBalanceMode[]; }>
1389
+ ```
1390
+
1391
+ Returns the white-balance modes supported by the active camera.
1392
+ Modes can include: 'AUTO', 'LOCK', 'CONTINUOUS'. `CUSTOM` is not listed
1393
+ until manual gains support is implemented.
1394
+
1395
+ **Returns:** <code>Promise&lt;{ modes: WhiteBalanceMode[]; }&gt;</code>
1396
+
1397
+ --------------------
1398
+
1399
+
1400
+ ### getWhiteBalanceMode()
1401
+
1402
+ ```typescript
1403
+ getWhiteBalanceMode() => Promise<{ mode: WhiteBalanceMode; }>
1404
+ ```
1405
+
1406
+ Returns the current white-balance mode.
1407
+
1408
+ **Returns:** <code>Promise&lt;{ mode: <a href="#whitebalancemode">WhiteBalanceMode</a>; }&gt;</code>
1409
+
1410
+ --------------------
1411
+
1412
+
1413
+ ### setWhiteBalanceMode(...)
1414
+
1415
+ ```typescript
1416
+ setWhiteBalanceMode(options: { mode: WhiteBalanceMode; }) => Promise<void>
1417
+ ```
1418
+
1419
+ Sets the white-balance mode. `CONTINUOUS` keeps auto white balance running
1420
+ (recommended; avoids a warm/yellow cast), `LOCK` freezes the current gains,
1421
+ `AUTO` performs a one-time adjustment. `CUSTOM` is reserved and rejected
1422
+ until manual gains support is implemented.
1423
+
1424
+ | Param | Type |
1425
+ | ------------- | ------------------------------------------------------------------------ |
1426
+ | **`options`** | <code>{ mode: <a href="#whitebalancemode">WhiteBalanceMode</a>; }</code> |
1427
+
1428
+ --------------------
1429
+
1430
+
1382
1431
  ### getSupportedVideoFrameRates()
1383
1432
 
1384
1433
  ```typescript
@@ -1516,15 +1565,17 @@ Defines the options for capturing a picture.
1516
1565
  | **`embedTimestamp`** | <code>boolean</code> | If true, the plugin will embed a timestamp in the top-right corner of the image. | <code>false</code> | 7.17.0 |
1517
1566
  | **`embedLocation`** | <code>boolean</code> | If true, the plugin will embed the current location in the top-right corner of the image. Requires `withExifLocation` to be enabled. | <code>false</code> | 7.18.0 |
1518
1567
  | **`photoQualityPrioritization`** | <code>'speed' \| 'balanced' \| 'quality'</code> | Sets the priority for photo quality vs. capture speed. - "speed": Prioritizes faster capture times, may reduce image quality. - "balanced": Aims for a balance between quality and speed. - "quality": Prioritizes image quality, may reduce capture speed. See https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/photoqualityprioritization for details. | <code>"speed"</code> | 7.21.0 |
1568
+ | **`mirrorFrontCamera`** | <code>boolean</code> | If true and the front camera is active, horizontally mirror the captured photo so it matches a selfie-style preview. Rear camera captures are never mirrored. Defaults to false to preserve the previous non-mirrored behavior. | <code>false</code> | 8.10.0 |
1519
1569
 
1520
1570
 
1521
1571
  #### CameraSampleOptions
1522
1572
 
1523
1573
  Defines the options for capturing a sample frame from the camera preview.
1524
1574
 
1525
- | Prop | Type | Description | Default |
1526
- | ------------- | ------------------- | -------------------------------------------------- | --------------- |
1527
- | **`quality`** | <code>number</code> | The quality of the captured sample, from 0 to 100. | <code>85</code> |
1575
+ | Prop | Type | Description | Default | Since |
1576
+ | ----------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------ |
1577
+ | **`quality`** | <code>number</code> | The quality of the captured sample, from 0 to 100. | <code>85</code> | |
1578
+ | **`mirrorFrontCamera`** | <code>boolean</code> | If true and the front camera is active, horizontally mirror the captured sample so it matches a selfie-style preview. Rear camera samples are never mirrored. Defaults to false to preserve the previous non-mirrored behavior. | <code>false</code> | 8.10.0 |
1528
1579
 
1529
1580
 
1530
1581
  #### CameraPermissionStatus
@@ -1774,6 +1825,15 @@ Reusable exposure mode type for cross-platform support.
1774
1825
  <code>'AUTO' | 'LOCK' | 'CONTINUOUS' | 'CUSTOM'</code>
1775
1826
 
1776
1827
 
1828
+ #### WhiteBalanceMode
1829
+
1830
+ Reusable white-balance mode type for cross-platform support.
1831
+ `CUSTOM` is reserved for a future manual white-balance gains API and is not
1832
+ returned by `getWhiteBalanceModes()` until implemented.
1833
+
1834
+ <code>'AUTO' | 'LOCK' | 'CONTINUOUS' | 'CUSTOM'</code>
1835
+
1836
+
1777
1837
  ### Enums
1778
1838
 
1779
1839
 
@@ -195,6 +195,49 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
195
195
  }
196
196
  }
197
197
 
198
+ @PluginMethod
199
+ public void getWhiteBalanceModes(PluginCall call) {
200
+ if (cameraXView == null || !cameraXView.isRunning()) {
201
+ call.reject("Camera is not running");
202
+ return;
203
+ }
204
+ JSArray arr = new JSArray();
205
+ for (String m : cameraXView.getWhiteBalanceModes()) arr.put(m);
206
+ JSObject ret = new JSObject();
207
+ ret.put("modes", arr);
208
+ call.resolve(ret);
209
+ }
210
+
211
+ @PluginMethod
212
+ public void getWhiteBalanceMode(PluginCall call) {
213
+ if (cameraXView == null || !cameraXView.isRunning()) {
214
+ call.reject("Camera is not running");
215
+ return;
216
+ }
217
+ JSObject ret = new JSObject();
218
+ ret.put("mode", cameraXView.getWhiteBalanceMode());
219
+ call.resolve(ret);
220
+ }
221
+
222
+ @PluginMethod
223
+ public void setWhiteBalanceMode(PluginCall call) {
224
+ if (cameraXView == null || !cameraXView.isRunning()) {
225
+ call.reject("Camera is not running");
226
+ return;
227
+ }
228
+ String mode = call.getString("mode");
229
+ if (mode == null || mode.isEmpty()) {
230
+ call.reject("mode parameter is required");
231
+ return;
232
+ }
233
+ try {
234
+ cameraXView.setWhiteBalanceMode(mode);
235
+ call.resolve();
236
+ } catch (Exception e) {
237
+ call.reject("Failed to set white balance mode: " + e.getMessage());
238
+ }
239
+ }
240
+
198
241
  @PluginMethod
199
242
  public void getExposureModes(PluginCall call) {
200
243
  if (cameraXView == null || !cameraXView.isRunning()) {
@@ -484,8 +527,9 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
484
527
  Integer height = call.getInt("height");
485
528
  final boolean embedTimestamp = Boolean.TRUE.equals(call.getBoolean("embedTimestamp"));
486
529
  final boolean embedLocation = Boolean.TRUE.equals(call.getBoolean("embedLocation"));
530
+ final boolean mirrorFrontCamera = Boolean.TRUE.equals(call.getBoolean("mirrorFrontCamera"));
487
531
 
488
- cameraXView.capturePhoto(quality, saveToGallery, width, height, location, embedTimestamp, embedLocation);
532
+ cameraXView.capturePhoto(quality, saveToGallery, width, height, location, embedTimestamp, embedLocation, mirrorFrontCamera);
489
533
  }
490
534
 
491
535
  @PluginMethod
@@ -497,7 +541,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
497
541
  bridge.saveCall(call);
498
542
  sampleCallbackId = call.getCallbackId();
499
543
  Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
500
- cameraXView.captureSample(quality);
544
+ final boolean mirrorFrontCamera = Boolean.TRUE.equals(call.getBoolean("mirrorFrontCamera"));
545
+ cameraXView.captureSample(quality, mirrorFrontCamera);
501
546
  }
502
547
 
503
548
  @PluginMethod
@@ -191,6 +191,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
191
191
  private Runnable pendingFrameRateBindSuccess;
192
192
  private java.util.function.Consumer<String> pendingFrameRateBindError;
193
193
  private String currentExposureMode = "CONTINUOUS"; // Default behavior
194
+ private String currentWhiteBalanceMode = "CONTINUOUS"; // Default behavior
194
195
  // Capture/stop coordination
195
196
  private final Object captureLock = new Object();
196
197
  private volatile boolean isCapturingPhoto = false;
@@ -1183,6 +1184,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1183
1184
  }
1184
1185
 
1185
1186
  resetExposureCompensationToDefault();
1187
+ reapplyCameraControlModes();
1186
1188
 
1187
1189
  // Log details about the active camera
1188
1190
  Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
@@ -2024,7 +2026,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2024
2026
  Integer height,
2025
2027
  Location location,
2026
2028
  final boolean embedTimestamp,
2027
- final boolean embedLocation
2029
+ final boolean embedLocation,
2030
+ final boolean mirrorFrontCamera
2028
2031
  ) {
2029
2032
  if (imageCapture == null) {
2030
2033
  if (listener != null) {
@@ -2058,7 +2061,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2058
2061
  ", embedTimestamp: " +
2059
2062
  embedTimestamp +
2060
2063
  ", embedLocation: " +
2061
- embedLocation
2064
+ embedLocation +
2065
+ ", mirrorFrontCamera: " +
2066
+ mirrorFrontCamera
2062
2067
  );
2063
2068
 
2064
2069
  boolean dispatched = false;
@@ -2114,6 +2119,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2114
2119
  if (width != null || height != null) {
2115
2120
  Bitmap bitmap = BitmapFactory.decodeByteArray(originalCaptureBytes, 0, originalCaptureBytes.length);
2116
2121
  bitmap = applyExifOrientation(bitmap, exifInterface);
2122
+ bitmap = maybeMirrorFrontCameraBitmap(bitmap, mirrorFrontCamera);
2117
2123
  Bitmap resizedBitmap = resizeBitmapToMaxDimensions(bitmap, width, height);
2118
2124
  if (embedTimestamp || embedLocation) {
2119
2125
  resizedBitmap = drawTimestampAndLocationOntoBitmap(
@@ -2142,6 +2148,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2142
2148
  // No explicit size/ratio: crop to match current preview content
2143
2149
  Bitmap originalBitmap = BitmapFactory.decodeByteArray(originalCaptureBytes, 0, originalCaptureBytes.length);
2144
2150
  originalBitmap = applyExifOrientation(originalBitmap, exifInterface);
2151
+ originalBitmap = maybeMirrorFrontCameraBitmap(originalBitmap, mirrorFrontCamera);
2145
2152
  Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
2146
2153
  if (embedTimestamp || embedLocation) {
2147
2154
  previewCropped = drawTimestampAndLocationOntoBitmap(
@@ -2399,6 +2406,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2399
2406
  }
2400
2407
  }
2401
2408
 
2409
+ private boolean shouldMirrorFrontCamera(boolean mirrorFrontCamera) {
2410
+ return mirrorFrontCamera && sessionConfig != null && "front".equals(sessionConfig.getPosition());
2411
+ }
2412
+
2413
+ private Bitmap maybeMirrorFrontCameraBitmap(Bitmap bitmap, boolean mirrorFrontCamera) {
2414
+ if (bitmap == null || !shouldMirrorFrontCamera(mirrorFrontCamera)) {
2415
+ return bitmap;
2416
+ }
2417
+ return mirrorBitmapHorizontally(bitmap);
2418
+ }
2419
+
2420
+ private Bitmap mirrorBitmapHorizontally(Bitmap bitmap) {
2421
+ Matrix matrix = new Matrix();
2422
+ matrix.preScale(-1.0f, 1.0f);
2423
+ Bitmap mirrored = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
2424
+ if (mirrored != bitmap) {
2425
+ try {
2426
+ bitmap.recycle();
2427
+ } catch (Exception ignore) {}
2428
+ }
2429
+ return mirrored;
2430
+ }
2431
+
2402
2432
  private Bitmap applyExifOrientation(Bitmap bitmap, ExifInterface exif) {
2403
2433
  try {
2404
2434
  int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
@@ -2761,7 +2791,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2761
2791
  // Note: We avoid temporary files for EXIF writes. When we transform pixels (resize/crop),
2762
2792
  // we recompress JPEG in-memory and update EXIF info only in the returned JSON, not in the bytes.
2763
2793
 
2764
- public void captureSample(int quality) {
2794
+ public void captureSample(int quality, final boolean mirrorFrontCamera) {
2765
2795
  if (sampleImageCapture == null) {
2766
2796
  if (listener != null) {
2767
2797
  listener.onSampleTakenError("Camera not ready");
@@ -2773,7 +2803,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2773
2803
  Log.d(TAG, "captureSample: Ignored because stop is pending");
2774
2804
  return;
2775
2805
  }
2776
- Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality);
2806
+ Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality + ", mirrorFrontCamera: " + mirrorFrontCamera);
2777
2807
 
2778
2808
  boolean dispatched = false;
2779
2809
  try {
@@ -2795,6 +2825,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2795
2825
  try {
2796
2826
  // Convert ImageProxy to byte array
2797
2827
  byte[] bytes = imageProxyToByteArray(image);
2828
+ if (shouldMirrorFrontCamera(mirrorFrontCamera)) {
2829
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
2830
+ if (bitmap != null) {
2831
+ Bitmap mirrored = mirrorBitmapHorizontally(bitmap);
2832
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
2833
+ mirrored.compress(Bitmap.CompressFormat.JPEG, quality, stream);
2834
+ bytes = stream.toByteArray();
2835
+ if (mirrored != bitmap) {
2836
+ try {
2837
+ bitmap.recycle();
2838
+ } catch (Exception ignore) {}
2839
+ }
2840
+ }
2841
+ }
2798
2842
  String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
2799
2843
 
2800
2844
  if (listener != null) {
@@ -3278,13 +3322,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3278
3322
 
3279
3323
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
3280
3324
  public void setExposureMode(String mode) throws Exception {
3281
- if (camera == null) {
3282
- throw new Exception("Camera not initialized");
3283
- }
3284
3325
  if (mode == null) {
3285
3326
  throw new Exception("mode is required");
3286
3327
  }
3287
3328
  String normalized = mode.toUpperCase(Locale.US);
3329
+ if (!"LOCK".equals(normalized) && !"CONTINUOUS".equals(normalized)) {
3330
+ throw new Exception("Unsupported exposure mode: " + mode);
3331
+ }
3332
+ final String modeToApply = normalized;
3333
+ mainExecutor.execute(() -> {
3334
+ try {
3335
+ applyExposureMode(modeToApply);
3336
+ currentExposureMode = modeToApply;
3337
+ } catch (Exception e) {
3338
+ Log.e(TAG, "setExposureMode: Failed to apply mode", e);
3339
+ }
3340
+ });
3341
+ }
3342
+
3343
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
3344
+ private void applyExposureMode(String normalized) throws Exception {
3345
+ if (camera == null) {
3346
+ throw new Exception("Camera not initialized");
3347
+ }
3288
3348
 
3289
3349
  Camera2CameraControl c2 = Camera2CameraControl.from(camera.getCameraControl());
3290
3350
  switch (normalized) {
@@ -3293,8 +3353,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3293
3353
  .setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, true)
3294
3354
  .setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
3295
3355
  .build();
3296
- mainExecutor.execute(() -> c2.setCaptureRequestOptions(opts));
3297
- currentExposureMode = "LOCK";
3356
+ c2.setCaptureRequestOptions(opts);
3298
3357
  break;
3299
3358
  }
3300
3359
  case "CONTINUOUS": {
@@ -3302,12 +3361,117 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3302
3361
  .setCaptureRequestOption(CaptureRequest.CONTROL_AE_LOCK, false)
3303
3362
  .setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
3304
3363
  .build();
3305
- mainExecutor.execute(() -> c2.setCaptureRequestOptions(opts));
3306
- currentExposureMode = "CONTINUOUS";
3364
+ c2.setCaptureRequestOptions(opts);
3307
3365
  break;
3308
3366
  }
3309
3367
  default:
3310
- throw new Exception("Unsupported exposure mode: " + mode);
3368
+ throw new Exception("Unsupported exposure mode: " + normalized);
3369
+ }
3370
+ }
3371
+
3372
+ // ===================== White Balance APIs =====================
3373
+ public java.util.List<String> getWhiteBalanceModes() {
3374
+ if (camera == null) {
3375
+ return Arrays.asList("LOCK", "CONTINUOUS");
3376
+ }
3377
+
3378
+ java.util.List<String> modes = new java.util.ArrayList<>();
3379
+ try {
3380
+ Camera2CameraInfo c2Info = Camera2CameraInfo.from(camera.getCameraInfo());
3381
+ int[] available = c2Info.getCameraCharacteristic(CameraCharacteristics.CONTROL_AWB_AVAILABLE_MODES);
3382
+ Boolean lockAvailable = c2Info.getCameraCharacteristic(CameraCharacteristics.CONTROL_AWB_LOCK_AVAILABLE);
3383
+
3384
+ if (available != null) {
3385
+ for (int awbMode : available) {
3386
+ if (awbMode == CaptureRequest.CONTROL_AWB_MODE_AUTO && !modes.contains("CONTINUOUS")) {
3387
+ modes.add("CONTINUOUS");
3388
+ }
3389
+ }
3390
+ }
3391
+ if (Boolean.TRUE.equals(lockAvailable) && !modes.contains("LOCK")) {
3392
+ modes.add("LOCK");
3393
+ }
3394
+ } catch (Exception e) {
3395
+ Log.w(TAG, "getWhiteBalanceModes: Failed to query capabilities, using defaults", e);
3396
+ return Arrays.asList("LOCK", "CONTINUOUS");
3397
+ }
3398
+
3399
+ if (modes.isEmpty()) {
3400
+ return Arrays.asList("LOCK", "CONTINUOUS");
3401
+ }
3402
+ return modes;
3403
+ }
3404
+
3405
+ public String getWhiteBalanceMode() {
3406
+ return currentWhiteBalanceMode;
3407
+ }
3408
+
3409
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
3410
+ public void setWhiteBalanceMode(String mode) throws Exception {
3411
+ if (mode == null) {
3412
+ throw new Exception("mode is required");
3413
+ }
3414
+ String normalized = mode.toUpperCase(Locale.US);
3415
+ if ("CUSTOM".equals(normalized)) {
3416
+ throw new Exception("CUSTOM white balance is not supported; manual gains are not yet exposed");
3417
+ }
3418
+ if (!"LOCK".equals(normalized) && !"AUTO".equals(normalized) && !"CONTINUOUS".equals(normalized)) {
3419
+ throw new Exception("Unsupported white balance mode: " + mode);
3420
+ }
3421
+ final String modeToApply = normalized;
3422
+ mainExecutor.execute(() -> {
3423
+ try {
3424
+ applyWhiteBalanceMode(modeToApply);
3425
+ currentWhiteBalanceMode = modeToApply;
3426
+ } catch (Exception e) {
3427
+ Log.e(TAG, "setWhiteBalanceMode: Failed to apply mode", e);
3428
+ }
3429
+ });
3430
+ }
3431
+
3432
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
3433
+ private void applyWhiteBalanceMode(String normalized) throws Exception {
3434
+ if (camera == null) {
3435
+ throw new Exception("Camera not initialized");
3436
+ }
3437
+
3438
+ Camera2CameraControl c2 = Camera2CameraControl.from(camera.getCameraControl());
3439
+ switch (normalized) {
3440
+ case "LOCK": {
3441
+ CaptureRequestOptions opts = new CaptureRequestOptions.Builder()
3442
+ .setCaptureRequestOption(CaptureRequest.CONTROL_AWB_LOCK, true)
3443
+ .setCaptureRequestOption(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO)
3444
+ .build();
3445
+ c2.setCaptureRequestOptions(opts);
3446
+ break;
3447
+ }
3448
+ case "AUTO":
3449
+ case "CONTINUOUS": {
3450
+ CaptureRequestOptions opts = new CaptureRequestOptions.Builder()
3451
+ .setCaptureRequestOption(CaptureRequest.CONTROL_AWB_LOCK, false)
3452
+ .setCaptureRequestOption(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO)
3453
+ .build();
3454
+ c2.setCaptureRequestOptions(opts);
3455
+ break;
3456
+ }
3457
+ default:
3458
+ throw new Exception("Unsupported white balance mode: " + normalized);
3459
+ }
3460
+ }
3461
+
3462
+ private void reapplyCameraControlModes() {
3463
+ if (camera == null) {
3464
+ return;
3465
+ }
3466
+ try {
3467
+ applyExposureMode(currentExposureMode);
3468
+ } catch (Exception e) {
3469
+ Log.w(TAG, "reapplyCameraControlModes: Failed to reapply exposure mode", e);
3470
+ }
3471
+ try {
3472
+ applyWhiteBalanceMode(currentWhiteBalanceMode);
3473
+ } catch (Exception e) {
3474
+ Log.w(TAG, "reapplyCameraControlModes: Failed to reapply white balance mode", e);
3311
3475
  }
3312
3476
  }
3313
3477
 
package/dist/docs.json CHANGED
@@ -1495,6 +1495,63 @@
1495
1495
  "complexTypes": [],
1496
1496
  "slug": "setexposurecompensation"
1497
1497
  },
1498
+ {
1499
+ "name": "getWhiteBalanceModes",
1500
+ "signature": "() => Promise<{ modes: WhiteBalanceMode[]; }>",
1501
+ "parameters": [],
1502
+ "returns": "Promise<{ modes: WhiteBalanceMode[]; }>",
1503
+ "tags": [
1504
+ {
1505
+ "name": "platform",
1506
+ "text": "android, ios"
1507
+ }
1508
+ ],
1509
+ "docs": "Returns the white-balance modes supported by the active camera.\nModes can include: 'AUTO', 'LOCK', 'CONTINUOUS'. `CUSTOM` is not listed\nuntil manual gains support is implemented.",
1510
+ "complexTypes": [
1511
+ "WhiteBalanceMode"
1512
+ ],
1513
+ "slug": "getwhitebalancemodes"
1514
+ },
1515
+ {
1516
+ "name": "getWhiteBalanceMode",
1517
+ "signature": "() => Promise<{ mode: WhiteBalanceMode; }>",
1518
+ "parameters": [],
1519
+ "returns": "Promise<{ mode: WhiteBalanceMode; }>",
1520
+ "tags": [
1521
+ {
1522
+ "name": "platform",
1523
+ "text": "android, ios"
1524
+ }
1525
+ ],
1526
+ "docs": "Returns the current white-balance mode.",
1527
+ "complexTypes": [
1528
+ "WhiteBalanceMode"
1529
+ ],
1530
+ "slug": "getwhitebalancemode"
1531
+ },
1532
+ {
1533
+ "name": "setWhiteBalanceMode",
1534
+ "signature": "(options: { mode: WhiteBalanceMode; }) => Promise<void>",
1535
+ "parameters": [
1536
+ {
1537
+ "name": "options",
1538
+ "docs": "",
1539
+ "type": "{ mode: WhiteBalanceMode; }"
1540
+ }
1541
+ ],
1542
+ "returns": "Promise<void>",
1543
+ "tags": [
1544
+ {
1545
+ "name": "platform",
1546
+ "text": "android, ios"
1547
+ }
1548
+ ],
1549
+ "docs": "Sets the white-balance mode. `CONTINUOUS` keeps auto white balance running\n(recommended; avoids a warm/yellow cast), `LOCK` freezes the current gains,\n`AUTO` performs a one-time adjustment. `CUSTOM` is reserved and rejected\nuntil manual gains support is implemented.",
1550
+ "complexTypes": [
1551
+ "WhiteBalanceMode"
1552
+ ],
1553
+ "slug": "setwhitebalancemode"
1554
+ },
1498
1555
  {
1499
1556
  "name": "getSupportedVideoFrameRates",
1500
1557
  "signature": "() => Promise<{ frameRates: number[]; }>",
@@ -2286,6 +2343,22 @@
2286
2343
  "docs": "Sets the priority for photo quality vs. capture speed.\n- \"speed\": Prioritizes faster capture times, may reduce image quality.\n- \"balanced\": Aims for a balance between quality and speed.\n- \"quality\": Prioritizes image quality, may reduce capture speed.\nSee https://developer.apple.com/documentation/avfoundation/avcapturephotosettings/photoqualityprioritization for details.",
2287
2344
  "complexTypes": [],
2288
2345
  "type": "'speed' | 'balanced' | 'quality' | undefined"
2346
+ },
2347
+ {
2348
+ "name": "mirrorFrontCamera",
2349
+ "tags": [
2350
+ {
2351
+ "text": "false",
2352
+ "name": "default"
2353
+ },
2354
+ {
2355
+ "text": "8.10.0",
2356
+ "name": "since"
2357
+ }
2358
+ ],
2359
+ "docs": "If true and the front camera is active, horizontally mirror the captured photo\nso it matches a selfie-style preview. Rear camera captures are never mirrored.\nDefaults to false to preserve the previous non-mirrored behavior.",
2360
+ "complexTypes": [],
2361
+ "type": "boolean | undefined"
2289
2362
  }
2290
2363
  ]
2291
2364
  },
@@ -2307,6 +2380,22 @@
2307
2380
  "docs": "The quality of the captured sample, from 0 to 100.",
2308
2381
  "complexTypes": [],
2309
2382
  "type": "number | undefined"
2383
+ },
2384
+ {
2385
+ "name": "mirrorFrontCamera",
2386
+ "tags": [
2387
+ {
2388
+ "text": "false",
2389
+ "name": "default"
2390
+ },
2391
+ {
2392
+ "text": "8.10.0",
2393
+ "name": "since"
2394
+ }
2395
+ ],
2396
+ "docs": "If true and the front camera is active, horizontally mirror the captured sample\nso it matches a selfie-style preview. Rear camera samples are never mirrored.\nDefaults to false to preserve the previous non-mirrored behavior.",
2397
+ "complexTypes": [],
2398
+ "type": "boolean | undefined"
2310
2399
  }
2311
2400
  ]
2312
2401
  },
@@ -3210,6 +3299,29 @@
3210
3299
  "complexTypes": []
3211
3300
  }
3212
3301
  ]
3302
+ },
3303
+ {
3304
+ "name": "WhiteBalanceMode",
3305
+ "slug": "whitebalancemode",
3306
+ "docs": "Reusable white-balance mode type for cross-platform support.\n`CUSTOM` is reserved for a future manual white-balance gains API and is not\nreturned by `getWhiteBalanceModes()` until implemented.",
3307
+ "types": [
3308
+ {
3309
+ "text": "'AUTO'",
3310
+ "complexTypes": []
3311
+ },
3312
+ {
3313
+ "text": "'LOCK'",
3314
+ "complexTypes": []
3315
+ },
3316
+ {
3317
+ "text": "'CONTINUOUS'",
3318
+ "complexTypes": []
3319
+ },
3320
+ {
3321
+ "text": "'CUSTOM'",
3322
+ "complexTypes": []
3323
+ }
3324
+ ]
3213
3325
  }
3214
3326
  ],
3215
3327
  "pluginConfigs": []
@@ -411,6 +411,14 @@ export interface CameraPreviewPictureOptions {
411
411
  * @default "speed"
412
412
  */
413
413
  photoQualityPrioritization?: 'speed' | 'balanced' | 'quality';
414
+ /**
415
+ * If true and the front camera is active, horizontally mirror the captured photo
416
+ * so it matches a selfie-style preview. Rear camera captures are never mirrored.
417
+ * Defaults to false to preserve the previous non-mirrored behavior.
418
+ * @default false
419
+ * @since 8.10.0
420
+ */
421
+ mirrorFrontCamera?: boolean;
414
422
  }
415
423
  /** Represents EXIF data extracted from an image. */
416
424
  export interface ExifData {
@@ -440,6 +448,14 @@ export interface CameraSampleOptions {
440
448
  * @default 85
441
449
  */
442
450
  quality?: number;
451
+ /**
452
+ * If true and the front camera is active, horizontally mirror the captured sample
453
+ * so it matches a selfie-style preview. Rear camera samples are never mirrored.
454
+ * Defaults to false to preserve the previous non-mirrored behavior.
455
+ * @default false
456
+ * @since 8.10.0
457
+ */
458
+ mirrorFrontCamera?: boolean;
443
459
  }
444
460
  /**
445
461
  * The available flash modes for the camera.
@@ -448,6 +464,12 @@ export interface CameraSampleOptions {
448
464
  export type CameraPreviewFlashMode = 'off' | 'on' | 'auto' | 'torch';
449
465
  /** Reusable exposure mode type for cross-platform support. */
450
466
  export type ExposureMode = 'AUTO' | 'LOCK' | 'CONTINUOUS' | 'CUSTOM';
467
+ /**
468
+ * Reusable white-balance mode type for cross-platform support.
469
+ * `CUSTOM` is reserved for a future manual white-balance gains API and is not
470
+ * returned by `getWhiteBalanceModes()` until implemented.
471
+ */
472
+ export type WhiteBalanceMode = 'AUTO' | 'LOCK' | 'CONTINUOUS' | 'CUSTOM';
451
473
  /**
452
474
  * Defines the options for setting the camera preview's opacity.
453
475
  */
@@ -1065,6 +1087,32 @@ export interface CameraPreviewPlugin {
1065
1087
  setExposureCompensation(options: {
1066
1088
  value: number;
1067
1089
  }): Promise<void>;
1090
+ /**
1091
+ * Returns the white-balance modes supported by the active camera.
1092
+ * Modes can include: 'AUTO', 'LOCK', 'CONTINUOUS'. `CUSTOM` is not listed
1093
+ * until manual gains support is implemented.
1094
+ * @platform android, ios
1095
+ */
1096
+ getWhiteBalanceModes(): Promise<{
1097
+ modes: WhiteBalanceMode[];
1098
+ }>;
1099
+ /**
1100
+ * Returns the current white-balance mode.
1101
+ * @platform android, ios
1102
+ */
1103
+ getWhiteBalanceMode(): Promise<{
1104
+ mode: WhiteBalanceMode;
1105
+ }>;
1106
+ /**
1107
+ * Sets the white-balance mode. `CONTINUOUS` keeps auto white balance running
1108
+ * (recommended; avoids a warm/yellow cast), `LOCK` freezes the current gains,
1109
+ * `AUTO` performs a one-time adjustment. `CUSTOM` is reserved and rejected
1110
+ * until manual gains support is implemented.
1111
+ * @platform android, ios
1112
+ */
1113
+ setWhiteBalanceMode(options: {
1114
+ mode: WhiteBalanceMode;
1115
+ }): Promise<void>;
1068
1116
  /**
1069
1117
  * Lists the video frame rates supported by the active camera for the current format.
1070
1118
  * Supported values depend on the selected camera, lens, and video quality.