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.
@@ -12,6 +12,7 @@ import com.getcapacitor.annotation.CapacitorPlugin
12
12
  import com.getcapacitor.annotation.Permission
13
13
  import com.getcapacitor.annotation.PermissionCallback
14
14
  import com.michaelwolz.capacitorcameraview.model.BarcodeDetectionResult
15
+ import com.michaelwolz.capacitorcameraview.model.VideoRecordingQuality
15
16
  import kotlinx.coroutines.CoroutineScope
16
17
  import kotlinx.coroutines.Dispatchers
17
18
  import kotlinx.coroutines.Job
@@ -21,7 +22,10 @@ import kotlinx.coroutines.launch
21
22
 
22
23
  @CapacitorPlugin(
23
24
  name = "CameraView",
24
- permissions = [Permission(strings = [Manifest.permission.CAMERA], alias = "camera")]
25
+ permissions = [
26
+ Permission(strings = [Manifest.permission.CAMERA], alias = "camera"),
27
+ Permission(strings = [Manifest.permission.RECORD_AUDIO], alias = "microphone")
28
+ ]
25
29
  )
26
30
  class CameraViewPlugin : Plugin() {
27
31
  // Coroutine scope for async operations
@@ -144,7 +148,113 @@ class CameraViewPlugin : Plugin() {
144
148
  },
145
149
  onError = { error ->
146
150
  call.reject("Failed to capture frame: ${error.message}", error)
147
- Log.d(TAG, "captureSample failed after ${System.currentTimeMillis() - timeStart}ms")
151
+ Log.d(
152
+ TAG,
153
+ "captureSample failed after ${System.currentTimeMillis() - timeStart}ms"
154
+ )
155
+ }
156
+ )
157
+ }
158
+ }
159
+
160
+ @PluginMethod
161
+ override fun requestPermissions(call: PluginCall) {
162
+ val permissionsList = call.getArray("permissions")
163
+ ?.toList<String>()
164
+ ?: listOf("camera")
165
+
166
+ // Determine which aliases still need to be requested
167
+ val aliasesToRequest = permissionsList.filter { alias ->
168
+ getPermissionState(alias) != PermissionState.GRANTED
169
+ }
170
+
171
+ if (aliasesToRequest.isEmpty()) {
172
+ checkPermissions(call)
173
+ return
174
+ }
175
+
176
+ // Store which permissions to request so callback can continue the chain
177
+ call.data.put("_pendingAliases", com.getcapacitor.JSArray(aliasesToRequest))
178
+ requestPermissionForAlias(aliasesToRequest.first(), call, "requestedPermsCallback")
179
+ }
180
+
181
+ @PermissionCallback
182
+ private fun requestedPermsCallback(call: PluginCall) {
183
+ val pendingAliases = call.getArray("_pendingAliases")?.toList<String>() ?: emptyList()
184
+
185
+ // Find remaining aliases that still need requesting
186
+ val remaining = pendingAliases.drop(1).filter { alias ->
187
+ getPermissionState(alias) != PermissionState.GRANTED
188
+ }
189
+
190
+ if (remaining.isNotEmpty()) {
191
+ call.data.put("_pendingAliases", com.getcapacitor.JSArray(remaining))
192
+ requestPermissionForAlias(remaining.first(), call, "requestedPermsCallback")
193
+ } else {
194
+ checkPermissions(call)
195
+ }
196
+ }
197
+
198
+ @PluginMethod
199
+ fun startRecording(call: PluginCall) {
200
+ val enableAudio = call.getBoolean("enableAudio") ?: false
201
+ val videoQuality =
202
+ parseVideoRecordingQuality(call.getString("videoQuality"))
203
+ ?: run {
204
+ call.reject("Invalid videoQuality. Use one of: lowest, sd, hd, fhd, uhd, highest")
205
+ return
206
+ }
207
+
208
+ if (enableAudio && getPermissionState("microphone") != PermissionState.GRANTED) {
209
+ requestPermissionForAlias("microphone", call, "microphonePermsCallback")
210
+ return
211
+ }
212
+
213
+ doStartRecording(call, enableAudio, videoQuality)
214
+ }
215
+
216
+ @PermissionCallback
217
+ private fun microphonePermsCallback(call: PluginCall) {
218
+ if (getPermissionState("microphone") == PermissionState.GRANTED) {
219
+ val enableAudio = call.getBoolean("enableAudio") ?: false
220
+ val videoQuality =
221
+ parseVideoRecordingQuality(call.getString("videoQuality"))
222
+ ?: run {
223
+ call.reject("Invalid videoQuality. Use one of: lowest, sd, hd, fhd, uhd, highest")
224
+ return
225
+ }
226
+ doStartRecording(call, enableAudio, videoQuality)
227
+ } else {
228
+ call.reject("Microphone permission is required for audio recording")
229
+ }
230
+ }
231
+
232
+
233
+ /**
234
+ * Helper method to start recording after ensuring permissions are granted.
235
+ */
236
+ private fun doStartRecording(
237
+ call: PluginCall,
238
+ enableAudio: Boolean,
239
+ videoQuality: VideoRecordingQuality
240
+ ) {
241
+ pluginScope.launch {
242
+ implementation.startRecordingAsync(enableAudio, videoQuality).fold(
243
+ onSuccess = { call.resolve() },
244
+ onError = { error ->
245
+ call.reject("Failed to start recording: ${error.message}", error)
246
+ }
247
+ )
248
+ }
249
+ }
250
+
251
+ @PluginMethod
252
+ fun stopRecording(call: PluginCall) {
253
+ pluginScope.launch {
254
+ implementation.stopRecordingAsync().fold(
255
+ onSuccess = { result -> call.resolve(result) },
256
+ onError = { error ->
257
+ call.reject("Failed to stop recording: ${error.message}", error)
148
258
  }
149
259
  )
150
260
  }
@@ -0,0 +1,10 @@
1
+ package com.michaelwolz.capacitorcameraview.model
2
+
3
+ enum class VideoRecordingQuality {
4
+ LOWEST,
5
+ SD,
6
+ HD,
7
+ FHD,
8
+ UHD,
9
+ HIGHEST,
10
+ }
@@ -13,6 +13,7 @@ import androidx.camera.view.PreviewView
13
13
  import com.getcapacitor.PluginCall
14
14
  import com.google.mlkit.vision.barcode.common.Barcode
15
15
  import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
16
+ import com.michaelwolz.capacitorcameraview.model.VideoRecordingQuality
16
17
  import com.michaelwolz.capacitorcameraview.model.WebBoundingRect
17
18
  import java.io.ByteArrayOutputStream
18
19
 
@@ -171,7 +172,7 @@ fun sessionConfigFromPluginCall(call: PluginCall): CameraSessionConfiguration {
171
172
  jsonArray.optString(i)?.let { stringTypes.add(it) }
172
173
  }
173
174
  val converted = convertToNativeBarcodeFormats(stringTypes)
174
- if (converted.isNotEmpty()) converted else null
175
+ converted.ifEmpty { null }
175
176
  }
