@capgo/camera-preview 8.9.0 → 8.11.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
@@ -780,7 +780,7 @@ startRecordVideo(options: CameraPreviewOptions) => Promise<void>
780
780
 
781
781
  Starts recording a video.
782
782
 
783
- Supports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, `disableAudio`, and `allowHapticsAndSystemSoundsDuringRecording` on each call.
783
+ Supports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, `disableAudio`, `allowHapticsAndSystemSoundsDuringRecording`, and `mirrorFrontCamera` on each call.
784
784
 
785
785
  | Param | Type | Description |
786
786
  | ------------- | --------------------------------------------------------------------- | ---------------------------------- |
@@ -1499,42 +1499,43 @@ Get the native Capacitor plugin version
1499
1499
 
1500
1500
  Defines the configuration options for starting the camera preview.
1501
1501
 
1502
- | Prop | Type | Description | Default | Since |
1503
- | ------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------ |
1504
- | **`parent`** | <code>string</code> | The parent element to attach the video preview to. | | |
1505
- | **`className`** | <code>string</code> | A CSS class name to add to the preview element. | | |
1506
- | **`width`** | <code>number</code> | The width of the preview in pixels. Defaults to the screen width. | | |
1507
- | **`height`** | <code>number</code> | The height of the preview in pixels. Defaults to the screen height. | | |
1508
- | **`x`** | <code>number</code> | The horizontal origin of the preview, in pixels. | | |
1509
- | **`y`** | <code>number</code> | The vertical origin of the preview, in pixels. | | |
1510
- | **`aspectRatio`** | <code><a href="#camerapreviewaspectratio">CameraPreviewAspectRatio</a></code> | The aspect ratio of the camera preview, '4:3' or '16:9' or 'fill'. Cannot be set if width or height is provided, otherwise the call will be rejected. Use setPreviewSize to adjust size after starting. | | 2.0.0 |
1511
- | **`aspectMode`** | <code>'cover' \| 'contain'</code> | Controls how the camera preview fills the available space. - 'contain': Fits the entire preview within the space, may show letterboxing (default). - 'cover': Fills the entire space, may crop edges of the preview. | <code>"contain"</code> | |
1512
- | **`gridMode`** | <code><a href="#gridmode">GridMode</a></code> | The grid overlay to display on the camera preview. | <code>"none"</code> | 2.1.0 |
1513
- | **`includeSafeAreaInsets`** | <code>boolean</code> | Adjusts the y-position to account for safe areas (e.g., notches). | <code>false</code> | |
1514
- | **`toBack`** | <code>boolean</code> | If true, places the preview behind the webview. | <code>true</code> | |
1515
- | **`paddingBottom`** | <code>number</code> | Bottom padding for the preview, in pixels. | | |
1516
- | **`rotateWhenOrientationChanged`** | <code>boolean</code> | Whether to rotate the preview when the device orientation changes. | <code>true</code> | |
1517
- | **`position`** | <code>string</code> | The camera to use. | <code>"rear"</code> | |
1518
- | **`storeToFile`** | <code>boolean</code> | If true, saves the captured image to a file and returns the file path. If false, returns a base64 encoded string. | <code>false</code> | |
1519
- | **`disableExifHeaderStripping`** | <code>boolean</code> | If true, prevents the plugin from rotating the image based on EXIF data. | <code>false</code> | |
1520
- | **`disableAudio`** | <code>boolean</code> | If true, disables the audio stream, preventing audio permission requests. | <code>true</code> | |
1521
- | **`lockAndroidOrientation`** | <code>boolean</code> | If true, locks the device orientation while the camera is active. | <code>false</code> | |
1522
- | **`enableOpacity`** | <code>boolean</code> | If true, allows the camera preview's opacity to be changed. | <code>false</code> | |
1523
- | **`disableFocusIndicator`** | <code>boolean</code> | If true, disables the visual focus indicator when tapping to focus. | <code>false</code> | |
1524
- | **`deviceId`** | <code>string</code> | The `deviceId` of the camera to use. If provided, `position` is ignored. | | |
1525
- | **`enablePhysicalDeviceSelection`** | <code>boolean</code> | On Android, attempts to bind a physical camera directly when `deviceId` refers to a physical lens. Disabled by default because OEM support is inconsistent; when false, Android keeps the current logical-camera fallback behavior. | <code>false</code> | |
1526
- | **`initialZoomLevel`** | <code>number</code> | The initial zoom level when starting the camera preview. If the requested zoom level is not available, the native plugin will reject. | <code>1.0</code> | 2.2.0 |
1527
- | **`positioning`** | <code><a href="#camerapositioning">CameraPositioning</a></code> | The vertical positioning of the camera preview. | <code>"center"</code> | 2.3.0 |
1528
- | **`enableVideoMode`** | <code>boolean</code> | If true, enables video capture capabilities when the camera starts. | <code>false</code> | 7.11.0 |
1529
- | **`force`** | <code>boolean</code> | If true, forces the camera to start/restart even if it's already running or busy. This will kill the current camera session and start a new one, ignoring all state checks. | <code>false</code> | |
1530
- | **`videoQuality`** | <code><a href="#videoquality">VideoQuality</a></code> | Sets the quality of video for recording. Options: 'low', 'medium', 'high', '2160p', '1080p', '720p', '480p', '4:3' | <code>"high"</code> | |
1531
- | **`maxDuration`** | <code>number</code> | Maximum recording duration in seconds. Recording stops automatically when reached. | | |
1532
- | **`maxFileSize`** | <code>number</code> | Maximum recording file size in bytes. Recording stops automatically when reached. | | |
1533
- | **`videoCodec`** | <code><a href="#videocodec">VideoCodec</a></code> | Preferred video codec for recording. | <code>"avc1"</code> | |
1534
- | **`frameRate`** | <code>number</code> | Target video frame rate in frames per second. Applied when recording starts. Use `getSupportedVideoFrameRates()` to list supported values. | | 8.6.0 |
1535
- | **`videoStabilizationMode`** | <code><a href="#videostabilizationmode">VideoStabilizationMode</a></code> | Preferred video stabilization mode for recording. | <code>"off"</code> | |
1536
- | **`allowHapticsAndSystemSoundsDuringRecording`** | <code>boolean</code> | When `true`, allows haptic feedback and system sounds while recording video with audio. iOS suppresses haptics during audio input recording unless this is enabled. Requires `disableAudio: false`. | <code>false</code> | 8.8.0 |
1537
- | **`barcodeScanner`** | <code>boolean \| <a href="#barcodescanneroptions">BarcodeScannerOptions</a></code> | Starts barcode scanning together with the camera preview. Set to `true` or pass options to scan all supported formats. Omit this option to keep barcode scanning disabled at startup. | | 8.8.0 |
1502
+ | Prop | Type | Description | Default | Since |
1503
+ | ------------------------------------------------ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------ |
1504
+ | **`parent`** | <code>string</code> | The parent element to attach the video preview to. | | |
1505
+ | **`className`** | <code>string</code> | A CSS class name to add to the preview element. | | |
1506
+ | **`width`** | <code>number</code> | The width of the preview in pixels. Defaults to the screen width. | | |
1507
+ | **`height`** | <code>number</code> | The height of the preview in pixels. Defaults to the screen height. | | |
1508
+ | **`x`** | <code>number</code> | The horizontal origin of the preview, in pixels. | | |
1509
+ | **`y`** | <code>number</code> | The vertical origin of the preview, in pixels. | | |
1510
+ | **`aspectRatio`** | <code><a href="#camerapreviewaspectratio">CameraPreviewAspectRatio</a></code> | The aspect ratio of the camera preview, '4:3' or '16:9' or 'fill'. Cannot be set if width or height is provided, otherwise the call will be rejected. Use setPreviewSize to adjust size after starting. | | 2.0.0 |
1511
+ | **`aspectMode`** | <code>'cover' \| 'contain'</code> | Controls how the camera preview fills the available space. - 'contain': Fits the entire preview within the space, may show letterboxing (default). - 'cover': Fills the entire space, may crop edges of the preview. | <code>"contain"</code> | |
1512
+ | **`gridMode`** | <code><a href="#gridmode">GridMode</a></code> | The grid overlay to display on the camera preview. | <code>"none"</code> | 2.1.0 |
1513
+ | **`includeSafeAreaInsets`** | <code>boolean</code> | Adjusts the y-position to account for safe areas (e.g., notches). | <code>false</code> | |
1514
+ | **`toBack`** | <code>boolean</code> | If true, places the preview behind the webview. | <code>true</code> | |
1515
+ | **`paddingBottom`** | <code>number</code> | Bottom padding for the preview, in pixels. | | |
1516
+ | **`rotateWhenOrientationChanged`** | <code>boolean</code> | Whether to rotate the preview when the device orientation changes. | <code>true</code> | |
1517
+ | **`position`** | <code>string</code> | The camera to use. | <code>"rear"</code> | |
1518
+ | **`storeToFile`** | <code>boolean</code> | If true, saves the captured image to a file and returns the file path. If false, returns a base64 encoded string. | <code>false</code> | |
1519
+ | **`disableExifHeaderStripping`** | <code>boolean</code> | If true, prevents the plugin from rotating the image based on EXIF data. | <code>false</code> | |
1520
+ | **`disableAudio`** | <code>boolean</code> | If true, disables the audio stream, preventing audio permission requests. | <code>true</code> | |
1521
+ | **`lockAndroidOrientation`** | <code>boolean</code> | If true, locks the device orientation while the camera is active. | <code>false</code> | |
1522
+ | **`enableOpacity`** | <code>boolean</code> | If true, allows the camera preview's opacity to be changed. | <code>false</code> | |
1523
+ | **`disableFocusIndicator`** | <code>boolean</code> | If true, disables the visual focus indicator when tapping to focus. | <code>false</code> | |
1524
+ | **`deviceId`** | <code>string</code> | The `deviceId` of the camera to use. If provided, `position` is ignored. | | |
1525
+ | **`enablePhysicalDeviceSelection`** | <code>boolean</code> | On Android, attempts to bind a physical camera directly when `deviceId` refers to a physical lens. Disabled by default because OEM support is inconsistent; when false, Android keeps the current logical-camera fallback behavior. | <code>false</code> | |
1526
+ | **`initialZoomLevel`** | <code>number</code> | The initial zoom level when starting the camera preview. If the requested zoom level is not available, the native plugin will reject. | <code>1.0</code> | 2.2.0 |
1527
+ | **`positioning`** | <code><a href="#camerapositioning">CameraPositioning</a></code> | The vertical positioning of the camera preview. | <code>"center"</code> | 2.3.0 |
1528
+ | **`enableVideoMode`** | <code>boolean</code> | If true, enables video capture capabilities when the camera starts. | <code>false</code> | 7.11.0 |
1529
+ | **`force`** | <code>boolean</code> | If true, forces the camera to start/restart even if it's already running or busy. This will kill the current camera session and start a new one, ignoring all state checks. | <code>false</code> | |
1530
+ | **`videoQuality`** | <code><a href="#videoquality">VideoQuality</a></code> | Sets the quality of video for recording. Options: 'low', 'medium', 'high', '2160p', '1080p', '720p', '480p', '4:3' | <code>"high"</code> | |
1531
+ | **`maxDuration`** | <code>number</code> | Maximum recording duration in seconds. Recording stops automatically when reached. | | |
1532
+ | **`maxFileSize`** | <code>number</code> | Maximum recording file size in bytes. Recording stops automatically when reached. | | |
1533
+ | **`videoCodec`** | <code><a href="#videocodec">VideoCodec</a></code> | Preferred video codec for recording. | <code>"avc1"</code> | |
1534
+ | **`frameRate`** | <code>number</code> | Target video frame rate in frames per second. Applied when recording starts. Use `getSupportedVideoFrameRates()` to list supported values. | | 8.6.0 |
1535
+ | **`videoStabilizationMode`** | <code><a href="#videostabilizationmode">VideoStabilizationMode</a></code> | Preferred video stabilization mode for recording. | <code>"off"</code> | |
1536
+ | **`allowHapticsAndSystemSoundsDuringRecording`** | <code>boolean</code> | When `true`, allows haptic feedback and system sounds while recording video with audio. iOS suppresses haptics during audio input recording unless this is enabled. Requires `disableAudio: false`. | <code>false</code> | 8.8.0 |
1537
+ | **`mirrorFrontCamera`** | <code>boolean</code> | When `true` and the front camera is active, mirrors the recorded video file so it matches the selfie-style preview (text and gestures are not reversed). Does not affect the live preview. Defaults to `false` to preserve existing behavior. | <code>false</code> | 8.10.0 |
1538
+ | **`barcodeScanner`** | <code>boolean \| <a href="#barcodescanneroptions">BarcodeScannerOptions</a></code> | Starts barcode scanning together with the camera preview. Set to `true` or pass options to scan all supported formats. Omit this option to keep barcode scanning disabled at startup. | | 8.8.0 |
1538
1539
 
