@capgo/camera-preview 8.6.0 → 8.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -412,6 +412,9 @@ Documentation for the [uploader](https://github.com/Cap-go/capacitor-uploader)
412
412
  * [`getExposureCompensationRange()`](#getexposurecompensationrange)
413
413
  * [`getExposureCompensation()`](#getexposurecompensation)
414
414
  * [`setExposureCompensation(...)`](#setexposurecompensation)
415
+ * [`getSupportedVideoFrameRates()`](#getsupportedvideoframerates)
416
+ * [`getVideoFrameRate()`](#getvideoframerate)
417
+ * [`setVideoFrameRate(...)`](#setvideoframerate)
415
418
  * [`getPluginVersion()`](#getpluginversion)
416
419
  * [Interfaces](#interfaces)
417
420
  * [Type Aliases](#type-aliases)
@@ -759,9 +762,11 @@ startRecordVideo(options: CameraPreviewOptions) => Promise<void>
759
762
 
760
763
  Starts recording a video.
761
764
 
762
- | Param | Type | Description |
763
- | ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
764
- | **`options`** | <code><a href="#camerapreviewoptions">CameraPreviewOptions</a></code> | - The options for video recording. Supports `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, and `disableAudio`. |
765
+ Supports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, and `disableAudio` on each call.
766
+
767
+ | Param | Type | Description |
768
+ | ------------- | --------------------------------------------------------------------- | ---------------------------------- |
769
+ | **`options`** | <code><a href="#camerapreviewoptions">CameraPreviewOptions</a></code> | - The options for video recording. |
765
770
 
766
771
  **Since:** 0.0.1
767
772
 
@@ -1359,6 +1364,57 @@ Sets the exposure compensation (EV bias). Value will be clamped to range.
1359
1364
  --------------------
1360
1365
 
1361
1366
 
1367
+ ### getSupportedVideoFrameRates()
1368
+
1369
+ ```typescript
1370
+ getSupportedVideoFrameRates() => Promise<{ frameRates: number[]; }>
1371
+ ```
1372
+
1373
+ Lists the video frame rates supported by the active camera for the current format.
1374
+ Supported values depend on the selected camera, lens, and video quality.
1375
+
1376
+ **Returns:** <code>Promise&lt;{ frameRates: number[]; }&gt;</code>
1377
+
1378
+ **Since:** 8.6.0
1379
+
1380
+ --------------------
1381
+
1382
+
1383
+ ### getVideoFrameRate()
1384
+
1385
+ ```typescript
1386
+ getVideoFrameRate() => Promise<{ frameRate: number; }>
1387
+ ```
1388
+
1389
+ Returns the configured video frame rate for the active camera.
1390
+ On Android the actual recording frame rate can still vary in low light or under thermal pressure.
1391
+
1392
+ **Returns:** <code>Promise&lt;{ frameRate: number; }&gt;</code>
1393
+
1394
+ **Since:** 8.6.0
1395
+
1396
+ --------------------
1397
+
1398
+
1399
+ ### setVideoFrameRate(...)
1400
+
1401
+ ```typescript
1402
+ setVideoFrameRate(options: { frameRate: number; }) => Promise<void>
1403
+ ```
1404
+
1405
+ Sets the target video frame rate for the active camera session.
1406
+ Prefer passing `frameRate` to `startRecordVideo()` when starting a recording.
1407
+ Rejects unsupported values with a clear error.
1408
+
1409
+ | Param | Type |
1410
+ | ------------- | ----------------------------------- |
1411
+ | **`options`** | <code>{ frameRate: number; }</code> |
1412
+
1413
+ **Since:** 8.6.0
1414
+
1415
+ --------------------
1416
+
1417
+
1362
1418
  ### getPluginVersion()
1363
1419
 
1364
1420
  ```typescript
@@ -1411,6 +1467,7 @@ Defines the configuration options for starting the camera preview.
1411
1467
  | **`maxDuration`** | <code>number</code> | Maximum recording duration in seconds. Recording stops automatically when reached. | | |
1412
1468
  | **`maxFileSize`** | <code>number</code> | Maximum recording file size in bytes. Recording stops automatically when reached. | | |
1413
1469
  | **`videoCodec`** | <code><a href="#videocodec">VideoCodec</a></code> | Preferred video codec for recording. | <code>"avc1"</code> | |
1470
+ | **`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 |
1414
1471
  | **`videoStabilizationMode`** | <code><a href="#videostabilizationmode">VideoStabilizationMode</a></code> | Preferred video stabilization mode for recording. | <code>"off"</code> | |
1415
1472
  | **`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 |
1416
1473
 
@@ -291,6 +291,56 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
291
291
  }
292
292
  }
293
293
 
294
+ @PluginMethod
295
+ public void getSupportedVideoFrameRates(PluginCall call) {
296
+ if (cameraXView == null || !cameraXView.isRunning()) {
297
+ call.reject("Camera is not running");
298
+ return;
299
+ }
300
+ try {
301
+ List<Integer> frameRates = cameraXView.getSupportedVideoFrameRates();
302
+ JSObject ret = new JSObject();
303
+ JSONArray frameRatesArray = new JSONArray();
304
+ for (Integer frameRate : frameRates) {
305
+ frameRatesArray.put(frameRate);
306
+ }
307
+ ret.put("frameRates", frameRatesArray);
308
+ call.resolve(ret);
309
+ } catch (Exception e) {
310
+ call.reject("Failed to get supported video frame rates: " + e.getMessage());
311
+ }
312
+ }
313
+
314
+ @PluginMethod
315
+ public void getVideoFrameRate(PluginCall call) {
316
+ if (cameraXView == null || !cameraXView.isRunning()) {
317
+ call.reject("Camera is not running");
318
+ return;
319
+ }
320
+ try {
321
+ int frameRate = cameraXView.getVideoFrameRate();
322
+ JSObject ret = new JSObject();
323
+ ret.put("frameRate", frameRate);
324
+ call.resolve(ret);
325
+ } catch (Exception e) {
326
+ call.reject("Failed to get video frame rate: " + e.getMessage());
327
+ }
328
+ }
329
+
330
+ @PluginMethod
331
+ public void setVideoFrameRate(PluginCall call) {
332
+ if (cameraXView == null || !cameraXView.isRunning()) {
333
+ call.reject("Camera is not running");
334
+ return;
335
+ }
336
+ Integer frameRate = call.getInt("frameRate");
337
+ if (frameRate == null) {
338
+ call.reject("frameRate parameter is required");
339
+ return;
340
+ }
341
+ cameraXView.setVideoFrameRate(frameRate, call::resolve, call::reject);
342
+ }
343
+
294
344
  @PluginMethod
295
345
  public void getOrientation(PluginCall call) {
296
346
  String o = getDeviceOrientationString();
@@ -2773,14 +2823,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2773
2823
  String permissionAlias = disableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
2774
2824
 
2775
2825
  if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
2776
- try {
2777
- applyVideoCodecFromCall(call);
2778
- applyVideoStabilizationFromCall(call);
2779
- cameraXView.startRecordVideo(getMaxDurationMillis(call), getMaxFileSize(call));
2780
- call.resolve();
2781
- } catch (Exception e) {
2782
- call.reject("Failed to start video recording: " + e.getMessage());
2783
- }
2826
+ beginVideoRecording(call);
2784
2827
  } else {
2785
2828
  requestPermissionForAlias(permissionAlias, call, "handleVideoRecordingPermissionResult");
2786
2829
  }
@@ -2858,6 +2901,23 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2858
2901
  return bytes > 0L ? bytes : null;
2859
2902
  }
2860
2903
 
2904
+ private void beginVideoRecording(PluginCall call) {
2905
+ if (cameraXView == null || !cameraXView.isRunning()) {
2906
+ call.reject("Camera is not running");
2907
+ return;
2908
+ }
2909
+ try {
2910
+ applyVideoCodecFromCall(call);
2911
+ applyVideoStabilizationFromCall(call);
2912
+ Integer frameRate = call.getInt("frameRate");
2913
+ cameraXView.startRecordVideo(getMaxDurationMillis(call), getMaxFileSize(call), frameRate, call::resolve, (message) ->
2914
+ call.reject("Failed to start video recording: " + message)
2915
+ );
2916
+ } catch (Exception e) {
2917
+ call.reject("Failed to start video recording: " + e.getMessage());
2918
+ }
2919
+ }
2920
+
2861
2921
  @PermissionCallback
2862
2922
  private void handleVideoRecordingPermissionResult(PluginCall call) {
2863
2923
  // Use the persisted session value to determine which permission we requested
@@ -2868,14 +2928,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2868
2928
  PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)) ||
2869
2929
  PermissionState.GRANTED.equals(getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS))
2870
2930
  ) {
2871
- try {
2872
- applyVideoCodecFromCall(call);
2873
- applyVideoStabilizationFromCall(call);
2874
- cameraXView.startRecordVideo(getMaxDurationMillis(call), getMaxFileSize(call));
2875
- call.resolve();
2876
- } catch (Exception e) {
2877
- call.reject("Failed to start video recording: " + e.getMessage());
2878
- }
2931
+ beginVideoRecording(call);
2879
2932
  } else {
2880
2933
  call.reject("camera permission denied. enable camera access in Settings.", "cameraPermissionDenied");
2881
2934
  }
@@ -45,6 +45,7 @@ import androidx.annotation.NonNull;
45
45
  import androidx.annotation.OptIn;
46
46
  import androidx.camera.camera2.interop.Camera2CameraControl;
47
47
  import androidx.camera.camera2.interop.Camera2CameraInfo;
48
+ import androidx.camera.camera2.interop.Camera2Interop;
48
49
  import androidx.camera.camera2.interop.CaptureRequestOptions;
49
50
  import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
50
51
  import androidx.camera.core.AspectRatio;
@@ -110,6 +111,7 @@ import java.util.Locale;
110
111
  import java.util.Map;
111
112
  import java.util.Objects;
112
113
  import java.util.Set;
114
+ import java.util.TreeSet;
113
115
  import java.util.concurrent.ConcurrentHashMap;
114
116
  import java.util.concurrent.Executor;
115
117
  import java.util.concurrent.ExecutorService;
@@ -184,6 +186,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
184
186
  private boolean isRunning = false;
185
187
  private Size currentPreviewResolution = null;
186
188
  private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
189
+ private Integer configuredVideoFrameRate = null;
190
+ private Range<Integer> configuredVideoFrameRateRange = null;
191
+ private Runnable pendingFrameRateBindSuccess;
192
+ private java.util.function.Consumer<String> pendingFrameRateBindError;
187
193
  private String currentExposureMode = "CONTINUOUS"; // Default behavior
188
194
  // Capture/stop coordination
189
195
  private final Object captureLock = new Object();
@@ -1071,7 +1077,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1071
1077
  ? previewView.getDisplay().getRotation()
1072
1078
  : android.view.Surface.ROTATION_0;
1073
1079
 
1074
- Preview preview = new Preview.Builder().setResolutionSelector(resolutionSelector).setTargetRotation(rotation).build();
1080
+ Preview.Builder previewBuilder = new Preview.Builder()
1081
+ .setResolutionSelector(resolutionSelector)
1082
+ .setTargetRotation(rotation);
1083
+ previewBuilder = applyTargetFpsToPreviewBuilder(previewBuilder);
1084
+ Preview preview = previewBuilder.build();
1075
1085
  // Keep reference to preview use case for later re-binding (e.g., when enabling video)
1076
1086
  imageCapture = new ImageCapture.Builder()
1077
1087
  .setResolutionSelector(resolutionSelector)
@@ -1140,6 +1150,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1140
1150
  }
1141
1151
  Recorder recorder = recorderBuilder.build();
1142
1152
  VideoCapture.Builder<Recorder> videoCaptureBuilder = new VideoCapture.Builder<>(recorder);
1153
+ videoCaptureBuilder = applyTargetFpsToVideoCaptureBuilder(videoCaptureBuilder);
1143
1154
  videoCaptureBuilder.setVideoStabilizationEnabled(isVideoStabilizationEnabledForSession());
1144
1155
  videoCapture = videoCaptureBuilder.build();
1145
1156
  }
@@ -1289,6 +1300,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1289
1300
  }
1290
1301
 
1291
1302
  isRunning = true;
1303
+ completePendingFrameRateBindSuccess();
1292
1304
  Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
1293
1305
  if (listener != null) {
1294
1306
  if (sessionConfig != null && sessionConfig.isToBack()) {
@@ -1326,6 +1338,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1326
1338
  } catch (Exception e) {
1327
1339
  // Restore webView background on error
1328
1340
  restoreWebViewBackground();
1341
+ completePendingFrameRateBindError("Error binding camera: " + e.getMessage());
1329
1342
  if (listener != null) listener.onCameraStartError(this, "Error binding camera: " + e.getMessage());
1330
1343
  }
1331
1344
  });
