capacitor-camera-view 2.1.0 → 2.2.0-rc.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 +196 -10
- package/android/build.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +287 -3
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +112 -2
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/VideoRecordingQuality.kt +10 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +21 -1
- package/dist/docs.json +200 -8
- package/dist/esm/definitions.d.ts +84 -6
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +20 -4
- package/dist/esm/web.js +151 -16
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +151 -16
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +151 -16
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +28 -0
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +9 -9
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +8 -8
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +13 -14
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +302 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +159 -150
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +114 -15
- package/ios/Sources/CameraViewPlugin/TempFileManager.swift +68 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
- 📹 Embed a **live camera feed** directly into your app.
|
|
31
31
|
- 📸 Capture photos or frames from the camera preview.
|
|
32
|
+
- 🎥 **Video recording** with optional audio support.
|
|
32
33
|
- 🔍 **Barcode detection** support.
|
|
33
34
|
- 📱 **Virtual device support** for automatic lens selection based on zoom level and focus (iOS only).
|
|
34
35
|
- 🔦 Control **zoom**, **flash** and **torch** modes programmatically.
|
|
@@ -62,12 +63,70 @@ Add the following keys to your app's `Info.plist` file:
|
|
|
62
63
|
<string>To capture photos and videos</string>
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
If you plan to use `startRecording` with `enableAudio: true`, also add:
|
|
67
|
+
|
|
68
|
+
```xml
|
|
69
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
70
|
+
<string>To record audio with video</string>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> [!IMPORTANT]
|
|
74
|
+
> The `NSMicrophoneUsageDescription` key must be present in `Info.plist` **before** microphone permission is ever requested — even if the request happens automatically when starting a recording with audio. Omitting it will cause your app to crash at runtime.
|
|
75
|
+
|
|
65
76
|
#### Android
|
|
66
77
|
|
|
67
|
-
The `CAMERA` permission is
|
|
78
|
+
The `CAMERA` permission is added to your app's `AndroidManifest.xml` automatically by the plugin. If you plan to use `startRecording` with `enableAudio: true`, the `RECORD_AUDIO` permission is also declared automatically by the plugin. You can verify these in your merged manifest:
|
|
68
79
|
|
|
69
80
|
```xml
|
|
70
81
|
<uses-permission android:name="android.permission.CAMERA" />
|
|
82
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> [!IMPORTANT]
|
|
86
|
+
> Declaring a permission in `AndroidManifest.xml` is required for the system to allow requesting it at runtime. The plugin handles this for you, but make sure you are not accidentally stripping the permission in your build configuration.
|
|
87
|
+
|
|
88
|
+
## 🔒 Permissions
|
|
89
|
+
|
|
90
|
+
The plugin handles permissions for you automatically when a feature that requires them is used. However, you can also request permissions explicitly in advance.
|
|
91
|
+
|
|
92
|
+
### Requesting permissions explicitly
|
|
93
|
+
|
|
94
|
+
By default, `requestPermissions()` only requests **camera** permission, preserving backward compatibility:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Request camera permission only (default behavior)
|
|
98
|
+
const status = await CameraView.requestPermissions();
|
|
99
|
+
console.log(status.camera); // 'granted' | 'denied' | 'prompt'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To also request **microphone** permission (needed for video recording with audio), pass the `permissions` option:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Request both camera and microphone permissions
|
|
106
|
+
const status = await CameraView.requestPermissions({
|
|
107
|
+
permissions: ['camera', 'microphone'],
|
|
108
|
+
});
|
|
109
|
+
console.log(status.camera); // 'granted' | 'denied' | 'prompt'
|
|
110
|
+
console.log(status.microphone); // 'granted' | 'denied' | 'prompt'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Automatic permission requests
|
|
114
|
+
|
|
115
|
+
You do not need to call `requestPermissions()` manually. The plugin will automatically request the required permissions when a feature is first used:
|
|
116
|
+
|
|
117
|
+
- **Camera** permission is requested automatically when `start()` is called.
|
|
118
|
+
- **Microphone** permission is requested automatically when `startRecording({ enableAudio: true })` is called.
|
|
119
|
+
|
|
120
|
+
Regardless of whether you request permissions manually or rely on the automatic flow, the corresponding entries **must** be declared in your app's platform configuration (`Info.plist` on iOS, `AndroidManifest.xml` on Android) as described above.
|
|
121
|
+
|
|
122
|
+
### Checking permission status
|
|
123
|
+
|
|
124
|
+
Use `checkPermissions()` to query the current permission state without triggering a system prompt:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const status = await CameraView.checkPermissions();
|
|
128
|
+
console.log(status.camera); // 'granted' | 'denied' | 'prompt'
|
|
129
|
+
console.log(status.microphone); // 'granted' | 'denied' | 'prompt'
|
|
71
130
|
```
|
|
72
131
|
|
|
73
132
|
## ▶️ Basic Usage
|
|
@@ -174,6 +233,54 @@ CameraView.addListener('barcodeDetected', (data) => {
|
|
|
174
233
|
|
|
175
234
|
See the [`BarcodeDetectionData`](#barcodedetectiondata) interface for details on the event payload.
|
|
176
235
|
|
|
236
|
+
## 🎥 Video Recording
|
|
237
|
+
|
|
238
|
+
This plugin supports recording video directly from the live camera feed.
|
|
239
|
+
|
|
240
|
+
**How it works:**
|
|
241
|
+
* **iOS:** Uses `AVCaptureMovieFileOutput` on top of the existing `AVCaptureSession`. Output is saved as `.mp4`.
|
|
242
|
+
* **Android:** Uses the CameraX `VideoCapture` use case via `LifecycleCameraController`. Output is saved as `.mp4`.
|
|
243
|
+
* **Web:** Uses the browser `MediaRecorder` API on the existing `MediaStream`. Output is a `.webm` blob URL (MP4 is not broadly supported by browsers).
|
|
244
|
+
|
|
245
|
+
**Basic usage:**
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { CameraView } from 'capacitor-camera-view';
|
|
249
|
+
|
|
250
|
+
// Start recording (camera must already be running)
|
|
251
|
+
await CameraView.startRecording({ enableAudio: false });
|
|
252
|
+
|
|
253
|
+
// Stop recording and get the result
|
|
254
|
+
const result = await CameraView.stopRecording();
|
|
255
|
+
console.log('Video saved to:', result.webPath);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Playing back the recorded video:**
|
|
259
|
+
|
|
260
|
+
```html
|
|
261
|
+
<video [src]="videoPath" controls></video>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Recording with audio:**
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
await CameraView.startRecording({ enableAudio: true });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Recording with explicit quality preset (native):**
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
await CameraView.startRecording({
|
|
274
|
+
enableAudio: true,
|
|
275
|
+
videoQuality: 'fhd', // lowest | sd | hd | fhd | uhd | highest
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
> [!NOTE]
|
|
280
|
+
> When `enableAudio: true` is used, the plugin automatically requests microphone permission from the user if it has not been granted yet. The permission declaration must still be present in your platform configuration — see the [Permissions](#-permissions) section for details.
|
|
281
|
+
|
|
282
|
+
See the [`VideoRecordingOptions`](#videorecordingoptions) and [`VideoRecordingResponse`](#videorecordingresponse) interfaces in the API section for the full set of options.
|
|
283
|
+
|
|
177
284
|
## 🧪 Example App
|
|
178
285
|
|
|
179
286
|
To see the plugin in action, check out the example app in the `example-app` folder. The app demonstrates how to integrate and use the Capacitor Camera View plugin in an Ionic Angular project.
|
|
@@ -206,6 +313,8 @@ chore: update dependencies
|
|
|
206
313
|
* [`isRunning()`](#isrunning)
|
|
207
314
|
* [`capture(...)`](#capture)
|
|
208
315
|
* [`captureSample(...)`](#capturesample)
|
|
316
|
+
* [`startRecording(...)`](#startrecording)
|
|
317
|
+
* [`stopRecording()`](#stoprecording)
|
|
209
318
|
* [`flipCamera()`](#flipcamera)
|
|
210
319
|
* [`getAvailableDevices()`](#getavailabledevices)
|
|
211
320
|
* [`getZoom()`](#getzoom)
|
|
@@ -217,7 +326,7 @@ chore: update dependencies
|
|
|
217
326
|
* [`getTorchMode()`](#gettorchmode)
|
|
218
327
|
* [`setTorchMode(...)`](#settorchmode)
|
|
219
328
|
* [`checkPermissions()`](#checkpermissions)
|
|
220
|
-
* [`requestPermissions()`](#requestpermissions)
|
|
329
|
+
* [`requestPermissions(...)`](#requestpermissions)
|
|
221
330
|
* [`addListener('barcodeDetected', ...)`](#addlistenerbarcodedetected-)
|
|
222
331
|
* [`removeAllListeners(...)`](#removealllisteners)
|
|
223
332
|
* [Interfaces](#interfaces)
|
|
@@ -321,6 +430,40 @@ not yet well supported on the web.
|
|
|
321
430
|
--------------------
|
|
322
431
|
|
|
323
432
|
|
|
433
|
+
### startRecording(...)
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
startRecording(options?: VideoRecordingOptions | undefined) => Promise<void>
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Start recording video from the current camera.
|
|
440
|
+
Camera must be running. Throws if already recording.
|
|
441
|
+
|
|
442
|
+
| Param | Type | Description |
|
|
443
|
+
| ------------- | ----------------------------------------------------------------------- | ---------------------------------- |
|
|
444
|
+
| **`options`** | <code><a href="#videorecordingoptions">VideoRecordingOptions</a></code> | - Optional recording configuration |
|
|
445
|
+
|
|
446
|
+
**Since:** 2.2.0
|
|
447
|
+
|
|
448
|
+
--------------------
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
### stopRecording()
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
stopRecording() => Promise<VideoRecordingResponse>
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Stop the current video recording and return the result.
|
|
458
|
+
Throws if no recording is in progress.
|
|
459
|
+
|
|
460
|
+
**Returns:** <code>Promise<<a href="#videorecordingresponse">VideoRecordingResponse</a>></code>
|
|
461
|
+
|
|
462
|
+
**Since:** 2.2.0
|
|
463
|
+
|
|
464
|
+
--------------------
|
|
465
|
+
|
|
466
|
+
|
|
324
467
|
### flipCamera()
|
|
325
468
|
|
|
326
469
|
```typescript
|
|
@@ -481,7 +624,7 @@ Set the torch (flashlight) mode and intensity.
|
|
|
481
624
|
checkPermissions() => Promise<PermissionStatus>
|
|
482
625
|
```
|
|
483
626
|
|
|
484
|
-
Check camera permission status without requesting permissions.
|
|
627
|
+
Check camera and microphone permission status without requesting permissions.
|
|
485
628
|
|
|
486
629
|
**Returns:** <code>Promise<<a href="#permissionstatus">PermissionStatus</a>></code>
|
|
487
630
|
|
|
@@ -490,13 +633,20 @@ Check camera permission status without requesting permissions.
|
|
|
490
633
|
--------------------
|
|
491
634
|
|
|
492
635
|
|
|
493
|
-
### requestPermissions()
|
|
636
|
+
### requestPermissions(...)
|
|
494
637
|
|
|
495
638
|
```typescript
|
|
496
|
-
requestPermissions() => Promise<PermissionStatus>
|
|
639
|
+
requestPermissions(options?: { permissions?: CameraPermissionType[] | undefined; } | undefined) => Promise<PermissionStatus>
|
|
497
640
|
```
|
|
498
641
|
|
|
499
|
-
Request camera
|
|
642
|
+
Request camera and/or microphone permissions from the user.
|
|
643
|
+
|
|
644
|
+
By default, only camera permission is requested. To also request microphone
|
|
645
|
+
permission (needed for video recording with audio), pass `{ permissions: ['camera', 'microphone'] }`.
|
|
646
|
+
|
|
647
|
+
| Param | Type | Description |
|
|
648
|
+
| ------------- | ------------------------------------------------------ | --------------------------------------------------------- |
|
|
649
|
+
| **`options`** | <code>{ permissions?: CameraPermissionType[]; }</code> | - Optional object specifying which permissions to request |
|
|
500
650
|
|
|
501
651
|
**Returns:** <code>Promise<<a href="#permissionstatus">PermissionStatus</a>></code>
|
|
502
652
|
|
|
@@ -581,6 +731,25 @@ Configuration options for capturing photos and samples.
|
|
|
581
731
|
| **`saveToFile`** | <code>boolean</code> | If true, saves to a temporary file and returns the web path instead of base64. The web path can be used to set the src attribute of an image for efficient loading and rendering. This reduces the data that needs to be transferred over the bridge, which can improve performance especially for high-resolution images. | <code>false</code> | 1.1.0 |
|
|
582
732
|
|
|
583
733
|
|
|
734
|
+
#### VideoRecordingOptions
|
|
735
|
+
|
|
736
|
+
Configuration options for video recording.
|
|
737
|
+
|
|
738
|
+
| Prop | Type | Description | Default | Since |
|
|
739
|
+
| ------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------- | ----- |
|
|
740
|
+
| **`enableAudio`** | <code>boolean</code> | Whether to record audio with the video. Requires microphone permission. | <code>false</code> | 2.2.0 |
|
|
741
|
+
| **`videoQuality`** | <code><a href="#videorecordingquality">VideoRecordingQuality</a></code> | Video recording quality preset. Native platforms only (iOS/Android). Ignored on web. | <code>'highest'</code> | 2.2.0 |
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
#### VideoRecordingResponse
|
|
745
|
+
|
|
746
|
+
Response from stopping a video recording.
|
|
747
|
+
|
|
748
|
+
| Prop | Type | Description | Since |
|
|
749
|
+
| ------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
750
|
+
| **`webPath`** | <code>string</code> | Web-accessible path to the recorded video file. On web, this is a blob URL. On iOS/Android, this is a path accessible via Capacitor's filesystem. | 2.2.0 |
|
|
751
|
+
|
|
752
|
+
|
|
584
753
|
#### GetAvailableDevicesResponse
|
|
585
754
|
|
|
586
755
|
Response for getting available camera devices.
|
|
@@ -652,11 +821,12 @@ Response for getting the current torch mode.
|
|
|
652
821
|
|
|
653
822
|
#### PermissionStatus
|
|
654
823
|
|
|
655
|
-
Response for the camera permission status.
|
|
824
|
+
Response for the camera and microphone permission status.
|
|
656
825
|
|
|
657
|
-
| Prop
|
|
658
|
-
|
|
|
659
|
-
| **`camera`**
|
|
826
|
+
| Prop | Type | Description |
|
|
827
|
+
| ---------------- | ----------------------------------------------------------- | -------------------------------------- |
|
|
828
|
+
| **`camera`** | <code><a href="#permissionstate">PermissionState</a></code> | The state of the camera permission |
|
|
829
|
+
| **`microphone`** | <code><a href="#permissionstate">PermissionState</a></code> | The state of the microphone permission |
|
|
660
830
|
|
|
661
831
|
|
|
662
832
|
#### PluginListenerHandle
|
|
@@ -729,6 +899,13 @@ depending on the `saveToFile` option in the <a href="#captureoptions">CaptureOpt
|
|
|
729
899
|
<code>T['saveToFile'] extends true ? { /** The web path to the captured photo that can be used to set the src attribute of an image for efficient loading and rendering (when saveToFile is true) */ webPath: string; } : { /** The base64 encoded string of the captured photo (when saveToFile is false or undefined) */ photo: string; }</code>
|
|
730
900
|
|
|
731
901
|
|
|
902
|
+
#### VideoRecordingQuality
|
|
903
|
+
|
|
904
|
+
Video recording quality presets.
|
|
905
|
+
|
|
906
|
+
<code>'lowest' | 'sd' | 'hd' | 'fhd' | 'uhd' | 'highest'</code>
|
|
907
|
+
|
|
908
|
+
|
|
732
909
|
#### FlashMode
|
|
733
910
|
|
|
734
911
|
Flash mode options for the camera.
|
|
@@ -743,4 +920,13 @@ Flash mode options for the camera.
|
|
|
743
920
|
|
|
744
921
|
<code>'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'</code>
|
|
745
922
|
|
|
923
|
+
|
|
924
|
+
#### CameraPermissionType
|
|
925
|
+
|
|
926
|
+
Permission types that can be requested.
|
|
927
|
+
- 'camera': Camera access permission
|
|
928
|
+
- 'microphone': Microphone access permission (needed for video recording with audio)
|
|
929
|
+
|
|
930
|
+
<code>'camera' | 'microphone'</code>
|
|
931
|
+
|
|
746
932
|
</docgen-api>
|
package/android/build.gradle
CHANGED
|
@@ -78,6 +78,7 @@ dependencies {
|
|
|
78
78
|
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
|
79
79
|
implementation "androidx.camera:camera-view:${camerax_version}"
|
|
80
80
|
implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
|
|
81
|
+
implementation "androidx.camera:camera-video:${camerax_version}"
|
|
81
82
|
|
|
82
83
|
// ML Kit for barcode scanning
|
|
83
84
|
implementation "com.google.mlkit:barcode-scanning:17.3.0"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
package com.michaelwolz.capacitorcameraview
|
|
2
2
|
|
|
3
|
+
import android.Manifest
|
|
3
4
|
import android.content.Context
|
|
4
5
|
import android.content.Context.CAMERA_SERVICE
|
|
6
|
+
import android.content.pm.PackageManager
|
|
5
7
|
import android.graphics.Bitmap
|
|
6
8
|
import android.hardware.camera2.CameraCharacteristics
|
|
7
9
|
import android.hardware.camera2.CameraManager
|
|
@@ -23,8 +25,16 @@ import androidx.camera.core.TorchState
|
|
|
23
25
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
|
24
26
|
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
25
27
|
import androidx.camera.mlkit.vision.MlKitAnalyzer
|
|
28
|
+
import androidx.camera.video.FallbackStrategy
|
|
29
|
+
import androidx.camera.video.FileOutputOptions
|
|
30
|
+
import androidx.camera.video.Quality
|
|
31
|
+
import androidx.camera.video.QualitySelector
|
|
32
|
+
import androidx.camera.video.Recording
|
|
33
|
+
import androidx.camera.video.VideoRecordEvent
|
|
34
|
+
import androidx.camera.view.CameraController
|
|
26
35
|
import androidx.camera.view.LifecycleCameraController
|
|
27
36
|
import androidx.camera.view.PreviewView
|
|
37
|
+
import androidx.camera.view.video.AudioConfig
|
|
28
38
|
import androidx.core.content.ContextCompat
|
|
29
39
|
import androidx.lifecycle.LifecycleOwner
|
|
30
40
|
import com.getcapacitor.FileUtils
|
|
@@ -38,7 +48,9 @@ import com.michaelwolz.capacitorcameraview.model.BarcodeDetectionResult
|
|
|
38
48
|
import com.michaelwolz.capacitorcameraview.model.CameraDevice
|
|
39
49
|
import com.michaelwolz.capacitorcameraview.model.CameraResult
|
|
40
50
|
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
|
|
51
|
+
import com.michaelwolz.capacitorcameraview.model.VideoRecordingQuality
|
|
41
52
|
import com.michaelwolz.capacitorcameraview.model.ZoomFactors
|
|
53
|
+
import kotlinx.coroutines.CancellableContinuation
|
|
42
54
|
import kotlinx.coroutines.CoroutineScope
|
|
43
55
|
import kotlinx.coroutines.Dispatchers
|
|
44
56
|
import kotlinx.coroutines.SupervisorJob
|
|
@@ -55,6 +67,7 @@ import java.io.File
|
|
|
55
67
|
import java.io.FileOutputStream
|
|
56
68
|
import java.util.concurrent.ExecutorService
|
|
57
69
|
import java.util.concurrent.Executors
|
|
70
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
58
71
|
import java.util.concurrent.atomic.AtomicLong
|
|
59
72
|
import java.util.concurrent.atomic.AtomicReference
|
|
60
73
|
import kotlin.coroutines.resume
|
|
@@ -72,7 +85,9 @@ class CameraView(plugin: Plugin) {
|
|
|
72
85
|
// Camera components (using atomic reference for thread safety)
|
|
73
86
|
private var cameraController: LifecycleCameraController?
|
|
74
87
|
get() = cameraControllerRef.get()
|
|
75
|
-
set(value) {
|
|
88
|
+
set(value) {
|
|
89
|
+
cameraControllerRef.set(value)
|
|
90
|
+
}
|
|
76
91
|
|
|
77
92
|
private val cameraExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() }
|
|
78
93
|
private var previewView: PreviewView? = null
|
|
@@ -81,6 +96,18 @@ class CameraView(plugin: Plugin) {
|
|
|
81
96
|
private var currentCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
82
97
|
private var currentFlashMode: Int = ImageCapture.FLASH_MODE_OFF
|
|
83
98
|
|
|
99
|
+
// Active video recording
|
|
100
|
+
private var activeRecording: Recording? = null
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Holds the pending stop-recording continuation result handler.
|
|
104
|
+
* Needed because CameraX delivers the final recording outcome asynchronously via Finalize.
|
|
105
|
+
*/
|
|
106
|
+
private var pendingStopCallback: ((CameraResult<JSObject>) -> Unit)? = null
|
|
107
|
+
|
|
108
|
+
// Track the output file for the current recording
|
|
109
|
+
private var currentRecordingFile: File? = null
|
|
110
|
+
|
|
84
111
|
// Plugin context
|
|
85
112
|
private var lifecycleOwner: LifecycleOwner? = null
|
|
86
113
|
private var pluginDelegate: Plugin = plugin
|
|
@@ -130,6 +157,15 @@ class CameraView(plugin: Plugin) {
|
|
|
130
157
|
/** Stop the camera session and release resources. */
|
|
131
158
|
suspend fun stopSessionAsync(): CameraResult<Unit> = withContext(Dispatchers.Main) {
|
|
132
159
|
try {
|
|
160
|
+
// Stop any active recording before unbinding
|
|
161
|
+
activeRecording?.stop()
|
|
162
|
+
activeRecording = null
|
|
163
|
+
pendingStopCallback?.invoke(
|
|
164
|
+
CameraResult.Error(Exception("Recording was interrupted because the camera session stopped"))
|
|
165
|
+
)
|
|
166
|
+
pendingStopCallback = null
|
|
167
|
+
currentRecordingFile = null
|
|
168
|
+
|
|
133
169
|
cameraController?.unbind()
|
|
134
170
|
|
|
135
171
|
previewView?.let { view ->
|
|
@@ -242,11 +278,15 @@ class CameraView(plugin: Plugin) {
|
|
|
242
278
|
"Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
|
|
243
279
|
)
|
|
244
280
|
try {
|
|
245
|
-
val base64String =
|
|
281
|
+
val base64String =
|
|
282
|
+
imageProxyToBase64(image, quality, imageRotationDegrees)
|
|
246
283
|
val result = JSObject().apply {
|
|
247
284
|
put("photo", base64String)
|
|
248
285
|
}
|
|
249
|
-
Log.d(
|
|
286
|
+
Log.d(
|
|
287
|
+
TAG,
|
|
288
|
+
"Image processed to Base64 in ${System.currentTimeMillis() - startTime}ms"
|
|
289
|
+
)
|
|
250
290
|
continuation.resume(CameraResult.Success(result))
|
|
251
291
|
} catch (e: Exception) {
|
|
252
292
|
Log.e(TAG, "Error processing captured image", e)
|
|
@@ -346,6 +386,244 @@ class CameraView(plugin: Plugin) {
|
|
|
346
386
|
}
|
|
347
387
|
}
|
|
348
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Starts video recording to a temporary file.
|
|
391
|
+
*/
|
|
392
|
+
suspend fun startRecordingAsync(
|
|
393
|
+
enableAudio: Boolean,
|
|
394
|
+
videoQuality: VideoRecordingQuality,
|
|
395
|
+
): CameraResult<Unit> = suspendCancellableCoroutine { continuation ->
|
|
396
|
+
mainHandler.post {
|
|
397
|
+
startRecordingOnMainThread(enableAudio, videoQuality, continuation)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private fun startRecordingOnMainThread(
|
|
402
|
+
enableAudio: Boolean,
|
|
403
|
+
videoQuality: VideoRecordingQuality,
|
|
404
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
405
|
+
) {
|
|
406
|
+
val controller = validateRecordingPreconditions(continuation) ?: return
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
controller.videoCaptureQualitySelector = videoQuality.toQualitySelector()
|
|
410
|
+
|
|
411
|
+
// Enable VIDEO_CAPTURE use case alongside IMAGE_CAPTURE
|
|
412
|
+
controller.setEnabledUseCases(
|
|
413
|
+
CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
val outputOptions = createRecordingOutputOptions()
|
|
417
|
+
val audioConfig = resolveAudioConfig(enableAudio, continuation) ?: return
|
|
418
|
+
|
|
419
|
+
startCameraRecording(controller, outputOptions, audioConfig, continuation)
|
|
420
|
+
} catch (e: SecurityException) {
|
|
421
|
+
Log.e(TAG, "Security exception when starting recording. Missing permission?", e)
|
|
422
|
+
// Restore normal use cases on permission error
|
|
423
|
+
cameraController?.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
|
|
424
|
+
continuation.resume(CameraResult.Error(e))
|
|
425
|
+
} catch (e: Exception) {
|
|
426
|
+
Log.e(TAG, "Error starting recording", e)
|
|
427
|
+
// Restore normal use cases on error
|
|
428
|
+
cameraController?.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
|
|
429
|
+
continuation.resume(CameraResult.Error(e))
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private fun validateRecordingPreconditions(
|
|
434
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
435
|
+
): LifecycleCameraController? {
|
|
436
|
+
val controller = cameraController
|
|
437
|
+
if (controller == null) {
|
|
438
|
+
continuation.resume(CameraResult.Error(CameraError.CameraNotInitialized()))
|
|
439
|
+
return null
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (activeRecording != null) {
|
|
443
|
+
continuation.resume(CameraResult.Error(Exception("Recording is already in progress")))
|
|
444
|
+
return null
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return controller
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private fun createRecordingOutputOptions(): FileOutputOptions {
|
|
451
|
+
val tempFile = File.createTempFile(
|
|
452
|
+
"camera_recording_",
|
|
453
|
+
".mp4",
|
|
454
|
+
context.cacheDir
|
|
455
|
+
)
|
|
456
|
+
currentRecordingFile = tempFile
|
|
457
|
+
return FileOutputOptions.Builder(tempFile).build()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private fun resolveAudioConfig(
|
|
461
|
+
enableAudio: Boolean,
|
|
462
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
463
|
+
): AudioConfig? {
|
|
464
|
+
if (!enableAudio) {
|
|
465
|
+
return AudioConfig.AUDIO_DISABLED
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (hasMicrophonePermission()) {
|
|
469
|
+
return try {
|
|
470
|
+
AudioConfig.create(true)
|
|
471
|
+
} catch (e: SecurityException) {
|
|
472
|
+
continuation.resume(CameraResult.Error(e))
|
|
473
|
+
null
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
continuation.resume(
|
|
478
|
+
CameraResult.Error(
|
|
479
|
+
SecurityException("Microphone permission is required for audio recording")
|
|
480
|
+
)
|
|
481
|
+
)
|
|
482
|
+
return null
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private fun hasMicrophonePermission(): Boolean {
|
|
486
|
+
return ContextCompat.checkSelfPermission(
|
|
487
|
+
context,
|
|
488
|
+
Manifest.permission.RECORD_AUDIO
|
|
489
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private fun startCameraRecording(
|
|
493
|
+
controller: LifecycleCameraController,
|
|
494
|
+
outputOptions: FileOutputOptions,
|
|
495
|
+
audioConfig: AudioConfig,
|
|
496
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
497
|
+
) {
|
|
498
|
+
val startResumed = AtomicBoolean(false)
|
|
499
|
+
activeRecording = controller.startRecording(
|
|
500
|
+
outputOptions,
|
|
501
|
+
audioConfig,
|
|
502
|
+
cameraExecutor
|
|
503
|
+
) { event ->
|
|
504
|
+
when (event) {
|
|
505
|
+
is VideoRecordEvent.Start -> handleRecordingStartEvent(startResumed, continuation)
|
|
506
|
+
is VideoRecordEvent.Finalize -> {
|
|
507
|
+
handleRecordingFinalizeEvent(event, startResumed, continuation)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
else -> Unit
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private fun handleRecordingStartEvent(
|
|
516
|
+
startResumed: AtomicBoolean,
|
|
517
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
518
|
+
) {
|
|
519
|
+
Log.d(TAG, "Video recording started")
|
|
520
|
+
if (continuation.isActive && startResumed.compareAndSet(false, true)) {
|
|
521
|
+
continuation.resume(CameraResult.Success(Unit))
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private fun handleRecordingFinalizeEvent(
|
|
526
|
+
event: VideoRecordEvent.Finalize,
|
|
527
|
+
startResumed: AtomicBoolean,
|
|
528
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
529
|
+
) {
|
|
530
|
+
// If recording finalized before Start was emitted, resume the
|
|
531
|
+
// startRecording continuation with an error
|
|
532
|
+
if (continuation.isActive && startResumed.compareAndSet(false, true)) {
|
|
533
|
+
continuation.resume(
|
|
534
|
+
CameraResult.Error(
|
|
535
|
+
Exception("Recording failed to start: error code ${event.error}")
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
finalizeRecordingAndNotifyStopCallback(event)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private fun finalizeRecordingAndNotifyStopCallback(event: VideoRecordEvent.Finalize) {
|
|
544
|
+
mainHandler.post {
|
|
545
|
+
// CameraX requires use case changes on the main thread.
|
|
546
|
+
cameraController?.setEnabledUseCases(CameraController.IMAGE_CAPTURE)
|
|
547
|
+
|
|
548
|
+
val callback = pendingStopCallback
|
|
549
|
+
pendingStopCallback = null
|
|
550
|
+
// Always clean up recording state
|
|
551
|
+
activeRecording = null
|
|
552
|
+
|
|
553
|
+
if (event.hasError()) {
|
|
554
|
+
Log.e(TAG, "Recording error: ${event.error}")
|
|
555
|
+
currentRecordingFile = null
|
|
556
|
+
callback?.invoke(CameraResult.Error(Exception("Recording failed with error code: ${event.error}")))
|
|
557
|
+
return@post
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
val file = currentRecordingFile
|
|
561
|
+
currentRecordingFile = null
|
|
562
|
+
if (file == null) {
|
|
563
|
+
callback?.invoke(CameraResult.Error(Exception("Recording file not found")))
|
|
564
|
+
return@post
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
val capacitorFilePath = FileUtils.getPortablePath(
|
|
568
|
+
context,
|
|
569
|
+
pluginDelegate.bridge.localUrl,
|
|
570
|
+
Uri.fromFile(file)
|
|
571
|
+
)
|
|
572
|
+
val result = JSObject().apply {
|
|
573
|
+
put("webPath", capacitorFilePath)
|
|
574
|
+
}
|
|
575
|
+
callback?.invoke(CameraResult.Success(result))
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private fun VideoRecordingQuality.toQualitySelector(): QualitySelector {
|
|
580
|
+
return when (this) {
|
|
581
|
+
VideoRecordingQuality.LOWEST -> QualitySelector.from(Quality.LOWEST)
|
|
582
|
+
VideoRecordingQuality.SD -> QualitySelector.from(
|
|
583
|
+
Quality.SD,
|
|
584
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.SD)
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
VideoRecordingQuality.HD -> QualitySelector.from(
|
|
588
|
+
Quality.HD,
|
|
589
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.HD)
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
VideoRecordingQuality.FHD -> QualitySelector.from(
|
|
593
|
+
Quality.FHD,
|
|
594
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.FHD)
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
VideoRecordingQuality.UHD -> QualitySelector.from(
|
|
598
|
+
Quality.UHD,
|
|
599
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
VideoRecordingQuality.HIGHEST -> QualitySelector.from(Quality.HIGHEST)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Stops the current video recording and returns the file path.
|
|
608
|
+
*/
|
|
609
|
+
suspend fun stopRecordingAsync(): CameraResult<JSObject> =
|
|
610
|
+
suspendCancellableCoroutine { continuation ->
|
|
611
|
+
mainHandler.post {
|
|
612
|
+
val recording = activeRecording
|
|
613
|
+
if (recording == null) {
|
|
614
|
+
continuation.resume(CameraResult.Error(Exception("No recording is in progress")))
|
|
615
|
+
return@post
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
pendingStopCallback = { result ->
|
|
619
|
+
continuation.resume(result)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
activeRecording = null
|
|
623
|
+
recording.stop()
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
349
627
|
/** Flip between front and back cameras */
|
|
350
628
|
fun flipCamera(callback: (Exception?) -> Unit) {
|
|
351
629
|
currentCameraSelector = when (currentCameraSelector) {
|
|
@@ -533,6 +811,12 @@ class CameraView(plugin: Plugin) {
|
|
|
533
811
|
|
|
534
812
|
mainHandler.post {
|
|
535
813
|
try {
|
|
814
|
+
// Stop any active recording before cleanup
|
|
815
|
+
activeRecording?.stop()
|
|
816
|
+
activeRecording = null
|
|
817
|
+
pendingStopCallback = null
|
|
818
|
+
currentRecordingFile = null
|
|
819
|
+
|
|
536
820
|
// Stop camera session
|
|
537
821
|
cameraController?.unbind()
|
|
538
822
|
cameraController = null
|