1539
1540
 
1540
1541
  #### BarcodeScannerOptions
@@ -1565,15 +1566,17 @@ Defines the options for capturing a picture.
1565
1566
  | **`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 |
1566
1567
  | **`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 |
1567
1568
  | **`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 |
1569
+ | **`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 |
1568
1570
 
1569
1571
 
1570
1572
  #### CameraSampleOptions
1571
1573
 
1572
1574
  Defines the options for capturing a sample frame from the camera preview.
1573
1575
 
1574
- | Prop | Type | Description | Default |
1575
- | ------------- | ------------------- | -------------------------------------------------- | --------------- |
1576
- | **`quality`** | <code>number</code> | The quality of the captured sample, from 0 to 100. | <code>85</code> |
1576
+ | Prop | Type | Description | Default | Since |
1577
+ | ----------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------ |
1578
+ | **`quality`** | <code>number</code> | The quality of the captured sample, from 0 to 100. | <code>85</code> | |
1579
+ | **`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 |
1577
1580
 
1578
1581
 
1579
1582
  #### CameraPermissionStatus
@@ -527,8 +527,9 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
527
527
  Integer height = call.getInt("height");
528
528
  final boolean embedTimestamp = Boolean.TRUE.equals(call.getBoolean("embedTimestamp"));
529
529
  final boolean embedLocation = Boolean.TRUE.equals(call.getBoolean("embedLocation"));
530
+ final boolean mirrorFrontCamera = Boolean.TRUE.equals(call.getBoolean("mirrorFrontCamera"));
530
531
 
531
- cameraXView.capturePhoto(quality, saveToGallery, width, height, location, embedTimestamp, embedLocation);
532
+ cameraXView.capturePhoto(quality, saveToGallery, width, height, location, embedTimestamp, embedLocation, mirrorFrontCamera);
532
533
  }
533
534
 
534
535
  @PluginMethod
@@ -540,7 +541,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
540
541
  bridge.saveCall(call);
541
542
  sampleCallbackId = call.getCallbackId();
542
543
  Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
543
- cameraXView.captureSample(quality);
544
+ final boolean mirrorFrontCamera = Boolean.TRUE.equals(call.getBoolean("mirrorFrontCamera"));
545
+ cameraXView.captureSample(quality, mirrorFrontCamera);
544
546
  }
545
547
 
546
548
  @PluginMethod
@@ -2953,8 +2955,14 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2953
2955
  applyVideoCodecFromCall(call);
2954
2956
  applyVideoStabilizationFromCall(call);
2955
2957
  Integer frameRate = call.getInt("frameRate");
2956
- cameraXView.startRecordVideo(getMaxDurationMillis(call), getMaxFileSize(call), frameRate, call::resolve, (message) ->
2957
- call.reject("Failed to start video recording: " + message)
2958
+ boolean mirrorFrontCamera = Boolean.TRUE.equals(call.getBoolean("mirrorFrontCamera", false));
2959
+ cameraXView.startRecordVideo(
2960
+ getMaxDurationMillis(call),
2961
+ getMaxFileSize(call),
2962
+ frameRate,
2963
+ mirrorFrontCamera,
2964
+ call::resolve,
2965
+ (message) -> call.reject("Failed to start video recording: " + message)
2958
2966
  );
2959
2967
  } catch (Exception e) {
2960
2968
  call.reject("Failed to start video recording: " + e.getMessage());
@@ -62,6 +62,7 @@ import androidx.camera.core.ImageCaptureException;
62
62
  import androidx.camera.core.ImageProxy;
63
63
  import androidx.camera.core.MeteringPoint;
64
64
  import androidx.camera.core.MeteringPointFactory;
65
+ import androidx.camera.core.MirrorMode;
65
66
  import androidx.camera.core.Preview;
66
67
  import androidx.camera.core.ResolutionInfo;
67
68
  import androidx.camera.core.TorchState;
@@ -1153,6 +1154,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1153
1154
  VideoCapture.Builder<Recorder> videoCaptureBuilder = new VideoCapture.Builder<>(recorder);
1154
1155
  videoCaptureBuilder = applyTargetFpsToVideoCaptureBuilder(videoCaptureBuilder);
1155
1156
  videoCaptureBuilder.setVideoStabilizationEnabled(isVideoStabilizationEnabledForSession());
1157
+ videoCaptureBuilder.setMirrorMode(
1158
+ sessionConfig.isMirrorFrontCamera() ? MirrorMode.MIRROR_MODE_ON_FRONT_ONLY : MirrorMode.MIRROR_MODE_OFF
1159
+ );
1156
1160
  videoCapture = videoCaptureBuilder.build();
1157
1161
  }
1158
1162
 
@@ -2026,7 +2030,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2026
2030
  Integer height,
2027
2031
  Location location,
2028
2032
  final boolean embedTimestamp,
2029
- final boolean embedLocation
2033
+ final boolean embedLocation,
2034
+ final boolean mirrorFrontCamera
2030
2035
  ) {
2031
2036
  if (imageCapture == null) {
2032
2037
  if (listener != null) {
@@ -2060,7 +2065,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2060
2065
  ", embedTimestamp: " +
2061
2066
  embedTimestamp +
2062
2067
  ", embedLocation: " +
2063
- embedLocation
2068
+ embedLocation +
2069
+ ", mirrorFrontCamera: " +
2070
+ mirrorFrontCamera
2064
2071
  );
2065
2072
 
2066
2073
  boolean dispatched = false;
@@ -2116,6 +2123,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2116
2123
  if (width != null || height != null) {
2117
2124
  Bitmap bitmap = BitmapFactory.decodeByteArray(originalCaptureBytes, 0, originalCaptureBytes.length);
2118
2125
  bitmap = applyExifOrientation(bitmap, exifInterface);
2126
+ bitmap = maybeMirrorFrontCameraBitmap(bitmap, mirrorFrontCamera);
2119
2127
  Bitmap resizedBitmap = resizeBitmapToMaxDimensions(bitmap, width, height);
2120
2128
  if (embedTimestamp || embedLocation) {
2121
2129
  resizedBitmap = drawTimestampAndLocationOntoBitmap(
@@ -2144,6 +2152,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2144
2152
  // No explicit size/ratio: crop to match current preview content
2145
2153
  Bitmap originalBitmap = BitmapFactory.decodeByteArray(originalCaptureBytes, 0, originalCaptureBytes.length);
2146
2154
  originalBitmap = applyExifOrientation(originalBitmap, exifInterface);
2155
+ originalBitmap = maybeMirrorFrontCameraBitmap(originalBitmap, mirrorFrontCamera);
2147
2156
  Bitmap previewCropped = cropBitmapToMatchPreview(originalBitmap);
2148
2157
  if (embedTimestamp || embedLocation) {
2149
2158
  previewCropped = drawTimestampAndLocationOntoBitmap(
@@ -2401,6 +2410,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2401
2410
  }
2402
2411
  }
2403
2412
 
2413
+ private boolean shouldMirrorFrontCamera(boolean mirrorFrontCamera) {
2414
+ return mirrorFrontCamera && sessionConfig != null && "front".equals(sessionConfig.getPosition());
2415
+ }
2416
+
2417
+ private Bitmap maybeMirrorFrontCameraBitmap(Bitmap bitmap, boolean mirrorFrontCamera) {
2418
+ if (bitmap == null || !shouldMirrorFrontCamera(mirrorFrontCamera)) {
2419
+ return bitmap;
2420
+ }
2421
+ return mirrorBitmapHorizontally(bitmap);
2422
+ }
2423
+
2424
+ private Bitmap mirrorBitmapHorizontally(Bitmap bitmap) {
2425
+ Matrix matrix = new Matrix();
2426
+ matrix.preScale(-1.0f, 1.0f);
2427
+ Bitmap mirrored = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
2428
+ if (mirrored != bitmap) {
2429
+ try {
2430
+ bitmap.recycle();
2431
+ } catch (Exception ignore) {}
2432
+ }
2433
+ return mirrored;
2434
+ }
2435
+
2404
2436
  private Bitmap applyExifOrientation(Bitmap bitmap, ExifInterface exif) {
2405
2437
  try {
2406
2438
  int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
@@ -2763,7 +2795,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2763
2795
  // Note: We avoid temporary files for EXIF writes. When we transform pixels (resize/crop),
2764
2796
  // we recompress JPEG in-memory and update EXIF info only in the returned JSON, not in the bytes.
2765
2797
 
2766
- public void captureSample(int quality) {
2798
+ public void captureSample(int quality, final boolean mirrorFrontCamera) {
2767
2799
  if (sampleImageCapture == null) {
2768
2800
  if (listener != null) {
2769
2801
  listener.onSampleTakenError("Camera not ready");
@@ -2775,7 +2807,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2775
2807
  Log.d(TAG, "captureSample: Ignored because stop is pending");
2776
2808
  return;
2777
2809
  }
2778
- Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality);
2810
+ Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality + ", mirrorFrontCamera: " + mirrorFrontCamera);
2779
2811
 
2780
2812
  boolean dispatched = false;
2781
2813
  try {
@@ -2797,6 +2829,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2797
2829
  try {
2798
2830
  // Convert ImageProxy to byte array
2799
2831
  byte[] bytes = imageProxyToByteArray(image);
2832
+ if (shouldMirrorFrontCamera(mirrorFrontCamera)) {
2833
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
2834
+ if (bitmap != null) {
2835
+ Bitmap mirrored = mirrorBitmapHorizontally(bitmap);
2836
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
2837
+ mirrored.compress(Bitmap.CompressFormat.JPEG, quality, stream);
2838
+ bytes = stream.toByteArray();
2839
+ if (mirrored != bitmap) {
2840
+ try {
2841
+ bitmap.recycle();
2842
+ } catch (Exception ignore) {}
2843
+ }
2844
+ }
2845
+ }
2800
2846
  String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
2801
2847
 
2802
2848
  if (listener != null) {
@@ -3535,6 +3581,33 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3535
3581
  }
3536
3582
  }
3537
3583
 
3584
+ private void applyMirrorFrontCamera(boolean mirrorFrontCamera, Runnable onSuccess, java.util.function.Consumer<String> onError) {
3585
+ mainExecutor.execute(() -> {
3586
+ try {
3587
+ if (sessionConfig == null) {
3588
+ throw new Exception("Camera session is not running");
3589
+ }
3590
+ if (currentRecording != null) {
3591
+ throw new Exception("Cannot change mirror mode while recording is in progress");
3592
+ }
3593
+ if (sessionConfig.isMirrorFrontCamera() == mirrorFrontCamera) {
3594
+ onSuccess.run();
3595
+ return;
3596
+ }
3597
+ sessionConfig.setMirrorFrontCamera(mirrorFrontCamera);
3598
+ if (sessionConfig.isVideoModeEnabled() && isRunning && cameraProvider != null) {
3599
+ pendingFrameRateBindSuccess = onSuccess;
3600
+ pendingFrameRateBindError = onError;
3601
+ bindCameraUseCases();
3602
+ return;
3603
+ }
3604
+ onSuccess.run();
3605
+ } catch (Exception e) {
3606
+ onError.accept(e.getMessage());
3607
+ }
3608
+ });
3609
+ }
3610
+
3538
3611
  private void completePendingFrameRateBindSuccess() {
3539
3612
  if (pendingFrameRateBindSuccess == null) {
3540
3613
  return;
@@ -4988,6 +5061,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
4988
5061
  Long maxDurationMillis,
4989
5062
  Long maxFileSize,
4990
5063
  Integer frameRate,
5064
+ boolean mirrorFrontCamera,
4991
5065
  Runnable onSuccess,
4992
5066
  java.util.function.Consumer<String> onError
4993
5067
  ) {
@@ -5000,12 +5074,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
5000
5074
  }
5001
5075
  };
5002
5076
 
5003
- if (frameRate == null) {
5004
- mainExecutor.execute(startRecording);
5005
- return;
5006
- }
5077
+ Runnable afterMirrorConfigured = () -> {
5078
+ if (frameRate == null) {
5079
+ mainExecutor.execute(startRecording);
5080
+ return;
5081
+ }
5082
+ setVideoFrameRate(frameRate, startRecording, onError);
5083
+ };
5007
5084
 
5008
- setVideoFrameRate(frameRate, startRecording, onError);
5085
+ applyMirrorFrontCamera(mirrorFrontCamera, afterMirrorConfigured, onError);
5009
5086
  }
5010
5087
 
5011
5088
  /** @noinspection ResultOfMethodCallIgnored*/
@@ -30,6 +30,7 @@ public class CameraSessionConfiguration {
30
30
  private String videoStabilizationMode = "off";
31
31
  private boolean enablePhysicalDeviceSelection = false;
32
32
  private boolean barcodeScannerEnabled = false;
33
+ private boolean mirrorFrontCamera = false;
33
34
 
34
35
  public CameraSessionConfiguration(
35
36
  String deviceId,
@@ -169,6 +170,14 @@ public class CameraSessionConfiguration {
169
170
  this.videoStabilizationMode = mode != null ? mode : "off";
170
171
  }
171
172
 
173
+ public boolean isMirrorFrontCamera() {
174
+ return mirrorFrontCamera;
175
+ }
176
+
177
+ public void setMirrorFrontCamera(boolean mirrorFrontCamera) {
178
+ this.mirrorFrontCamera = mirrorFrontCamera;
179
+ }
180
+
172
181
  public boolean isPhysicalDeviceSelectionEnabled() {
173
182
  return enablePhysicalDeviceSelection;
174
183
  }
package/dist/docs.json CHANGED
@@ -539,7 +539,7 @@
539
539
  "text": "0.0.1"
540
540
  }
541
541
  ],
542
- "docs": "Starts recording a video.\n\nSupports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, `disableAudio`, and `allowHapticsAndSystemSoundsDuringRecording` on each call.",
542
+ "docs": "Starts recording a video.\n\nSupports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, `disableAudio`, `allowHapticsAndSystemSoundsDuringRecording`, and `mirrorFrontCamera` on each call.",
543
543
  "complexTypes": [
544
544
  "CameraPreviewOptions"
545
545
  ],
@@ -2155,6 +2155,26 @@
2155
2155
  "complexTypes": [],
2156
2156
  "type": "boolean | undefined"
2157
2157
  },
2158
+ {
2159
+ "name": "mirrorFrontCamera",
2160
+ "tags": [
2161
+ {
2162
+ "text": "ios, android",
2163
+ "name": "platform"
2164
+ },
2165
+ {
2166
+ "text": "false",
2167
+ "name": "default"
2168
+ },
2169
+ {
2170
+ "text": "8.10.0",
2171
+ "name": "since"
2172
+ }
2173
+ ],
2174
+ "docs": "When `true` and the front camera is active, mirrors the recorded video file\nso it matches the selfie-style preview (text and gestures are not reversed).\nDoes not affect the live preview. Defaults to `false` to preserve existing behavior.",
2175
+ "complexTypes": [],
2176
+ "type": "boolean | undefined"
2177
+ },
2158
2178
  {
2159
2179
  "name": "barcodeScanner",
2160
2180
  "tags": [
@@ -2343,6 +2363,22 @@
2343
2363
  "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.",
2344
2364
  "complexTypes": [],
2345
2365
  "type": "'speed' | 'balanced' | 'quality' | undefined"
2366
+ },
2367
+ {
2368
+ "name": "mirrorFrontCamera",
2369
+ "tags": [
2370
+ {
2371
+ "text": "false",
2372
+ "name": "default"
2373
+ },
2374
+ {
2375
+ "text": "8.10.0",
2376
+ "name": "since"
2377
+ }
2378
+ ],
2379
+ "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.",
2380
+ "complexTypes": [],
2381
+ "type": "boolean | undefined"
2346
2382
  }
2347
2383
  ]
2348
2384
  },
@@ -2364,6 +2400,22 @@
2364
2400
  "docs": "The quality of the captured sample, from 0 to 100.",
2365
2401
  "complexTypes": [],
2366
2402
  "type": "number | undefined"
2403
+ },
2404
+ {
2405
+ "name": "mirrorFrontCamera",
2406
+ "tags": [
2407
+ {
2408
+ "text": "false",
2409
+ "name": "default"
2410
+ },
2411
+ {
2412
+ "text": "8.10.0",
2413
+ "name": "since"
2414
+ }
2415
+ ],
2416
+ "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.",
2417
+ "complexTypes": [],
2418
+ "type": "boolean | undefined"
2367
2419
  }
2368
2420
  ]
2369
2421
  },
@@ -339,6 +339,15 @@ export interface CameraPreviewOptions {
339
339
  * @since 8.8.0
340
340
  */
341
341
  allowHapticsAndSystemSoundsDuringRecording?: boolean;
342
+ /**
343
+ * When `true` and the front camera is active, mirrors the recorded video file
344
+ * so it matches the selfie-style preview (text and gestures are not reversed).
345
+ * Does not affect the live preview. Defaults to `false` to preserve existing behavior.
346
+ * @platform ios, android
347
+ * @default false
348
+ * @since 8.10.0
349
+ */
350
+ mirrorFrontCamera?: boolean;
342
351
  /**
343
352
  * Starts barcode scanning together with the camera preview.
344
353
  * Set to `true` or pass options to scan all supported formats.
@@ -411,6 +420,14 @@ export interface CameraPreviewPictureOptions {
411
420
  * @default "speed"
412
421
  */
413
422
  photoQualityPrioritization?: 'speed' | 'balanced' | 'quality';
423
+ /**
424
+ * If true and the front camera is active, horizontally mirror the captured photo
425
+ * so it matches a selfie-style preview. Rear camera captures are never mirrored.
426
+ * Defaults to false to preserve the previous non-mirrored behavior.
427
+ * @default false
428
+ * @since 8.10.0
429
+ */
430
+ mirrorFrontCamera?: boolean;
414
431
  }
415
432
  /** Represents EXIF data extracted from an image. */
416
433
  export interface ExifData {
@@ -440,6 +457,14 @@ export interface CameraSampleOptions {
440
457
  * @default 85
441
458
  */
442
459
  quality?: number;
460
+ /**
461
+ * If true and the front camera is active, horizontally mirror the captured sample
462
+ * so it matches a selfie-style preview. Rear camera samples are never mirrored.
463
+ * Defaults to false to preserve the previous non-mirrored behavior.
464
+ * @default false
465
+ * @since 8.10.0
466
+ */
467
+ mirrorFrontCamera?: boolean;
443
468
  }
444
469
  /**
445
470
  * The available flash modes for the camera.
@@ -693,7 +718,7 @@ export interface CameraPreviewPlugin {
693
718
  /**
694
719
  * Starts recording a video.
695
720
  *
696
- * Supports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, `disableAudio`, and `allowHapticsAndSystemSoundsDuringRecording` on each call.
721
+ * Supports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, `disableAudio`, `allowHapticsAndSystemSoundsDuringRecording`, and `mirrorFrontCamera` on each call.
697
722
  *
698
723
  * @param {CameraPreviewOptions} options - The options for video recording.
699
724
  * @returns {Promise<void>} A promise that resolves when video recording starts.