176
177
 
177
178
  return CameraSessionConfiguration(
@@ -250,4 +251,23 @@ fun imageProxyToBase64(image: ImageProxy, quality: Int, rotationDegrees: Int): S
250
251
  // Ensure bitmap is always recycled
251
252
  bitmap.recycle()
252
253
  }
254
+ }
255
+
256
+ /**
257
+ * Parses a string representation of video recording quality into a [VideoRecordingQuality] enum.
258
+ */
259
+ fun parseVideoRecordingQuality(rawValue: String?): VideoRecordingQuality? {
260
+ if (rawValue == null) {
261
+ return VideoRecordingQuality.HIGHEST
262
+ }
263
+
264
+ return when (rawValue) {
265
+ "lowest" -> VideoRecordingQuality.LOWEST
266
+ "sd" -> VideoRecordingQuality.SD
267
+ "hd" -> VideoRecordingQuality.HD
268
+ "fhd" -> VideoRecordingQuality.FHD
269
+ "uhd" -> VideoRecordingQuality.UHD
270
+ "highest" -> VideoRecordingQuality.HIGHEST
271
+ else -> null
272
+ }
253
273
  }
package/dist/docs.json CHANGED
@@ -147,6 +147,58 @@
147
147
  ],
148
148
  "slug": "capturesample"
