capacitor-camera-view 2.2.0 → 2.3.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 +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 +309 -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 +157 -16
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +157 -16
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +157 -16
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +28 -0
- 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
|
@@ -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,21 @@ 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
|
+
|
|
111
|
+
// Enabled CameraX use cases before video recording temporarily changes them.
|
|
112
|
+
private var enabledUseCasesBeforeRecording: Int? = null
|
|
113
|
+
|
|
84
114
|
// Plugin context
|
|
85
115
|
private var lifecycleOwner: LifecycleOwner? = null
|
|
86
116
|
private var pluginDelegate: Plugin = plugin
|
|
@@ -130,6 +160,15 @@ class CameraView(plugin: Plugin) {
|
|
|
130
160
|
/** Stop the camera session and release resources. */
|
|
131
161
|
suspend fun stopSessionAsync(): CameraResult<Unit> = withContext(Dispatchers.Main) {
|
|
132
162
|
try {
|
|
163
|
+
// Stop any active recording before unbinding
|
|
164
|
+
activeRecording?.stop()
|
|
165
|
+
activeRecording = null
|
|
166
|
+
pendingStopCallback?.invoke(
|
|
167
|
+
CameraResult.Error(Exception("Recording was interrupted because the camera session stopped"))
|
|
168
|
+
)
|
|
169
|
+
pendingStopCallback = null
|
|
170
|
+
currentRecordingFile = null
|
|
171
|
+
|
|
133
172
|
cameraController?.unbind()
|
|
134
173
|
|
|
135
174
|
previewView?.let { view ->
|
|
@@ -242,11 +281,15 @@ class CameraView(plugin: Plugin) {
|
|
|
242
281
|
"Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
|
|
243
282
|
)
|
|
244
283
|
try {
|
|
245
|
-
val base64String =
|
|
284
|
+
val base64String =
|
|
285
|
+
imageProxyToBase64(image, quality, imageRotationDegrees)
|
|
246
286
|
val result = JSObject().apply {
|
|
247
287
|
put("photo", base64String)
|
|
248
288
|
}
|
|
249
|
-
Log.d(
|
|
289
|
+
Log.d(
|
|
290
|
+
TAG,
|
|
291
|
+
"Image processed to Base64 in ${System.currentTimeMillis() - startTime}ms"
|
|
292
|
+
)
|
|
250
293
|
continuation.resume(CameraResult.Success(result))
|
|
251
294
|
} catch (e: Exception) {
|
|
252
295
|
Log.e(TAG, "Error processing captured image", e)
|
|
@@ -346,6 +389,263 @@ class CameraView(plugin: Plugin) {
|
|
|
346
389
|
}
|
|
347
390
|
}
|
|
348
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Starts video recording to a temporary file.
|
|
394
|
+
*/
|
|
395
|
+
suspend fun startRecordingAsync(
|
|
396
|
+
enableAudio: Boolean,
|
|
397
|
+
videoQuality: VideoRecordingQuality,
|
|
398
|
+
): CameraResult<Unit> = suspendCancellableCoroutine { continuation ->
|
|
399
|
+
mainHandler.post {
|
|
400
|
+
startRecordingOnMainThread(enableAudio, videoQuality, continuation)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private fun startRecordingOnMainThread(
|
|
405
|
+
enableAudio: Boolean,
|
|
406
|
+
videoQuality: VideoRecordingQuality,
|
|
407
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
408
|
+
) {
|
|
409
|
+
val controller = validateRecordingPreconditions(continuation) ?: return
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
controller.videoCaptureQualitySelector = videoQuality.toQualitySelector()
|
|
413
|
+
|
|
414
|
+
// Enable VIDEO_CAPTURE use case alongside IMAGE_CAPTURE
|
|
415
|
+
enabledUseCasesBeforeRecording = controller.currentEnabledUseCases()
|
|
416
|
+
controller.setEnabledUseCases(
|
|
417
|
+
CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
val outputOptions = createRecordingOutputOptions()
|
|
421
|
+
val audioConfig = resolveAudioConfig(enableAudio, continuation) ?: return
|
|
422
|
+
|
|
423
|
+
startCameraRecording(controller, outputOptions, audioConfig, continuation)
|
|
424
|
+
} catch (e: SecurityException) {
|
|
425
|
+
Log.e(TAG, "Security exception when starting recording. Missing permission?", e)
|
|
426
|
+
restoreUseCasesAfterRecording()
|
|
427
|
+
continuation.resume(CameraResult.Error(e))
|
|
428
|
+
} catch (e: Exception) {
|
|
429
|
+
Log.e(TAG, "Error starting recording", e)
|
|
430
|
+
restoreUseCasesAfterRecording()
|
|
431
|
+
continuation.resume(CameraResult.Error(e))
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private fun validateRecordingPreconditions(
|
|
436
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
437
|
+
): LifecycleCameraController? {
|
|
438
|
+
val controller = cameraController
|
|
439
|
+
if (controller == null) {
|
|
440
|
+
continuation.resume(CameraResult.Error(CameraError.CameraNotInitialized()))
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (activeRecording != null) {
|
|
445
|
+
continuation.resume(CameraResult.Error(Exception("Recording is already in progress")))
|
|
446
|
+
return null
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return controller
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private fun createRecordingOutputOptions(): FileOutputOptions {
|
|
453
|
+
val tempFile = File.createTempFile(
|
|
454
|
+
"camera_recording_",
|
|
455
|
+
".mp4",
|
|
456
|
+
context.cacheDir
|
|
457
|
+
)
|
|
458
|
+
currentRecordingFile = tempFile
|
|
459
|
+
return FileOutputOptions.Builder(tempFile).build()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private fun resolveAudioConfig(
|
|
463
|
+
enableAudio: Boolean,
|
|
464
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
465
|
+
): AudioConfig? {
|
|
466
|
+
if (!enableAudio) {
|
|
467
|
+
return AudioConfig.AUDIO_DISABLED
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (hasMicrophonePermission()) {
|
|
471
|
+
return try {
|
|
472
|
+
AudioConfig.create(true)
|
|
473
|
+
} catch (e: SecurityException) {
|
|
474
|
+
continuation.resume(CameraResult.Error(e))
|
|
475
|
+
null
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
continuation.resume(
|
|
480
|
+
CameraResult.Error(
|
|
481
|
+
SecurityException("Microphone permission is required for audio recording")
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
return null
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private fun hasMicrophonePermission(): Boolean {
|
|
488
|
+
return ContextCompat.checkSelfPermission(
|
|
489
|
+
context,
|
|
490
|
+
Manifest.permission.RECORD_AUDIO
|
|
491
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private fun startCameraRecording(
|
|
495
|
+
controller: LifecycleCameraController,
|
|
496
|
+
outputOptions: FileOutputOptions,
|
|
497
|
+
audioConfig: AudioConfig,
|
|
498
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
499
|
+
) {
|
|
500
|
+
val startResumed = AtomicBoolean(false)
|
|
501
|
+
activeRecording = controller.startRecording(
|
|
502
|
+
outputOptions,
|
|
503
|
+
audioConfig,
|
|
504
|
+
cameraExecutor
|
|
505
|
+
) { event ->
|
|
506
|
+
when (event) {
|
|
507
|
+
is VideoRecordEvent.Start -> handleRecordingStartEvent(startResumed, continuation)
|
|
508
|
+
is VideoRecordEvent.Finalize -> {
|
|
509
|
+
handleRecordingFinalizeEvent(event, startResumed, continuation)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
else -> Unit
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private fun handleRecordingStartEvent(
|
|
518
|
+
startResumed: AtomicBoolean,
|
|
519
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
520
|
+
) {
|
|
521
|
+
Log.d(TAG, "Video recording started")
|
|
522
|
+
if (continuation.isActive && startResumed.compareAndSet(false, true)) {
|
|
523
|
+
continuation.resume(CameraResult.Success(Unit))
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private fun handleRecordingFinalizeEvent(
|
|
528
|
+
event: VideoRecordEvent.Finalize,
|
|
529
|
+
startResumed: AtomicBoolean,
|
|
530
|
+
continuation: CancellableContinuation<CameraResult<Unit>>
|
|
531
|
+
) {
|
|
532
|
+
// If recording finalized before Start was emitted, resume the
|
|
533
|
+
// startRecording continuation with an error
|
|
534
|
+
if (continuation.isActive && startResumed.compareAndSet(false, true)) {
|
|
535
|
+
continuation.resume(
|
|
536
|
+
CameraResult.Error(
|
|
537
|
+
Exception("Recording failed to start: error code ${event.error}")
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
finalizeRecordingAndNotifyStopCallback(event)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private fun finalizeRecordingAndNotifyStopCallback(event: VideoRecordEvent.Finalize) {
|
|
546
|
+
mainHandler.post {
|
|
547
|
+
// CameraX requires use case changes on the main thread.
|
|
548
|
+
restoreUseCasesAfterRecording()
|
|
549
|
+
|
|
550
|
+
val callback = pendingStopCallback
|
|
551
|
+
pendingStopCallback = null
|
|
552
|
+
// Always clean up recording state
|
|
553
|
+
activeRecording = null
|
|
554
|
+
|
|
555
|
+
if (event.hasError()) {
|
|
556
|
+
Log.e(TAG, "Recording error: ${event.error}")
|
|
557
|
+
currentRecordingFile = null
|
|
558
|
+
callback?.invoke(CameraResult.Error(Exception("Recording failed with error code: ${event.error}")))
|
|
559
|
+
return@post
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
val file = currentRecordingFile
|
|
563
|
+
currentRecordingFile = null
|
|
564
|
+
if (file == null) {
|
|
565
|
+
callback?.invoke(CameraResult.Error(Exception("Recording file not found")))
|
|
566
|
+
return@post
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
val capacitorFilePath = FileUtils.getPortablePath(
|
|
570
|
+
context,
|
|
571
|
+
pluginDelegate.bridge.localUrl,
|
|
572
|
+
Uri.fromFile(file)
|
|
573
|
+
)
|
|
574
|
+
val result = JSObject().apply {
|
|
575
|
+
put("webPath", capacitorFilePath)
|
|
576
|
+
}
|
|
577
|
+
callback?.invoke(CameraResult.Success(result))
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private fun LifecycleCameraController.currentEnabledUseCases(): Int {
|
|
582
|
+
var enabledUseCases = 0
|
|
583
|
+
if (isImageCaptureEnabled) {
|
|
584
|
+
enabledUseCases = enabledUseCases or CameraController.IMAGE_CAPTURE
|
|
585
|
+
}
|
|
586
|
+
if (isImageAnalysisEnabled) {
|
|
587
|
+
enabledUseCases = enabledUseCases or CameraController.IMAGE_ANALYSIS
|
|
588
|
+
}
|
|
589
|
+
if (isVideoCaptureEnabled) {
|
|
590
|
+
enabledUseCases = enabledUseCases or CameraController.VIDEO_CAPTURE
|
|
591
|
+
}
|
|
592
|
+
return enabledUseCases
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private fun restoreUseCasesAfterRecording() {
|
|
596
|
+
val useCases = enabledUseCasesBeforeRecording ?: CameraController.IMAGE_CAPTURE
|
|
597
|
+
enabledUseCasesBeforeRecording = null
|
|
598
|
+
cameraController?.setEnabledUseCases(useCases)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private fun VideoRecordingQuality.toQualitySelector(): QualitySelector {
|
|
602
|
+
return when (this) {
|
|
603
|
+
VideoRecordingQuality.LOWEST -> QualitySelector.from(Quality.LOWEST)
|
|
604
|
+
VideoRecordingQuality.SD -> QualitySelector.from(
|
|
605
|
+
Quality.SD,
|
|
606
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.SD)
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
VideoRecordingQuality.HD -> QualitySelector.from(
|
|
610
|
+
Quality.HD,
|
|
611
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.HD)
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
VideoRecordingQuality.FHD -> QualitySelector.from(
|
|
615
|
+
Quality.FHD,
|
|
616
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.FHD)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
VideoRecordingQuality.UHD -> QualitySelector.from(
|
|
620
|
+
Quality.UHD,
|
|
621
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
VideoRecordingQuality.HIGHEST -> QualitySelector.from(Quality.HIGHEST)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Stops the current video recording and returns the file path.
|
|
630
|
+
*/
|
|
631
|
+
suspend fun stopRecordingAsync(): CameraResult<JSObject> =
|
|
632
|
+
suspendCancellableCoroutine { continuation ->
|
|
633
|
+
mainHandler.post {
|
|
634
|
+
val recording = activeRecording
|
|
635
|
+
if (recording == null) {
|
|
636
|
+
continuation.resume(CameraResult.Error(Exception("No recording is in progress")))
|
|
637
|
+
return@post
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
pendingStopCallback = { result ->
|
|
641
|
+
continuation.resume(result)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
activeRecording = null
|
|
645
|
+
recording.stop()
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
349
649
|
/** Flip between front and back cameras */
|
|
350
650
|
fun flipCamera(callback: (Exception?) -> Unit) {
|
|
351
651
|
currentCameraSelector = when (currentCameraSelector) {
|
|
@@ -533,6 +833,12 @@ class CameraView(plugin: Plugin) {
|
|
|
533
833
|
|
|
534
834
|
mainHandler.post {
|
|
535
835
|
try {
|
|
836
|
+
// Stop any active recording before cleanup
|
|
837
|
+
activeRecording?.stop()
|
|
838
|
+
activeRecording = null
|
|
839
|
+
pendingStopCallback = null
|
|
840
|
+
currentRecordingFile = null
|
|
841
|
+
|
|
536
842
|
// Stop camera session
|
|
537
843
|
cameraController?.unbind()
|
|
538
844
|
cameraController = null
|
|
@@ -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 = [
|
|
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(
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
}
|