@@ -3357,6 +3370,185 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3357
3370
  }
3358
3371
  }
3359
3372
 
3373
+ private void clearConfiguredVideoFrameRate() {
3374
+ configuredVideoFrameRate = null;
3375
+ configuredVideoFrameRateRange = null;
3376
+ }
3377
+
3378
+ private Range<Integer>[] getAdvertisedFpsRanges() throws Exception {
3379
+ CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
3380
+ String cameraId = resolveActiveCameraIdForCharacteristics();
3381
+ CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
3382
+ return characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
3383
+ }
3384
+
3385
+ private Range<Integer> findAdvertisedFpsRangeForRate(int frameRate, Range<Integer>[] ranges) {
3386
+ if (ranges == null) {
3387
+ return null;
3388
+ }
3389
+
3390
+ Range<Integer> containingRange = null;
3391
+ for (Range<Integer> range : ranges) {
3392
+ if (range.getLower() > frameRate || range.getUpper() < frameRate) {
3393
+ continue;
3394
+ }
3395
+ if (range.getLower().equals(range.getUpper()) && range.getLower() == frameRate) {
3396
+ return range;
3397
+ }
3398
+ containingRange = range;
3399
+ }
3400
+ return containingRange;
3401
+ }
3402
+
3403
+ private Range<Integer> resolveConfiguredVideoFrameRateRange() {
3404
+ if (configuredVideoFrameRate == null) {
3405
+ return null;
3406
+ }
3407
+ try {
3408
+ Range<Integer>[] ranges = getAdvertisedFpsRanges();
3409
+ return findAdvertisedFpsRangeForRate(configuredVideoFrameRate, ranges);
3410
+ } catch (Exception e) {
3411
+ Log.w(TAG, "resolveConfiguredVideoFrameRateRange: Failed to resolve FPS range", e);
3412
+ return null;
3413
+ }
3414
+ }
3415
+
3416
+ private void completePendingFrameRateBindSuccess() {
3417
+ if (pendingFrameRateBindSuccess == null) {
3418
+ return;
3419
+ }
3420
+ Runnable callback = pendingFrameRateBindSuccess;
3421
+ pendingFrameRateBindSuccess = null;
3422
+ pendingFrameRateBindError = null;
3423
+ callback.run();
3424
+ }
3425
+
3426
+ private void completePendingFrameRateBindError(String message) {
3427
+ if (pendingFrameRateBindError == null) {
3428
+ return;
3429
+ }
3430
+ java.util.function.Consumer<String> callback = pendingFrameRateBindError;
3431
+ pendingFrameRateBindSuccess = null;
3432
+ pendingFrameRateBindError = null;
3433
+ callback.accept(message);
3434
+ }
3435
+
3436
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
3437
+ private Preview.Builder applyTargetFpsToPreviewBuilder(Preview.Builder builder) {
3438
+ Range<Integer> fpsRange = resolveConfiguredVideoFrameRateRange();
3439
+ if (fpsRange == null) {
3440
+ return builder;
3441
+ }
3442
+
3443
+ Camera2Interop.Extender<Preview> extender = new Camera2Interop.Extender<>(builder);
3444
+ extender.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
3445
+ return builder;
3446
+ }
3447
+
3448
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
3449
+ private VideoCapture.Builder<Recorder> applyTargetFpsToVideoCaptureBuilder(VideoCapture.Builder<Recorder> builder) {
3450
+ Range<Integer> fpsRange = resolveConfiguredVideoFrameRateRange();
3451
+ if (fpsRange == null) {
3452
+ return builder;
3453
+ }
3454
+
3455
+ Camera2Interop.Extender<VideoCapture<Recorder>> extender = new Camera2Interop.Extender<>(builder);
3456
+ extender.setCaptureRequestOption(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);
3457
+ return builder;
3458
+ }
3459
+
3460
+ private String resolveActiveCameraIdForCharacteristics() throws Exception {
3461
+ if (currentLogicalDeviceId != null) {
3462
+ return currentLogicalDeviceId;
3463
+ }
3464
+ if (camera != null) {
3465
+ return Camera2CameraInfo.from(camera.getCameraInfo()).getCameraId();
3466
+ }
3467
+ throw new Exception("Camera not initialized");
3468
+ }
3469
+
3470
+ private List<Integer> frameRatesFromFpsRanges(Range<Integer>[] ranges) {
3471
+ TreeSet<Integer> rates = new TreeSet<>();
3472
+ int[] standardRates = new int[] { 24, 25, 30, 50, 60, 120 };
3473
+
3474
+ if (ranges == null) {
3475
+ return new ArrayList<>();
3476
+ }
3477
+
3478
+ for (Range<Integer> range : ranges) {
3479
+ int min = range.getLower();
3480
+ int max = range.getUpper();
3481
+ if (min == max) {
3482
+ rates.add(min);
3483
+ continue;
3484
+ }
3485
+
3486
+ rates.add(min);
3487
+ rates.add(max);
3488
+ for (int standardRate : standardRates) {
3489
+ if (standardRate >= min && standardRate <= max) {
3490
+ rates.add(standardRate);
3491
+ }
3492
+ }
3493
+ }
3494
+
3495
+ return new ArrayList<>(rates);
3496
+ }
3497
+
3498
+ public List<Integer> getSupportedVideoFrameRates() throws Exception {
3499
+ return frameRatesFromFpsRanges(getAdvertisedFpsRanges());
3500
+ }
3501
+
3502
+ public int getVideoFrameRate() throws Exception {
3503
+ if (configuredVideoFrameRate != null) {
3504
+ return configuredVideoFrameRate;
3505
+ }
3506
+ return 30;
3507
+ }
3508
+
3509
+ public void setVideoFrameRate(int frameRate, Runnable onSuccess, java.util.function.Consumer<String> onError) {
3510
+ mainExecutor.execute(() -> {
3511
+ Integer previousFrameRate = configuredVideoFrameRate;
3512
+ Range<Integer> previousRange = configuredVideoFrameRateRange;
3513
+ try {
3514
+ if (currentRecording != null) {
3515
+ throw new Exception("Cannot change video frame rate while recording");
3516
+ }
3517
+ if (frameRate <= 0) {
3518
+ throw new Exception("frameRate must be greater than 0");
3519
+ }
3520
+
3521
+ List<Integer> supportedRates = getSupportedVideoFrameRates();
3522
+ if (!supportedRates.contains(frameRate)) {
3523
+ throw new Exception("Unsupported frame rate " + frameRate + ". Supported values: " + supportedRates);
3524
+ }
3525
+
3526
+ Range<Integer> advertisedRange = findAdvertisedFpsRangeForRate(frameRate, getAdvertisedFpsRanges());
3527
+ if (advertisedRange == null) {
3528
+ throw new Exception("Unsupported frame rate " + frameRate + " for the active camera");
3529
+ }
3530
+
3531
+ configuredVideoFrameRate = frameRate;
3532
+ configuredVideoFrameRateRange = advertisedRange;
3533
+ if (isRunning && cameraProvider != null) {
3534
+ pendingFrameRateBindSuccess = onSuccess;
3535
+ pendingFrameRateBindError = (message) -> {
3536
+ configuredVideoFrameRate = previousFrameRate;
3537
+ configuredVideoFrameRateRange = previousRange;
3538
+ onError.accept(message);
3539
+ };
3540
+ bindCameraUseCases();
3541
+ return;
3542
+ }
3543
+ onSuccess.run();
3544
+ } catch (Exception e) {
3545
+ configuredVideoFrameRate = previousFrameRate;
3546
+ configuredVideoFrameRateRange = previousRange;
3547
+ onError.accept(e.getMessage());
3548
+ }
3549
+ });
3550
+ }
3551
+
3360
3552
  private long showFocusIndicator(float x, float y) {
3361
3553
  // If preview is gone (e.g., stopping/closing), bail out safely
3362
3554
  if (previewContainer == null) {
@@ -3648,6 +3840,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3648
3840
 
3649
3841
  mainExecutor.execute(() -> {
3650
3842
  try {
3843
+ String previousDeviceId = currentDeviceId;
3651
3844
  CameraSessionConfiguration previousConfig = sessionConfig;
3652
3845
  CameraInfo targetCameraInfo = findAvailableCameraInfoById(deviceId);
3653
3846
  String fallbackPosition = resolveFallbackPositionForDeviceId(deviceId);
@@ -3690,6 +3883,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3690
3883
  );
3691
3884
  copyMutableSessionConfigState(previousConfig, updatedConfig);
3692
3885
  updatedConfig.setTargetZoom(1.0f);
3886
+ if (!Objects.equals(deviceId, previousDeviceId)) {
3887
+ clearConfiguredVideoFrameRate();
3888
+ }
3693
3889
  sessionConfig = updatedConfig;
3694
3890
 
3695
3891
  Log.d(TAG, "switchToDevice: Updated sessionConfig with deviceId: " + deviceId);
@@ -3703,6 +3899,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3703
3899
 
3704
3900
  public void flipCamera() {
3705
3901
  Log.d(TAG, "flipCamera: Flipping camera");
3902
+ clearConfiguredVideoFrameRate();
3706
3903
 
3707
3904
  // Determine current position based on session config and flip it
3708
3905
  String currentPosition = sessionConfig.getPosition();
@@ -4665,6 +4862,30 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
4665
4862
  }
4666
4863
  }
4667
4864
 
4865
+ public void startRecordVideo(
4866
+ Long maxDurationMillis,
4867
+ Long maxFileSize,
4868
+ Integer frameRate,
4869
+ Runnable onSuccess,
4870
+ java.util.function.Consumer<String> onError
4871
+ ) {
4872
+ Runnable startRecording = () -> {
4873
+ try {
4874
+ startRecordVideo(maxDurationMillis, maxFileSize);
4875
+ onSuccess.run();
4876
+ } catch (Exception e) {
4877
+ onError.accept(e.getMessage());
4878
+ }
4879
+ };
4880
+
4881
+ if (frameRate == null) {
4882
+ mainExecutor.execute(startRecording);
4883
+ return;
4884
+ }
4885
+
4886
+ setVideoFrameRate(frameRate, startRecording, onError);
4887
+ }
4888
+
4668
4889
  /** @noinspection ResultOfMethodCallIgnored*/
4669
4890
  public void startRecordVideo(Long maxDurationMillis, Long maxFileSize) throws Exception {
4670
4891
  if (videoCapture == null) {
package/dist/docs.json CHANGED
@@ -520,7 +520,7 @@
520
520
  "parameters": [
521
521
  {
522
522
  "name": "options",
523
- "docs": "- The options for video recording. Supports `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, and `disableAudio`.",
523
+ "docs": "- The options for video recording.",
524
524
  "type": "CameraPreviewOptions"
525
525
  }
526
526
  ],
@@ -528,7 +528,7 @@
528
528
  "tags": [
529
529
  {
530
530
  "name": "param",
531
- "text": "options - The options for video recording. Supports `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, and `disableAudio`."
531
+ "text": "options - The options for video recording."
532
532
  },
533
533
  {
534
534
  "name": "returns",
@@ -539,7 +539,7 @@
539
539
  "text": "0.0.1"
540
540
  }
541
541
  ],
542
- "docs": "Starts recording a video.",
542
+ "docs": "Starts recording a video.\n\nSupports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, and `disableAudio` on each call.",
543
543
  "complexTypes": [
544
544
  "CameraPreviewOptions"
545
545
  ],
@@ -1495,6 +1495,69 @@
1495
1495
  "complexTypes": [],
1496
1496
  "slug": "setexposurecompensation"
1497
1497
  },
1498
+ {
1499
+ "name": "getSupportedVideoFrameRates",
1500
+ "signature": "() => Promise<{ frameRates: number[]; }>",
1501
+ "parameters": [],
1502
+ "returns": "Promise<{ frameRates: number[]; }>",
1503
+ "tags": [
1504
+ {
1505
+ "name": "platform",
1506
+ "text": "android, ios"
1507
+ },
1508
+ {
1509
+ "name": "since",
1510
+ "text": "8.6.0"
1511
+ }
1512
+ ],
1513
+ "docs": "Lists the video frame rates supported by the active camera for the current format.\nSupported values depend on the selected camera, lens, and video quality.",
1514
+ "complexTypes": [],
1515
+ "slug": "getsupportedvideoframerates"
1516
+ },
1517
+ {
1518
+ "name": "getVideoFrameRate",
1519
+ "signature": "() => Promise<{ frameRate: number; }>",
1520
+ "parameters": [],
1521
+ "returns": "Promise<{ frameRate: number; }>",
1522
+ "tags": [
1523
+ {
1524
+ "name": "platform",
1525
+ "text": "android, ios"
1526
+ },
1527
+ {
1528
+ "name": "since",
1529
+ "text": "8.6.0"
1530
+ }
1531
+ ],
1532
+ "docs": "Returns the configured video frame rate for the active camera.\nOn Android the actual recording frame rate can still vary in low light or under thermal pressure.",
1533
+ "complexTypes": [],
1534
+ "slug": "getvideoframerate"
1535
+ },
1536
+ {
1537
+ "name": "setVideoFrameRate",
1538
+ "signature": "(options: { frameRate: number; }) => Promise<void>",
1539
+ "parameters": [
1540
+ {
1541
+ "name": "options",
1542
+ "docs": "",
1543
+ "type": "{ frameRate: number; }"
1544
+ }
1545
+ ],
1546
+ "returns": "Promise<void>",
1547
+ "tags": [
1548
+ {
1549
+ "name": "platform",
1550
+ "text": "android, ios"
1551
+ },
1552
+ {
1553
+ "name": "since",
1554
+ "text": "8.6.0"
1555
+ }
1556
+ ],
1557
+ "docs": "Sets the target video frame rate for the active camera session.\nPrefer passing `frameRate` to `startRecordVideo()` when starting a recording.\nRejects unsupported values with a clear error.",
1558
+ "complexTypes": [],
1559
+ "slug": "setvideoframerate"
1560
+ },
1498
1561
  {
1499
1562
  "name": "getPluginVersion",
1500
1563
  "signature": "() => Promise<{ version: string; }>",
@@ -1981,6 +2044,22 @@
1981
2044
  ],
1982
2045
  "type": "VideoCodec"
1983
2046
  },
2047
+ {
2048
+ "name": "frameRate",
2049
+ "tags": [
2050
+ {
2051
+ "text": "ios, android",
2052
+ "name": "platform"
2053
+ },
2054
+ {
2055
+ "text": "8.6.0",
2056
+ "name": "since"
2057
+ }
2058
+ ],
2059
+ "docs": "Target video frame rate in frames per second.\nApplied when recording starts. Use `getSupportedVideoFrameRates()` to list supported values.",
2060
+ "complexTypes": [],
2061
+ "type": "number | undefined"
2062
+ },
1984
2063
  {
1985
2064
  "name": "videoStabilizationMode",
1986
2065
  "tags": [