149
149
  },
150
+ {
151
+ "name": "startRecording",
152
+ "signature": "(options?: VideoRecordingOptions | undefined) => Promise<void>",
153
+ "parameters": [
154
+ {
155
+ "name": "options",
156
+ "docs": "- Optional recording configuration",
157
+ "type": "VideoRecordingOptions | undefined"
158
+ }
159
+ ],
160
+ "returns": "Promise<void>",
161
+ "tags": [
162
+ {
163
+ "name": "param",
164
+ "text": "options - Optional recording configuration"
165
+ },
166
+ {
167
+ "name": "returns",
168
+ "text": "A promise that resolves when recording has started"
169
+ },
170
+ {
171
+ "name": "since",
172
+ "text": "2.2.0"
173
+ }
174
+ ],
175
+ "docs": "Start recording video from the current camera.\nCamera must be running. Throws if already recording.",
176
+ "complexTypes": [
177
+ "VideoRecordingOptions"
178
+ ],
179
+ "slug": "startrecording"
180
+ },
181
+ {
182
+ "name": "stopRecording",
183
+ "signature": "() => Promise<VideoRecordingResponse>",
184
+ "parameters": [],
185
+ "returns": "Promise<VideoRecordingResponse>",
186
+ "tags": [
187
+ {
188
+ "name": "returns",
189
+ "text": "A promise that resolves with the recorded video file path"
190
+ },
191
+ {
192
+ "name": "since",
193
+ "text": "2.2.0"
194
+ }
195
+ ],
196
+ "docs": "Stop the current video recording and return the result.\nThrows if no recording is in progress.",
197
+ "complexTypes": [
198
+ "VideoRecordingResponse"
199
+ ],
200
+ "slug": "stoprecording"
201
+ },
150
202
  {
151
203
  "name": "flipCamera",
152
204
  "signature": "() => Promise<void>",
@@ -429,14 +481,14 @@
429
481
  "tags": [
430
482
  {
431
483
  "name": "returns",
432
- "text": "A promise that resolves with an object containing the camera permission status"
484
+ "text": "A promise that resolves with an object containing the camera and microphone permission status"
433
485
  },
434
486
  {
435
487
  "name": "since",
436
488
  "text": "1.0.0"
437
489
  }
438
490
  ],
439
- "docs": "Check camera permission status without requesting permissions.",
491
+ "docs": "Check camera and microphone permission status without requesting permissions.",
440
492
  "complexTypes": [
441
493
  "PermissionStatus"
442
494
  ],
@@ -444,22 +496,33 @@
444
496
  },
