@capgo/camera-preview 8.6.0 → 8.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -37
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +69 -16
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +222 -1
- package/dist/docs.json +102 -3
- package/dist/esm/definitions.d.ts +50 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +9 -0
- package/dist/esm/web.js +10 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +9 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +9 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +276 -38
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +74 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -279,6 +279,21 @@ In your `ios/App/App/Info.plist`, you must provide descriptions for the permissi
|
|
|
279
279
|
<string>To add location data to your photos</string>
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
+
- **Haptics during video recording** (`allowHapticsAndSystemSoundsDuringRecording: true`):
|
|
283
|
+
When recording video with audio enabled (`disableAudio: false`), iOS suppresses haptic feedback by default. Opt in on `startRecordVideo()` to allow haptics while recording, for example to warn users before a time limit expires:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { Haptics, ImpactStyle } from '@capacitor/haptics';
|
|
287
|
+
|
|
288
|
+
await CameraPreview.startRecordVideo({
|
|
289
|
+
disableAudio: false,
|
|
290
|
+
allowHapticsAndSystemSoundsDuringRecording: true,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Later, while recording:
|
|
294
|
+
await Haptics.impact({ style: ImpactStyle.Medium });
|
|
295
|
+
```
|
|
296
|
+
|
|
282
297
|
## Extra Android installation steps
|
|
283
298
|
|
|
284
299
|
**Important** `camera-preview` 3+ requires Gradle 7.
|
|
@@ -412,6 +427,9 @@ Documentation for the [uploader](https://github.com/Cap-go/capacitor-uploader)
|
|
|
412
427
|
* [`getExposureCompensationRange()`](#getexposurecompensationrange)
|
|
413
428
|
* [`getExposureCompensation()`](#getexposurecompensation)
|
|
414
429
|
* [`setExposureCompensation(...)`](#setexposurecompensation)
|
|
430
|
+
* [`getSupportedVideoFrameRates()`](#getsupportedvideoframerates)
|
|
431
|
+
* [`getVideoFrameRate()`](#getvideoframerate)
|
|
432
|
+
* [`setVideoFrameRate(...)`](#setvideoframerate)
|
|
415
433
|
* [`getPluginVersion()`](#getpluginversion)
|
|
416
434
|
* [Interfaces](#interfaces)
|
|
417
435
|
* [Type Aliases](#type-aliases)
|
|
@@ -759,9 +777,11 @@ startRecordVideo(options: CameraPreviewOptions) => Promise<void>
|
|
|
759
777
|
|
|
760
778
|
Starts recording a video.
|
|
761
779
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
|
780
|
+
Supports `frameRate`, `videoCodec`, `videoStabilizationMode`, `maxDuration`, `maxFileSize`, `disableAudio`, and `allowHapticsAndSystemSoundsDuringRecording` on each call.
|
|
781
|
+
|
|
782
|
+
| Param | Type | Description |
|
|
783
|
+
| ------------- | --------------------------------------------------------------------- | ---------------------------------- |
|
|
784
|
+
| **`options`** | <code><a href="#camerapreviewoptions">CameraPreviewOptions</a></code> | - The options for video recording. |
|
|
765
785
|
|
|
766
786
|
**Since:** 0.0.1
|
|
767
787
|
|
|
@@ -1359,6 +1379,57 @@ Sets the exposure compensation (EV bias). Value will be clamped to range.
|
|
|
1359
1379
|
--------------------
|
|
1360
1380
|
|
|
1361
1381
|
|
|
1382
|
+
### getSupportedVideoFrameRates()
|
|
1383
|
+
|
|
1384
|
+
```typescript
|
|
1385
|
+
getSupportedVideoFrameRates() => Promise<{ frameRates: number[]; }>
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
Lists the video frame rates supported by the active camera for the current format.
|
|
1389
|
+
Supported values depend on the selected camera, lens, and video quality.
|
|
1390
|
+
|
|
1391
|
+
**Returns:** <code>Promise<{ frameRates: number[]; }></code>
|
|
1392
|
+
|
|
1393
|
+
**Since:** 8.6.0
|
|
1394
|
+
|
|
1395
|
+
--------------------
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
### getVideoFrameRate()
|
|
1399
|
+
|
|
1400
|
+
```typescript
|
|
1401
|
+
getVideoFrameRate() => Promise<{ frameRate: number; }>
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
Returns the configured video frame rate for the active camera.
|
|
1405
|
+
On Android the actual recording frame rate can still vary in low light or under thermal pressure.
|
|
1406
|
+
|
|
1407
|
+
**Returns:** <code>Promise<{ frameRate: number; }></code>
|
|
1408
|
+
|
|
1409
|
+
**Since:** 8.6.0
|
|
1410
|
+
|
|
1411
|
+
--------------------
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
### setVideoFrameRate(...)
|
|
1415
|
+
|
|
1416
|
+
```typescript
|
|
1417
|
+
setVideoFrameRate(options: { frameRate: number; }) => Promise<void>
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
Sets the target video frame rate for the active camera session.
|
|
1421
|
+
Prefer passing `frameRate` to `startRecordVideo()` when starting a recording.
|
|
1422
|
+
Rejects unsupported values with a clear error.
|
|
1423
|
+
|
|
1424
|
+
| Param | Type |
|
|
1425
|
+
| ------------- | ----------------------------------- |
|
|
1426
|
+
| **`options`** | <code>{ frameRate: number; }</code> |
|
|
1427
|
+
|
|
1428
|
+
**Since:** 8.6.0
|
|
1429
|
+
|
|
1430
|
+
--------------------
|
|
1431
|
+
|
|
1432
|
+
|
|
1362
1433
|
### getPluginVersion()
|
|
1363
1434
|
|
|
1364
1435
|
```typescript
|
|
@@ -1379,40 +1450,42 @@ Get the native Capacitor plugin version
|
|
|
1379
1450
|
|
|
1380
1451
|
Defines the configuration options for starting the camera preview.
|
|
1381
1452
|
|
|
1382
|
-
| Prop
|
|
1383
|
-
|
|
|
1384
|
-
| **`parent`**
|
|
1385
|
-
| **`className`**
|
|
1386
|
-
| **`width`**
|
|
1387
|
-
| **`height`**
|
|
1388
|
-
| **`x`**
|
|
1389
|
-
| **`y`**
|
|
1390
|
-
| **`aspectRatio`**
|
|
1391
|
-
| **`aspectMode`**
|
|
1392
|
-
| **`gridMode`**
|
|
1393
|
-
| **`includeSafeAreaInsets`**
|
|
1394
|
-
| **`toBack`**
|
|
1395
|
-
| **`paddingBottom`**
|
|
1396
|
-
| **`rotateWhenOrientationChanged`**
|
|
1397
|
-
| **`position`**
|
|
1398
|
-
| **`storeToFile`**
|
|
1399
|
-
| **`disableExifHeaderStripping`**
|
|
1400
|
-
| **`disableAudio`**
|
|
1401
|
-
| **`lockAndroidOrientation`**
|
|
1402
|
-
| **`enableOpacity`**
|
|
1403
|
-
| **`disableFocusIndicator`**
|
|
1404
|
-
| **`deviceId`**
|
|
1405
|
-
| **`enablePhysicalDeviceSelection`**
|
|
1406
|
-
| **`initialZoomLevel`**
|
|
1407
|
-
| **`positioning`**
|
|
1408
|
-
| **`enableVideoMode`**
|
|
1409
|
-
| **`force`**
|
|
1410
|
-
| **`videoQuality`**
|
|
1411
|
-
| **`maxDuration`**
|
|
1412
|
-
| **`maxFileSize`**
|
|
1413
|
-
| **`videoCodec`**
|
|
1414
|
-
| **`
|
|
1415
|
-
| **`
|
|
1453
|
+
| Prop | Type | Description | Default | Since |
|
|
1454
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------ |
|
|
1455
|
+
| **`parent`** | <code>string</code> | The parent element to attach the video preview to. | | |
|
|
1456
|
+
| **`className`** | <code>string</code> | A CSS class name to add to the preview element. | | |
|
|
1457
|
+
| **`width`** | <code>number</code> | The width of the preview in pixels. Defaults to the screen width. | | |
|
|
1458
|
+
| **`height`** | <code>number</code> | The height of the preview in pixels. Defaults to the screen height. | | |
|
|
1459
|
+
| **`x`** | <code>number</code> | The horizontal origin of the preview, in pixels. | | |
|
|
1460
|
+
| **`y`** | <code>number</code> | The vertical origin of the preview, in pixels. | | |
|
|
1461
|
+
| **`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 |
|
|
1462
|
+
| **`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> | |
|
|
1463
|
+
| **`gridMode`** | <code><a href="#gridmode">GridMode</a></code> | The grid overlay to display on the camera preview. | <code>"none"</code> | 2.1.0 |
|
|
1464
|
+
| **`includeSafeAreaInsets`** | <code>boolean</code> | Adjusts the y-position to account for safe areas (e.g., notches). | <code>false</code> | |
|
|
1465
|
+
| **`toBack`** | <code>boolean</code> | If true, places the preview behind the webview. | <code>true</code> | |
|
|
1466
|
+
| **`paddingBottom`** | <code>number</code> | Bottom padding for the preview, in pixels. | | |
|
|
1467
|
+
| **`rotateWhenOrientationChanged`** | <code>boolean</code> | Whether to rotate the preview when the device orientation changes. | <code>true</code> | |
|
|
1468
|
+
| **`position`** | <code>string</code> | The camera to use. | <code>"rear"</code> | |
|
|
1469
|
+
| **`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> | |
|
|
1470
|
+
| **`disableExifHeaderStripping`** | <code>boolean</code> | If true, prevents the plugin from rotating the image based on EXIF data. | <code>false</code> | |
|
|
1471
|
+
| **`disableAudio`** | <code>boolean</code> | If true, disables the audio stream, preventing audio permission requests. | <code>true</code> | |
|
|
1472
|
+
| **`lockAndroidOrientation`** | <code>boolean</code> | If true, locks the device orientation while the camera is active. | <code>false</code> | |
|
|
1473
|
+
| **`enableOpacity`** | <code>boolean</code> | If true, allows the camera preview's opacity to be changed. | <code>false</code> | |
|
|
1474
|
+
| **`disableFocusIndicator`** | <code>boolean</code> | If true, disables the visual focus indicator when tapping to focus. | <code>false</code> | |
|
|
1475
|
+
| **`deviceId`** | <code>string</code> | The `deviceId` of the camera to use. If provided, `position` is ignored. | | |
|
|
1476
|
+
| **`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> | |
|
|
1477
|
+
| **`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 |
|
|
1478
|
+
| **`positioning`** | <code><a href="#camerapositioning">CameraPositioning</a></code> | The vertical positioning of the camera preview. | <code>"center"</code> | 2.3.0 |
|
|
1479
|
+
| **`enableVideoMode`** | <code>boolean</code> | If true, enables video capture capabilities when the camera starts. | <code>false</code> | 7.11.0 |
|
|
1480
|
+
| **`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> | |
|
|
1481
|
+
| **`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> | |
|
|
1482
|
+
| **`maxDuration`** | <code>number</code> | Maximum recording duration in seconds. Recording stops automatically when reached. | | |
|
|
1483
|
+
| **`maxFileSize`** | <code>number</code> | Maximum recording file size in bytes. Recording stops automatically when reached. | | |
|
|
1484
|
+
| **`videoCodec`** | <code><a href="#videocodec">VideoCodec</a></code> | Preferred video codec for recording. | <code>"avc1"</code> | |
|
|
1485
|
+
| **`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 |
|
|
1486
|
+
| **`videoStabilizationMode`** | <code><a href="#videostabilizationmode">VideoStabilizationMode</a></code> | Preferred video stabilization mode for recording. | <code>"off"</code> | |
|
|
1487
|
+
| **`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 |
|
|
1488
|
+
| **`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
1489
|
|
|
1417
1490
|
|
|
1418
1491
|
#### BarcodeScannerOptions
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|