@capgo/camera-preview 8.4.8 → 8.5.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
@@ -376,6 +376,12 @@ Documentation for the [uploader](https://github.com/Cap-go/capacitor-uploader)
376
376
  * [`setOpacity(...)`](#setopacity)
377
377
  * [`stopRecordVideo()`](#stoprecordvideo)
378
378
  * [`startRecordVideo(...)`](#startrecordvideo)
379
+ * [`setVideoQuality(...)`](#setvideoquality)
380
+ * [`getVideoQuality()`](#getvideoquality)
381
+ * [`getSupportedVideoQualities()`](#getsupportedvideoqualities)
382
+ * [`setVideoCodec(...)`](#setvideocodec)
383
+ * [`getVideoCodec()`](#getvideocodec)
384
+ * [`getSupportedVideoCodecs()`](#getsupportedvideocodecs)
379
385
  * [`isRunning()`](#isrunning)
380
386
  * [`getAvailableDevices()`](#getavailabledevices)
381
387
  * [`getZoom()`](#getzoom)
@@ -392,6 +398,7 @@ Documentation for the [uploader](https://github.com/Cap-go/capacitor-uploader)
392
398
  * [`addListener('orientationChange', ...)`](#addlistenerorientationchange-)
393
399
  * [`addListener('barcodeScanned', ...)`](#addlistenerbarcodescanned-)
394
400
  * [`addListener('barcodeScanError', ...)`](#addlistenerbarcodescanerror-)
401
+ * [`addListener('recordingFinished', ...)`](#addlistenerrecordingfinished-)
395
402
  * [`deleteFile(...)`](#deletefile)
396
403
  * [`getSafeAreaInsets()`](#getsafeareainsets)
397
404
  * [`getOrientation()`](#getorientation)
@@ -728,12 +735,12 @@ Sets the opacity of the camera preview.
728
735
  ### stopRecordVideo()
729
736
 
730
737
  ```typescript
731
- stopRecordVideo() => Promise<{ videoFilePath: string; }>
738
+ stopRecordVideo() => Promise<RecordingFinishedEvent>
732
739
  ```
733
740
 
734
741
  Stops an ongoing video recording.
735
742
 
736
- **Returns:** <code>Promise&lt;{ videoFilePath: string; }&gt;</code>
743
+ **Returns:** <code>Promise&lt;<a href="#recordingfinishedevent">RecordingFinishedEvent</a>&gt;</code>
737
744
 
738
745
  **Since:** 0.0.1
739
746
 
@@ -748,15 +755,109 @@ startRecordVideo(options: CameraPreviewOptions) => Promise<void>
748
755
 
749
756
  Starts recording a video.
750
757
 
751
- | Param | Type | Description |
752
- | ------------- | --------------------------------------------------------------------- | -------------------------------------------- |
753
- | **`options`** | <code><a href="#camerapreviewoptions">CameraPreviewOptions</a></code> | - The options for video recording. Only iOS. |
758
+ | Param | Type | Description |
759
+ | ------------- | --------------------------------------------------------------------- | ---------------------------------- |
760
+ | **`options`** | <code><a href="#camerapreviewoptions">CameraPreviewOptions</a></code> | - The options for video recording. |
754
761
 
755
762
  **Since:** 0.0.1
756
763
 
757
764
  --------------------
758
765
 
759
766
 
767
+ ### setVideoQuality(...)
768
+
769
+ ```typescript
770
+ setVideoQuality(options: { quality: VideoQuality; }) => Promise<void>
771
+ ```
772
+
773
+ Sets the video recording quality for the active camera session.
774
+
775
+ | Param | Type | Description |
776
+ | ------------- | ------------------------------------------------------------------- | ---------------------------- |
777
+ | **`options`** | <code>{ quality: <a href="#videoquality">VideoQuality</a>; }</code> | - The desired video quality. |
778
+
779
+ **Since:** 8.5.0
780
+
781
+ --------------------
782
+
783
+
784
+ ### getVideoQuality()
785
+
786
+ ```typescript
787
+ getVideoQuality() => Promise<{ quality: VideoQuality; }>
788
+ ```
789
+
790
+ Gets the current video recording quality.
791
+
792
+ **Returns:** <code>Promise&lt;{ quality: <a href="#videoquality">VideoQuality</a>; }&gt;</code>
793
+
794
+ **Since:** 8.5.0
795
+
796
+ --------------------
797
+
798
+
799
+ ### getSupportedVideoQualities()
800
+
801
+ ```typescript
802
+ getSupportedVideoQualities() => Promise<{ qualities: VideoQuality[]; }>
803
+ ```
804
+
805
+ Returns the video qualities supported by the active camera.
806
+
807
+ **Returns:** <code>Promise&lt;{ qualities: VideoQuality[]; }&gt;</code>
808
+
809
+ **Since:** 8.5.0
810
+
811
+ --------------------
812
+
813
+
814
+ ### setVideoCodec(...)
815
+
816
+ ```typescript
817
+ setVideoCodec(options: { codec: VideoCodec; }) => Promise<void>
818
+ ```
819
+
820
+ Sets the video codec used when recording.
821
+
822
+ | Param | Type | Description |
823
+ | ------------- | ------------------------------------------------------------- | -------------------- |
824
+ | **`options`** | <code>{ codec: <a href="#videocodec">VideoCodec</a>; }</code> | - The desired codec. |
825
+
826
+ **Since:** 8.5.0
827
+
828
+ --------------------
829
+
830
+
831
+ ### getVideoCodec()
832
+
833
+ ```typescript
834
+ getVideoCodec() => Promise<{ codec: VideoCodec; }>
835
+ ```
836
+
837
+ Gets the current video codec used for recording.
838
+
839
+ **Returns:** <code>Promise&lt;{ codec: <a href="#videocodec">VideoCodec</a>; }&gt;</code>
840
+
841
+ **Since:** 8.5.0
842
+
843
+ --------------------
844
+
845
+
846
+ ### getSupportedVideoCodecs()
847
+
848
+ ```typescript
849
+ getSupportedVideoCodecs() => Promise<{ codecs: VideoCodec[]; }>
850
+ ```
851
+
852
+ Returns the video codecs supported by the active camera.
853
+
854
+ **Returns:** <code>Promise&lt;{ codecs: VideoCodec[]; }&gt;</code>
855
+
856
+ **Since:** 8.5.0
857
+
858
+ --------------------
859
+
860
+
760
861
  ### isRunning()
761
862
 
762
863
  ```typescript
@@ -1031,6 +1132,26 @@ Adds a listener for non-fatal barcode scanner errors.
1031
1132
  --------------------
1032
1133
 
1033
1134
 
1135
+ ### addListener('recordingFinished', ...)
1136
+
1137
+ ```typescript
1138
+ addListener(eventName: 'recordingFinished', listenerFunc: (data: RecordingFinishedEvent) => void) => Promise<PluginListenerHandle>
1139
+ ```
1140
+
1141
+ Adds a listener fired when a video recording finishes natively, including automatic stops.
1142
+
1143
+ | Param | Type |
1144
+ | ------------------ | -------------------------------------------------------------------------------------------- |
1145
+ | **`eventName`** | <code>'recordingFinished'</code> |
1146
+ | **`listenerFunc`** | <code>(data: <a href="#recordingfinishedevent">RecordingFinishedEvent</a>) =&gt; void</code> |
1147
+
1148
+ **Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt;</code>
1149
+
1150
+ **Since:** 8.4.7
1151
+
1152
+ --------------------
1153
+
1154
+
1034
1155
  ### deleteFile(...)
1035
1156
 
1036
1157
  ```typescript
@@ -1218,7 +1339,10 @@ Defines the configuration options for starting the camera preview.
1218
1339
  | **`positioning`** | <code><a href="#camerapositioning">CameraPositioning</a></code> | The vertical positioning of the camera preview. | <code>"center"</code> | 2.3.0 |
1219
1340
  | **`enableVideoMode`** | <code>boolean</code> | If true, enables video capture capabilities when the camera starts. | <code>false</code> | 7.11.0 |
1220
1341
  | **`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> | |
1221
- | **`videoQuality`** | <code>'low' \| 'medium' \| 'high'</code> | Sets the quality of video for recording. Options: 'low', 'medium', 'high' | <code>"high"</code> | |
1342
+ | **`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> | |
1343
+ | **`maxDuration`** | <code>number</code> | Maximum recording duration in seconds. Recording stops automatically when reached. | | |
1344
+ | **`maxFileSize`** | <code>number</code> | Maximum recording file size in bytes. Recording stops automatically when reached. | | |
1345
+ | **`videoCodec`** | <code><a href="#videocodec">VideoCodec</a></code> | Preferred video codec for recording. | <code>"avc1"</code> | |
1222
1346
  | **`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 |
1223
1347
 
1224
1348
 
@@ -1310,6 +1434,14 @@ Defines the options for setting the camera preview's opacity.
1310
1434
  | **`opacity`** | <code>number</code> | The opacity percentage, from 0.0 (fully transparent) to 1.0 (fully opaque). | <code>1.0</code> |
1311
1435
 
1312
1436
 
1437
+ #### RecordingFinishedEvent
1438
+
1439
+ | Prop | Type | Description |
1440
+ | ------------------- | --------------------------------------------------------------------------- | ------------------------------------ |
1441
+ | **`videoFilePath`** | <code>string</code> | The path to the recorded video file. |
1442
+ | **`reason`** | <code><a href="#recordingfinishedreason">RecordingFinishedReason</a></code> | Why the recording stopped. |
1443
+
1444
+
1313
1445
  #### CameraDevice
1314
1446
 
1315
1447
  Represents a physical camera on the device (e.g., the front-facing camera).
@@ -1417,6 +1549,20 @@ iOS: Values are expressed in physical pixels and exclude status bar.
1417
1549
  <code>'center' | 'top' | 'bottom'</code>
1418
1550
 
1419
1551
 
1552
+ #### VideoQuality
1553
+
1554
+ <code>'low' | 'medium' | 'high' | '2160p' | '1080p' | '720p' | '480p' | '4:3'</code>
1555
+
1556
+
1557
+ #### VideoCodec
1558
+
1559
+ Video codec identifiers used when recording.
1560
+ - `avc1`: H.264
1561
+ - `hvc1`: HEVC / H.265
1562
+
1563
+ <code>'avc1' | 'hvc1' | 'jpeg' | 'apcn' | 'ap4h'</code>
1564
+
1565
+
1420
1566
  #### BarcodeScannerFormat
1421
1567
 
1422
1568
  <code>'aztec' | 'codabar' | 'code_39' | 'code_93' | 'code_128' | 'data_matrix' | 'ean_8' | 'ean_13' | 'itf' | 'pdf417' | 'qr_code' | 'upc_a' | 'upc_e'</code>
@@ -1447,6 +1593,11 @@ From T, pick a set of properties whose keys are in the union K
1447
1593
  <code>{
1448
1594
  [P in K]: T[P];
1449
1595
  }</code>
1450
1596
 
1451
1597
 
1598
+ #### RecordingFinishedReason
1599
+
1600
+ <code>'manual' | 'maxDuration' | 'maxFileSize'</code>
1601
+
1602
+
1452
1603
  #### FlashMode
1453
1604
 
1454
1605
  <code><a href="#camerapreviewflashmode">CameraPreviewFlashMode</a></code>
@@ -51,6 +51,7 @@ import com.getcapacitor.annotation.PermissionCallback;
51
51
  import com.google.android.gms.location.FusedLocationProviderClient;
52
52
  import com.google.android.gms.location.LocationServices;
53
53
  import java.util.ArrayList;
54
+ import java.util.Arrays;
54
55
  import java.util.List;
55
56
  import java.util.Locale;
56
57
  import java.util.Objects;
@@ -2372,6 +2373,14 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2372
2373
  notifyListeners("barcodeScanError", data);
2373
2374
  }
2374
2375
 
2376
+ @Override
2377
+ public void onVideoRecordingFinished(String filePath, String reason) {
2378
+ JSObject data = new JSObject();
2379
+ data.put("videoFilePath", filePath);
2380
+ data.put("reason", reason);
2381
+ notifyListeners("recordingFinished", data);
2382
+ }
2383
+
2375
2384
  @Override
2376
2385
  public void onCameraStartError(CameraXView source, String message) {
2377
2386
  if (cameraXView != null && cameraXView != source) {
@@ -2602,6 +2611,96 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2602
2611
  }
2603
2612
  }
2604
2613
 
2614
+ @PluginMethod
2615
+ public void setVideoQuality(PluginCall call) {
2616
+ if (cameraXView == null || !cameraXView.isRunning()) {
2617
+ call.reject("Camera is not running");
2618
+ return;
2619
+ }
2620
+ String quality = call.getString("quality");
2621
+ if (quality == null) {
2622
+ call.reject("quality is required");
2623
+ return;
2624
+ }
2625
+ try {
2626
+ cameraXView.setVideoQualitySetting(quality);
2627
+ call.resolve();
2628
+ } catch (IllegalArgumentException e) {
2629
+ call.reject(e.getMessage());
2630
+ } catch (Exception e) {
2631
+ call.reject("Failed to set video quality: " + e.getMessage());
2632
+ }
2633
+ }
2634
+
2635
+ @PluginMethod
2636
+ public void getVideoQuality(PluginCall call) {
2637
+ if (cameraXView == null || !cameraXView.isRunning()) {
2638
+ call.reject("Camera is not running");
2639
+ return;
2640
+ }
2641
+ JSObject ret = new JSObject();
2642
+ ret.put("quality", cameraXView.getVideoQualitySetting());
2643
+ call.resolve(ret);
2644
+ }
2645
+
2646
+ @PluginMethod
2647
+ public void getSupportedVideoQualities(PluginCall call) {
2648
+ List<String> qualities = cameraXView != null
2649
+ ? cameraXView.getSupportedVideoQualities()
2650
+ : Arrays.asList("low", "medium", "high", "2160p", "1080p", "720p", "480p", "4:3");
2651
+ JSONArray arr = new JSONArray();
2652
+ for (String quality : qualities) {
2653
+ arr.put(quality);
2654
+ }
2655
+ JSObject ret = new JSObject();
2656
+ ret.put("qualities", arr);
2657
+ call.resolve(ret);
2658
+ }
2659
+
2660
+ @PluginMethod
2661
+ public void setVideoCodec(PluginCall call) {
2662
+ if (cameraXView == null || !cameraXView.isRunning()) {
2663
+ call.reject("Camera is not running");
2664
+ return;
2665
+ }
2666
+ String codec = call.getString("codec");
2667
+ if (codec == null) {
2668
+ call.reject("codec is required");
2669
+ return;
2670
+ }
2671
+ try {
2672
+ cameraXView.setVideoCodecSetting(codec);
2673
+ call.resolve();
2674
+ } catch (IllegalArgumentException e) {
2675
+ call.reject(e.getMessage());
2676
+ } catch (Exception e) {
2677
+ call.reject("Failed to set video codec: " + e.getMessage());
2678
+ }
2679
+ }
2680
+
2681
+ @PluginMethod
2682
+ public void getVideoCodec(PluginCall call) {
2683
+ if (cameraXView == null || !cameraXView.isRunning()) {
2684
+ call.reject("Camera is not running");
2685
+ return;
2686
+ }
2687
+ JSObject ret = new JSObject();
2688
+ ret.put("codec", cameraXView.getVideoCodecSetting());
2689
+ call.resolve(ret);
2690
+ }
2691
+
2692
+ @PluginMethod
2693
+ public void getSupportedVideoCodecs(PluginCall call) {
2694
+ List<String> codecs = cameraXView != null ? cameraXView.getSupportedVideoCodecs() : Arrays.asList("avc1");
2695
+ JSONArray arr = new JSONArray();
2696
+ for (String codec : codecs) {
2697
+ arr.put(codec);
2698
+ }
2699
+ JSObject ret = new JSObject();
2700
+ ret.put("codecs", arr);
2701
+ call.resolve(ret);
2702
+ }
2703
+
2605
2704
  @PluginMethod
2606
2705
  public void startRecordVideo(PluginCall call) {
2607
2706
  if (cameraXView == null || !cameraXView.isRunning()) {
@@ -2617,7 +2716,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2617
2716
 
2618
2717
  if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
2619
2718
  try {
2620
- cameraXView.startRecordVideo();
2719
+ applyVideoCodecFromCall(call);
2720
+ cameraXView.startRecordVideo(getMaxDurationMillis(call), getMaxFileSize(call));
2621
2721
  call.resolve();
2622
2722
  } catch (Exception e) {
2623
2723
  call.reject("Failed to start video recording: " + e.getMessage());
@@ -2640,11 +2740,12 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2640
2740
  cameraXView.stopRecordVideo(
2641
2741
  new CameraXView.VideoRecordingCallback() {
2642
2742
  @Override
2643
- public void onSuccess(String filePath) {
2743
+ public void onSuccess(String filePath, String reason) {
2644
2744
  PluginCall saved = bridge.getSavedCall(cbId);
2645
2745
  if (saved != null) {
2646
2746
  JSObject result = new JSObject();
2647
2747
  result.put("videoFilePath", filePath);
2748
+ result.put("reason", reason);
2648
2749
  saved.resolve(result);
2649
2750
  bridge.releaseCall(saved);
2650
2751
  }
@@ -2665,6 +2766,32 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2665
2766
  }
2666
2767
  }
2667
2768
 
2769
+ private void applyVideoCodecFromCall(PluginCall call) {
2770
+ String codec = call.getString("videoCodec");
2771
+ if (codec != null && cameraXView != null) {
2772
+ cameraXView.setVideoCodecSetting(codec);
2773
+ }
2774
+ }
2775
+
2776
+ private Long getMaxDurationMillis(PluginCall call) {
2777
+ if (!call.getData().has("maxDuration") || call.getData().isNull("maxDuration")) {
2778
+ return null;
2779
+ }
2780
+ double seconds = call.getData().optDouble("maxDuration", 0D);
2781
+ if (seconds <= 0D) {
2782
+ return null;
2783
+ }
2784
+ return Math.max(1L, Math.round(seconds * 1000D));
2785
+ }
2786
+
2787
+ private Long getMaxFileSize(PluginCall call) {
2788
+ if (!call.getData().has("maxFileSize") || call.getData().isNull("maxFileSize")) {
2789
+ return null;
2790
+ }
2791
+ long bytes = call.getData().optLong("maxFileSize", 0L);
2792
+ return bytes > 0L ? bytes : null;
2793
+ }
2794
+
2668
2795
  @PermissionCallback
2669
2796
  private void handleVideoRecordingPermissionResult(PluginCall call) {
2670
2797
  // Use the persisted session value to determine which permission we requested
@@ -2676,7 +2803,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2676
2803
  PermissionState.GRANTED.equals(getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS))
2677
2804
  ) {
2678
2805
  try {
2679
- cameraXView.startRecordVideo();
2806
+ applyVideoCodecFromCall(call);
2807
+ cameraXView.startRecordVideo(getMaxDurationMillis(call), getMaxFileSize(call));
2680
2808
  call.resolve();
2681
2809
  } catch (Exception e) {
2682
2810
  call.reject("Failed to start video recording: " + e.getMessage());
@@ -23,6 +23,8 @@ import android.hardware.camera2.CameraManager;
23
23
  import android.hardware.camera2.CaptureRequest;
24
24
  import android.location.Location;
25
25
  import android.media.Image;
26
+ import android.media.MediaCodecList;
27
+ import android.media.MediaFormat;
26
28
  import android.media.MediaScannerConnection;
27
29
  import android.os.Build;
28
30
  import android.os.Environment;
@@ -128,6 +130,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
128
130
  void onSampleTakenError(String message);
129
131
  void onBarcodesScanned(JSONArray barcodes);
130
132
  void onBarcodeScanError(String message);
133
+ void onVideoRecordingFinished(String filePath, String reason);
131
134
  void onCameraStarted(int width, int height, int x, int y);
132
135
  void onCameraStartError(CameraXView source, String message);
133
136
  void onCameraStopped(CameraXView source);
@@ -139,7 +142,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
139
142
  }
140
143
 
141
144
  public interface VideoRecordingCallback {
142
- void onSuccess(String filePath);
145
+ void onSuccess(String filePath, String reason);
143
146
  void onError(String message);
144
147
  }
145
148
 
@@ -1093,24 +1096,36 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1093
1096
  // Get quality from sessionConfig default to high if null
1094
1097
  String videoQuality = sessionConfig.getVideoQuality() != null ? sessionConfig.getVideoQuality() : "high";
1095
1098
 
1096
- switch (videoQuality.toLowerCase()) {
1097
- case "low":
1098
- // Target SD, allow falling back to lower if needed
1099
+ switch (videoQuality.toLowerCase(Locale.US)) {
1100
+ case "2160p":
1099
1101
  qualitySelector = QualitySelector.fromOrderedList(
1100
- Arrays.asList(Quality.SD, Quality.LOWEST),
1101
- FallbackStrategy.lowerQualityOrHigherThan(Quality.SD)
1102
+ Arrays.asList(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
1103
+ FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
1102
1104
  );
1103
1105
  break;
1106
+ case "1080p":
1107
+ qualitySelector = QualitySelector.fromOrderedList(
1108
+ Arrays.asList(Quality.FHD, Quality.HD, Quality.SD),
1109
+ FallbackStrategy.higherQualityOrLowerThan(Quality.FHD)
1110
+ );
1111
+ break;
1112
+ case "720p":
1104
1113
  case "medium":
1105
- // Target HD, allow falling back to SD
1106
1114
  qualitySelector = QualitySelector.fromOrderedList(
1107
1115
  Arrays.asList(Quality.HD, Quality.SD),
1108
1116
  FallbackStrategy.lowerQualityOrHigherThan(Quality.HD)
1109
1117
  );
1110
1118
  break;
1119
+ case "480p":
1120
+ case "low":
1121
+ qualitySelector = QualitySelector.fromOrderedList(
1122
+ Arrays.asList(Quality.SD, Quality.LOWEST),
1123
+ FallbackStrategy.lowerQualityOrHigherThan(Quality.SD)
1124
+ );
1125
+ break;
1111
1126
  case "high":
1127
+ case "4:3":
1112
1128
  default:
1113
- // Target FHD allow falling back SD
1114
1129
  qualitySelector = QualitySelector.fromOrderedList(
1115
1130
  Arrays.asList(Quality.FHD, Quality.HD, Quality.SD),
1116
1131
  FallbackStrategy.higherQualityOrLowerThan(Quality.FHD)
@@ -1118,7 +1133,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1118
1133
  break;
1119
1134
  }
1120
1135
 
1121
- Recorder recorder = new Recorder.Builder().setQualitySelector(qualitySelector).build();
1136
+ Recorder.Builder recorderBuilder = new Recorder.Builder().setQualitySelector(qualitySelector);
1137
+ String videoCodec = sessionConfig.getVideoCodec() != null ? sessionConfig.getVideoCodec() : "avc1";
1138
+ if ("hvc1".equalsIgnoreCase(videoCodec)) {
1139
+ recorderBuilder.setVideoCapabilitiesSource(Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES);
1140
+ }
1141
+ Recorder recorder = recorderBuilder.build();
1122
1142
  videoCapture = VideoCapture.withOutput(recorder);
1123
1143
  }
1124
1144
 
@@ -4644,7 +4664,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
4644
4664
  }
4645
4665
 
4646
4666
  /** @noinspection ResultOfMethodCallIgnored*/
4647
- public void startRecordVideo() throws Exception {
4667
+ public void startRecordVideo(Long maxDurationMillis, Long maxFileSize) throws Exception {
4648
4668
  if (videoCapture == null) {
4649
4669
  throw new Exception("VideoCapture is not initialized");
4650
4670
  }
@@ -4666,7 +4686,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
4666
4686
  }
4667
4687
  currentVideoFile = new File(outputDir, fileName);
4668
4688
 
4669
- FileOutputOptions outputOptions = new FileOutputOptions.Builder(currentVideoFile).build();
4689
+ FileOutputOptions.Builder outputOptionsBuilder = new FileOutputOptions.Builder(currentVideoFile);
4690
+ if (maxDurationMillis != null && maxDurationMillis > 0) {
4691
+ outputOptionsBuilder.setDurationLimitMillis(maxDurationMillis);
4692
+ }
4693
+ if (maxFileSize != null && maxFileSize > 0) {
4694
+ outputOptionsBuilder.setFileSizeLimit(maxFileSize);
4695
+ }
4696
+ FileOutputOptions outputOptions = outputOptionsBuilder.build();
4670
4697
 
4671
4698
  // Create recording event listener
4672
4699
  androidx.core.util.Consumer<VideoRecordEvent> videoRecordEventListener = (videoRecordEvent) -> {
@@ -4712,11 +4739,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
4712
4739
  }
4713
4740
 
4714
4741
  private void handleRecordingFinalized(VideoRecordEvent.Finalize finalizeEvent) {
4715
- if (!finalizeEvent.hasError()) {
4716
- Log.d(TAG, "Video recording completed successfully");
4742
+ String reason = getRecordingFinishReason(finalizeEvent.getError());
4743
+ boolean hasVideoFile = currentVideoFile != null && currentVideoFile.exists();
4744
+ boolean finishedWithUsableFile = !finalizeEvent.hasError() || isRecordingLimitReached(finalizeEvent.getError());
4745
+
4746
+ if (finishedWithUsableFile && hasVideoFile) {
4747
+ String filePath = "file://" + currentVideoFile.getAbsolutePath();
4748
+ Log.d(TAG, "Video recording completed: " + reason);
4717
4749
  if (currentVideoCallback != null) {
4718
- String filePath = "file://" + currentVideoFile.getAbsolutePath();
4719
- currentVideoCallback.onSuccess(filePath);
4750
+ currentVideoCallback.onSuccess(filePath, reason);
4751
+ }
4752
+ if (listener != null) {
4753
+ listener.onVideoRecordingFinished(filePath, reason);
4720
4754
  }
4721
4755
  } else {
4722
4756
  Log.e(TAG, "Video recording failed: " + finalizeEvent.getError());
@@ -4730,4 +4764,104 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
4730
4764
  currentVideoFile = null;
4731
4765
  currentVideoCallback = null;
4732
4766
  }
4767
+
4768
+ private boolean isRecordingLimitReached(int error) {
4769
+ return (
4770
+ error == VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED ||
4771
+ error == VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED
4772
+ );
4773
+ }
4774
+
4775
+ private String getRecordingFinishReason(int error) {
4776
+ if (error == VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED) {
4777
+ return "maxDuration";
4778
+ }
4779
+ if (error == VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED) {
4780
+ return "maxFileSize";
4781
+ }
4782
+ return "manual";
4783
+ }
4784
+
4785
+ private static final List<String> ALL_VIDEO_QUALITIES = Arrays.asList("low", "medium", "high", "2160p", "1080p", "720p", "480p", "4:3");
4786
+
4787
+ private static final List<String> ALL_VIDEO_CODECS = Arrays.asList("avc1", "hvc1");
4788
+
4789
+ public String getVideoQualitySetting() {
4790
+ if (sessionConfig == null) {
4791
+ return "high";
4792
+ }
4793
+ return sessionConfig.getVideoQuality();
4794
+ }
4795
+
4796
+ public void setVideoQualitySetting(String quality) {
4797
+ if (sessionConfig == null) {
4798
+ throw new IllegalStateException("Camera session is not running");
4799
+ }
4800
+ String normalized = quality != null ? quality.toLowerCase(Locale.US) : "high";
4801
+ if (!ALL_VIDEO_QUALITIES.contains(normalized)) {
4802
+ throw new IllegalArgumentException("Unsupported video quality: " + quality);
4803
+ }
4804
+ if (normalized.equals(sessionConfig.getVideoQuality())) {
4805
+ return;
4806
+ }
4807
+ sessionConfig.setVideoQuality(normalized);
4808
+ if (sessionConfig.isVideoModeEnabled() && isRunning) {
4809
+ mainExecutor.execute(this::bindCameraUseCases);
4810
+ }
4811
+ }
4812
+
4813
+ public List<String> getSupportedVideoQualities() {
4814
+ return new ArrayList<>(ALL_VIDEO_QUALITIES);
4815
+ }
4816
+
4817
+ public String getVideoCodecSetting() {
4818
+ if (sessionConfig == null) {
4819
+ return "avc1";
4820
+ }
4821
+ return sessionConfig.getVideoCodec();
4822
+ }
4823
+
4824
+ public void setVideoCodecSetting(String codec) {
4825
+ if (sessionConfig == null) {
4826
+ throw new IllegalStateException("Camera session is not running");
4827
+ }
4828
+ String normalized = codec != null ? codec.toLowerCase(Locale.US) : "avc1";
4829
+ if (!ALL_VIDEO_CODECS.contains(normalized)) {
4830
+ throw new IllegalArgumentException("Unsupported video codec: " + codec);
4831
+ }
4832
+ if (normalized.equals(sessionConfig.getVideoCodec())) {
4833
+ return;
4834
+ }
4835
+ sessionConfig.setVideoCodec(normalized);
4836
+ if (sessionConfig.isVideoModeEnabled() && isRunning) {
4837
+ mainExecutor.execute(this::bindCameraUseCases);
4838
+ }
4839
+ }
4840
+
4841
+ public List<String> getSupportedVideoCodecs() {
4842
+ List<String> codecs = new ArrayList<>();
4843
+ codecs.add("avc1");
4844
+ if (isVideoCodecSupported(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
4845
+ codecs.add("hvc1");
4846
+ }
4847
+ return codecs;
4848
+ }
4849
+
4850
+ private boolean isVideoCodecSupported(String mimeType) {
4851
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
4852
+ return false;
4853
+ }
4854
+ MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
4855
+ for (android.media.MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
4856
+ if (!codecInfo.isEncoder()) {
4857
+ continue;
4858
+ }
4859
+ for (String type : codecInfo.getSupportedTypes()) {
4860
+ if (mimeType.equalsIgnoreCase(type)) {
4861
+ return true;
4862
+ }
4863
+ }
4864
+ }
4865
+ return false;
4866
+ }
4733
4867
  }