445
497
  {
446
498
  "name": "requestPermissions",
447
- "signature": "() => Promise<PermissionStatus>",
448
- "parameters": [],
499
+ "signature": "(options?: { permissions?: CameraPermissionType[] | undefined; } | undefined) => Promise<PermissionStatus>",
500
+ "parameters": [
501
+ {
502
+ "name": "options",
503
+ "docs": "- Optional object specifying which permissions to request",
504
+ "type": "{ permissions?: CameraPermissionType[] | undefined; } | undefined"
505
+ }
506
+ ],
449
507
  "returns": "Promise<PermissionStatus>",
450
508
  "tags": [
509
+ {
510
+ "name": "param",
511
+ "text": "options - Optional object specifying which permissions to request"
512
+ },
451
513
  {
452
514
  "name": "returns",
453
- "text": "A promise that resolves with an object containing the camera permission status"
515
+ "text": "A promise that resolves with an object containing the camera and microphone permission status"
454
516
  },
455
517
  {
456
518
  "name": "since",
457
519
  "text": "1.0.0"
458
520
  }
459
521
  ],
460
- "docs": "Request camera permission from the user.",
522
+ "docs": "Request camera and/or microphone permissions from the user.\n\nBy default, only camera permission is requested. To also request microphone\npermission (needed for video recording with audio), pass `{ permissions: ['camera', 'microphone'] }`.",
461
523
  "complexTypes": [
462
- "PermissionStatus"
524
+ "PermissionStatus",
525
+ "CameraPermissionType"
463
526
  ],
464
527
  "slug": "requestpermissions"
465
528
  },
@@ -723,6 +786,80 @@
723
786
  }
724
787
  ]
725
788
  },
789
+ {
790
+ "name": "VideoRecordingOptions",
791
+ "slug": "videorecordingoptions",
792
+ "docs": "Configuration options for video recording.",
793
+ "tags": [
794
+ {
795
+ "text": "2.2.0",
796
+ "name": "since"
797
+ }
798
+ ],
799
+ "methods": [],
800
+ "properties": [
801
+ {
802
+ "name": "enableAudio",
803
+ "tags": [
804
+ {
805
+ "text": "false",
806
+ "name": "default"
807
+ },
808
+ {
809
+ "text": "2.2.0",
810
+ "name": "since"
811
+ }
812
+ ],
813
+ "docs": "Whether to record audio with the video.\nRequires microphone permission.",
814
+ "complexTypes": [],
815
+ "type": "boolean | undefined"
816
+ },
817
+ {
818
+ "name": "videoQuality",
819
+ "tags": [
820
+ {
821
+ "text": "'highest'",
822
+ "name": "default"
823
+ },
824
+ {
825
+ "text": "2.2.0",
826
+ "name": "since"
827
+ }
828
+ ],
829
+ "docs": "Video recording quality preset.\nNative platforms only (iOS/Android). Ignored on web.",
830
+ "complexTypes": [
831
+ "VideoRecordingQuality"
832
+ ],
833
+ "type": "VideoRecordingQuality"
834
+ }
835
+ ]
836
+ },
837
+ {
838
+ "name": "VideoRecordingResponse",
839
+ "slug": "videorecordingresponse",
840
+ "docs": "Response from stopping a video recording.",
841
+ "tags": [
842
+ {
843
+ "text": "2.2.0",
844
+ "name": "since"
845
+ }
846
+ ],
847
+ "methods": [],
848
+ "properties": [
849
+ {
850
+ "name": "webPath",
851
+ "tags": [
852
+ {
853
+ "text": "2.2.0",
854
+ "name": "since"
855
+ }
856
+ ],
857
+ "docs": "Web-accessible path to the recorded video file.\nOn web, this is a blob URL.\nOn iOS/Android, this is a path accessible via Capacitor's filesystem.",
858
+ "complexTypes": [],
859
+ "type": "string"
860
+ }
861
+ ]
862
+ },
726
863
  {
727
864
  "name": "GetAvailableDevicesResponse",
728
865
  "slug": "getavailabledevicesresponse",
@@ -925,7 +1062,7 @@
925
1062
  {
926
1063
  "name": "PermissionStatus",
927
1064
  "slug": "permissionstatus",
928
- "docs": "Response for the camera permission status.",
1065
+ "docs": "Response for the camera and microphone permission status.",
929
1066
  "tags": [
930
1067
  {
931
1068
  "text": "1.0.0",
@@ -942,6 +1079,15 @@
942
1079
  "PermissionState"
943
1080
  ],
944
1081
  "type": "PermissionState"
1082
+ },
1083
+ {
1084
+ "name": "microphone",
1085
+ "tags": [],
1086
+ "docs": "The state of the microphone permission",
1087
+ "complexTypes": [
1088
+ "PermissionState"
1089
+ ],
1090
+ "type": "PermissionState"
945
1091
  }
946
1092
  ]
947
1093
  },
@@ -1172,6 +1318,37 @@
1172
1318
  }
1173
1319
  ]
1174
1320
  },
1321
+ {
1322
+ "name": "VideoRecordingQuality",
1323
+ "slug": "videorecordingquality",
1324
+ "docs": "Video recording quality presets.",
1325
+ "types": [
1326
+ {
1327
+ "text": "'lowest'",
1328
+ "complexTypes": []
1329
+ },
1330
+ {
1331
+ "text": "'sd'",
1332
+ "complexTypes": []
1333
+ },
1334
+ {
1335
+ "text": "'hd'",
1336
+ "complexTypes": []
1337
+ },
1338
+ {
1339
+ "text": "'fhd'",
1340
+ "complexTypes": []
1341
+ },
1342
+ {
1343
+ "text": "'uhd'",
1344
+ "complexTypes": []
1345
+ },
1346
+ {
1347
+ "text": "'highest'",
1348
+ "complexTypes": []
1349
+ }
1350
+ ]
1351
+ },
1175
1352
  {
1176
1353
  "name": "FlashMode",
1177
1354
  "slug": "flashmode",
@@ -1213,6 +1390,21 @@
1213
1390
  "complexTypes": []
1214
1391
  }
1215
1392
  ]
1393
+ },
1394
+ {
1395
+ "name": "CameraPermissionType",
1396
+ "slug": "camerapermissiontype",
1397
+ "docs": "Permission types that can be requested.\n- 'camera': Camera access permission\n- 'microphone': Microphone access permission (needed for video recording with audio)",
1398
+ "types": [
1399
+ {
1400
+ "text": "'camera'",
1401
+ "complexTypes": []
1402
+ },
1403
+ {
1404
+ "text": "'microphone'",
1405
+ "complexTypes": []
1406
+ }
1407
+ ]
1216
1408
  }
1217
1409
  ],
1218
1410
  "pluginConfigs": []
@@ -56,6 +56,25 @@ export interface CameraViewPlugin {
56
56
  * @since 1.0.0
57
57
  */
58
58
  captureSample<T extends CaptureOptions>(options: T): Promise<CaptureResponse<T>>;
59
+ /**
60
+ * Start recording video from the current camera.
61
+ * Camera must be running. Throws if already recording.
62
+ *
63
+ * @param options - Optional recording configuration
64
+ * @returns A promise that resolves when recording has started
65
+ *
66
+ * @since 2.2.0
67
+ */
68
+ startRecording(options?: VideoRecordingOptions): Promise<void>;
69
+ /**
70
+ * Stop the current video recording and return the result.
71
+ * Throws if no recording is in progress.
72
+ *
73
+ * @returns A promise that resolves with the recorded video file path
74
+ *
75
+ * @since 2.2.0
76
+ */
77
+ stopRecording(): Promise<VideoRecordingResponse>;
59
78
  /**
60
79
  * Switch between front and back camera.
61
80
  *
@@ -177,21 +196,27 @@ export interface CameraViewPlugin {
177
196
  level?: number;
178
197
  }): Promise<void>;
179
198
  /**
180
- * Check camera permission status without requesting permissions.
199
+ * Check camera and microphone permission status without requesting permissions.
181
200
  *
182
- * @returns A promise that resolves with an object containing the camera permission status
201
+ * @returns A promise that resolves with an object containing the camera and microphone permission status
183
202
  *
184
203
  * @since 1.0.0
185
204
  */
186
205
  checkPermissions(): Promise<PermissionStatus>;
187
206
  /**
188
- * Request camera permission from the user.
207
+ * Request camera and/or microphone permissions from the user.
189
208
  *
190
- * @returns A promise that resolves with an object containing the camera permission status
209
+ * By default, only camera permission is requested. To also request microphone
210
+ * permission (needed for video recording with audio), pass `{ permissions: ['camera', 'microphone'] }`.
211
+ *
212
+ * @param options - Optional object specifying which permissions to request
213
+ * @returns A promise that resolves with an object containing the camera and microphone permission status
191
214
  *
192
215
  * @since 1.0.0
193
216
  */
194
- requestPermissions(): Promise<PermissionStatus>;
217
+ requestPermissions(options?: {
218
+ permissions?: CameraPermissionType[];
219
+ }): Promise<PermissionStatus>;
195
220
  /**
196
221
  * Listen for barcode detection events.
197
222
  * This event is emitted when a barcode is detected in the camera preview.
@@ -230,6 +255,16 @@ export type CameraPosition = 'front' | 'back';
230
255
  * @since 1.0.0
231
256
  */
232
257
  export type FlashMode = 'off' | 'on' | 'auto';
258
+ /**
259
+ * Video recording quality presets.
260
+ *
261
+ * @remarks
262
+ * On iOS this maps to `AVCaptureSession.Preset` values.
263
+ * On Android this maps to CameraX `QualitySelector` values.
264
+ *
265
+ * @since 2.2.0
266
+ */
267
+ export type VideoRecordingQuality = 'lowest' | 'sd' | 'hd' | 'fhd' | 'uhd' | 'highest';
233
268
  /**
234
269
  * Represents a physical camera device on the device.
235
270
  *
@@ -383,6 +418,39 @@ export interface CaptureOptions {
383
418
  */
384
419
  saveToFile?: boolean;
385
420
  }
421
+ /**
422
+ * Configuration options for video recording.
423
+ * @since 2.2.0
424
+ */
425
+ export interface VideoRecordingOptions {
426
+ /**
427
+ * Whether to record audio with the video.
428
+ * Requires microphone permission.
429
+ * @default false
430
+ * @since 2.2.0
431
+ */
432
+ enableAudio?: boolean;
433
+ /**
434
+ * Video recording quality preset.
435
+ * Native platforms only (iOS/Android). Ignored on web.
436
+ * @default 'highest'
437
+ * @since 2.2.0
438
+ */
439
+ videoQuality?: VideoRecordingQuality;
440
+ }
441
+ /**
442
+ * Response from stopping a video recording.
443
+ * @since 2.2.0
444
+ */
445
+ export interface VideoRecordingResponse {
446
+ /**
447
+ * Web-accessible path to the recorded video file.
448
+ * On web, this is a blob URL.
449
+ * On iOS/Android, this is a path accessible via Capacitor's filesystem.
450
+ * @since 2.2.0
451
+ */
452
+ webPath: string;
453
+ }
386
454
  /**
387
455
  * Response for checking if the camera view is running.
388
456
  *
@@ -497,11 +565,21 @@ export interface BoundingRect {
497
565
  height: number;
498
566
  }
499
567
  /**
500
- * Response for the camera permission status.
568
+ * Permission types that can be requested.
569
+ * - 'camera': Camera access permission
570
+ * - 'microphone': Microphone access permission (needed for video recording with audio)
571
+ *
572
+ * @since 2.2.0
573
+ */
574
+ export type CameraPermissionType = 'camera' | 'microphone';
575
+ /**
576
+ * Response for the camera and microphone permission status.
501
577
  *
502
578
  * @since 1.0.0
503
579
  */
504
580
  export interface PermissionStatus {
505
581
  /** The state of the camera permission */
506
582
  camera: PermissionState;
583
+ /** The state of the microphone permission */
584
+ microphone: PermissionState;
507
585
  }