capacitor-camera-view 2.0.1 → 2.1.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.
Files changed (28) hide show
  1. package/README.md +19 -9
  2. package/android/build.gradle +8 -5
  3. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +217 -126
  4. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +70 -30
  5. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
  6. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
  7. package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +94 -5
  8. package/dist/docs.json +81 -0
  9. package/dist/esm/definitions.d.ts +44 -0
  10. package/dist/esm/definitions.js.map +1 -1
  11. package/dist/esm/web.d.ts +7 -1
  12. package/dist/esm/web.js +67 -2
  13. package/dist/esm/web.js.map +1 -1
  14. package/dist/plugin.cjs.js +68 -2
  15. package/dist/plugin.cjs.js.map +1 -1
  16. package/dist/plugin.js +68 -2
  17. package/dist/plugin.js.map +1 -1
  18. package/ios/Sources/CameraViewPlugin/CameraError.swift +97 -2
  19. package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
  20. package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +29 -2
  21. package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
  22. package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +45 -13
  23. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
  24. package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +193 -59
  25. package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +83 -84
  26. package/ios/Sources/CameraViewPlugin/TempFileManager.swift +181 -0
  27. package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
  28. package/package.json +17 -17
package/README.md CHANGED
@@ -550,15 +550,16 @@ Remove all listeners for this plugin.
550
550
 
551
551
  Configuration options for starting a camera session.
552
552
 
553
- | Prop | Type | Description | Default |
554
- | -------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
555
- | **`enableBarcodeDetection`** | <code>boolean</code> | Enables the barcode detection functionality | <code>false</code> |
556
- | **`position`** | <code><a href="#cameraposition">CameraPosition</a></code> | Position of the camera to use | <code>'back'</code> |
557
- | **`deviceId`** | <code>string</code> | Specific device ID of the camera to use If provided, takes precedence over position | |
558
- | **`useTripleCameraIfAvailable`** | <code>boolean</code> | Whether to use the triple camera if available (iPhone Pro models only) | <code>false</code> |
559
- | **`preferredCameraDeviceTypes`** | <code>CameraDeviceType[]</code> | Ordered list of preferred camera device types to use (iOS only). The system will attempt to use the first available camera type in the list. If position is also provided, the system will use the first available camera type that matches the position and is in the list. This will fallback to the default camera type if none of the preferred types are available. | <code>undefined - system will decide based on position/deviceId</code> |
560
- | **`zoomFactor`** | <code>number</code> | The initial zoom factor to use | <code>1.0</code> |
561
- | **`containerElementId`** | <code>string</code> | Optional HTML ID of the container element where the camera view should be rendered. If not provided, the camera view will be appended to the document body. Web only. | |
553
+ | Prop | Type | Description | Default | Since |
554
+ | -------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ----- |
555
+ | **`enableBarcodeDetection`** | <code>boolean</code> | Enables the barcode detection functionality | <code>false</code> | |
556
+ | **`barcodeTypes`** | <code>BarcodeType[]</code> | Specific barcode types to detect. If not provided, all supported types are detected. Specifying only the types you need can significantly improve performance and reduce battery consumption, especially on mobile devices. | <code>undefined - all supported types are detected</code> | 2.1.0 |
557
+ | **`position`** | <code><a href="#cameraposition">CameraPosition</a></code> | Position of the camera to use | <code>'back'</code> | |
558
+ | **`deviceId`** | <code>string</code> | Specific device ID of the camera to use If provided, takes precedence over position | | |
559
+ | **`useTripleCameraIfAvailable`** | <code>boolean</code> | Whether to use the triple camera if available (iPhone Pro models only) | <code>false</code> | |
560
+ | **`preferredCameraDeviceTypes`** | <code>CameraDeviceType[]</code> | Ordered list of preferred camera device types to use (iOS only). The system will attempt to use the first available camera type in the list. If position is also provided, the system will use the first available camera type that matches the position and is in the list. This will fallback to the default camera type if none of the preferred types are available. | <code>undefined - system will decide based on position/deviceId</code> | |
561
+ | **`zoomFactor`** | <code>number</code> | The initial zoom factor to use | <code>1.0</code> | |
562
+ | **`containerElementId`** | <code>string</code> | Optional HTML ID of the container element where the camera view should be rendered. If not provided, the camera view will be appended to the document body. Web only. | | |
562
563
 
563
564
 
564
565
  #### IsRunningResponse
@@ -693,6 +694,15 @@ Coordinates are normalized between 0 and 1 relative to the camera frame.
693
694
  ### Type Aliases
694
695
 
695
696
 
697
+ #### BarcodeType
698
+
699
+ Supported barcode types for detection.
700
+ Specifying only the barcode types you need can improve performance
701
+ and reduce battery consumption.
702
+
703
+ <code>'qr' | 'code128' | 'code39' | 'code39Mod43' | 'code93' | 'ean8' | 'ean13' | 'interleaved2of5' | 'itf14' | 'pdf417' | 'aztec' | 'dataMatrix' | 'upce'</code>
704
+
705
+
696
706
  #### CameraPosition
697
707
 
698
708
  Position options for the camera.
@@ -16,7 +16,7 @@ buildscript {
16
16
  mavenCentral()
17
17
  }
18
18
  dependencies {
19
- classpath 'com.android.tools.build:gradle:8.13.0'
19
+ classpath 'com.android.tools.build:gradle:8.13.2'
20
20
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
21
21
  }
22
22
  }
@@ -37,10 +37,10 @@ android {
37
37
  buildTypes {
38
38
  release {
39
39
  minifyEnabled false
40
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
40
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
41
41
  }
42
42
  }
43
- lintOptions {
43
+ lint {
44
44
  abortOnError = false
45
45
  }
46
46
  compileOptions {
@@ -66,13 +66,13 @@ dependencies {
66
66
  implementation project(':capacitor-android')
67
67
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
68
68
  implementation 'androidx.core:core-ktx:1.16.0'
69
- implementation 'androidx.compose.material3:material3-android:1.3.2'
69
+ implementation 'androidx.compose.material3:material3-android:1.4.0'
70
70
  testImplementation "junit:junit:$junitVersion"
71
71
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
72
72
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
73
73
 
74
74
  // CameraX dependencies
75
- def camerax_version = "1.4.2"
75
+ def camerax_version = "1.5.3"
76
76
  implementation "androidx.camera:camera-core:${camerax_version}"
77
77
  implementation "androidx.camera:camera-camera2:${camerax_version}"
78
78
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
@@ -81,4 +81,7 @@ dependencies {
81
81
 
82
82
  // ML Kit for barcode scanning
83
83
  implementation "com.google.mlkit:barcode-scanning:17.3.0"
84
+
85
+ // Kotlin coroutines for modern async handling
86
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
84
87
  }
@@ -36,20 +36,44 @@ import com.google.mlkit.vision.barcode.BarcodeScanning
36
36
  import com.google.mlkit.vision.barcode.common.Barcode
37
37
  import com.michaelwolz.capacitorcameraview.model.BarcodeDetectionResult
38
38
  import com.michaelwolz.capacitorcameraview.model.CameraDevice
39
+ import com.michaelwolz.capacitorcameraview.model.CameraResult
39
40
  import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
40
41
  import com.michaelwolz.capacitorcameraview.model.ZoomFactors
42
+ import kotlinx.coroutines.CoroutineScope
43
+ import kotlinx.coroutines.Dispatchers
44
+ import kotlinx.coroutines.SupervisorJob
45
+ import kotlinx.coroutines.cancel
46
+ import kotlinx.coroutines.channels.BufferOverflow
47
+ import kotlinx.coroutines.flow.MutableSharedFlow
48
+ import kotlinx.coroutines.flow.SharedFlow
49
+ import kotlinx.coroutines.flow.asSharedFlow
50
+ import kotlinx.coroutines.launch
51
+ import kotlinx.coroutines.suspendCancellableCoroutine
52
+ import kotlinx.coroutines.withContext
41
53
  import java.io.ByteArrayOutputStream
42
54
  import java.io.File
43
55
  import java.io.FileOutputStream
44
56
  import java.util.concurrent.ExecutorService
45
57
  import java.util.concurrent.Executors
58
+ import java.util.concurrent.atomic.AtomicLong
59
+ import java.util.concurrent.atomic.AtomicReference
60
+ import kotlin.coroutines.resume
46
61
 
47
62
  /** Throttle time for barcode detection in milliseconds. */
48
- const val BARCODE_DETECTION_THROTTLE_MS = 100
63
+ const val BARCODE_DETECTION_THROTTLE_MS = 100L
49
64
 
50
65
  class CameraView(plugin: Plugin) {
51
- // Camera components
52
- private var cameraController: LifecycleCameraController? = null
66
+ // Coroutine scope for async operations
67
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
68
+
69
+ // Thread-safe camera controller reference
70
+ private val cameraControllerRef = AtomicReference<LifecycleCameraController?>(null)
71
+
72
+ // Camera components (using atomic reference for thread safety)
73
+ private var cameraController: LifecycleCameraController?
74
+ get() = cameraControllerRef.get()
75
+ set(value) { cameraControllerRef.set(value) }
76
+
53
77
  private val cameraExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() }
54
78
  private var previewView: PreviewView? = null
55
79
 
@@ -65,56 +89,77 @@ class CameraView(plugin: Plugin) {
65
89
 
66
90
  private val mainHandler by lazy { android.os.Handler(android.os.Looper.getMainLooper()) }
67
91
 
68
- private var lastBarcodeDetectionTime = 0L
92
+ // Thread-safe barcode throttle timestamp
93
+ private val lastBarcodeDetectionTime = AtomicLong(0L)
94
+
95
+ // Flow for reactive barcode events
96
+ private val _barcodeEvents = MutableSharedFlow<BarcodeDetectionResult>(
97
+ replay = 0,
98
+ extraBufferCapacity = 1,
99
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
100
+ )
101
+ val barcodeEvents: SharedFlow<BarcodeDetectionResult> = _barcodeEvents.asSharedFlow()
69
102
 
70
103
  /** Starts a camera session with the provided configuration. */
71
- fun startSession(config: CameraSessionConfiguration, callback: (Exception?) -> Unit) {
72
- val lifecycleOwner =
73
- context as? LifecycleOwner
74
- ?: run {
75
- callback(CameraError.LifecycleOwnerMissing())
76
- return
77
- }
104
+ suspend fun startSessionAsync(config: CameraSessionConfiguration): CameraResult<Unit> =
105
+ withContext(Dispatchers.Main) {
106
+ val lifecycleOwner = context as? LifecycleOwner
107
+ ?: return@withContext CameraResult.Error(CameraError.LifecycleOwnerMissing())
78
108
 
79
- // Store references for later use
80
- this.lifecycleOwner = lifecycleOwner
109
+ this@CameraView.lifecycleOwner = lifecycleOwner
81
110
 
82
- mainHandler.post {
83
111
  try {
84
112
  initializeCamera(context, lifecycleOwner, config)
85
- callback(null)
113
+ CameraResult.Success(Unit)
86
114
  } catch (e: Exception) {
87
115
  Log.e(TAG, "Error in camera setup", e)
88
- callback(e)
116
+ CameraResult.Error(e)
89
117
  }
90
118
  }
119
+
120
+ /** Starts a camera session with the provided configuration (callback version for backward compatibility). */
121
+ fun startSession(config: CameraSessionConfiguration, callback: (Exception?) -> Unit) {
122
+ scope.launch {
123
+ startSessionAsync(config).fold(
124
+ onSuccess = { callback(null) },
125
+ onError = { callback(it) }
126
+ )
127
+ }
91
128
  }
92
129
 
93
- /** Stop the camera session and release resources */
94
- fun stopSession(callback: ((Exception?) -> Unit)? = null) {
95
- mainHandler.post {
130
+ /** Stop the camera session and release resources. */
131
+ suspend fun stopSessionAsync(): CameraResult<Unit> = withContext(Dispatchers.Main) {
132
+ try {
96
133
  cameraController?.unbind()
97
134
 
98
- try {
99
- previewView?.let { view ->
100
- try {
101
- (webView.parent as? ViewGroup)?.removeView(view)
102
- } catch (e: Exception) {
103
- Log.e(TAG, "Error removing preview view", e)
104
- } finally {
105
- previewView = null
106
- }
135
+ previewView?.let { view ->
136
+ try {
137
+ (webView.parent as? ViewGroup)?.removeView(view)
138
+ } catch (e: Exception) {
139
+ Log.e(TAG, "Error removing preview view", e)
140
+ } finally {
141
+ previewView = null
107
142
  }
143
+ }
108
144
 
109
- webView.setLayerType(WebView.LAYER_TYPE_NONE, null)
110
- webView.setBackgroundColor(android.graphics.Color.WHITE)
145
+ webView.setLayerType(WebView.LAYER_TYPE_NONE, null)
146
+ webView.setBackgroundColor(android.graphics.Color.WHITE)
111
147
 
112
- Log.d(TAG, "Camera session stopped successfully")
113
- callback?.invoke(null)
114
- } catch (e: Exception) {
115
- Log.e(TAG, "Error stopping camera session", e)
116
- callback?.invoke(e)
117
- }
148
+ Log.d(TAG, "Camera session stopped successfully")
149
+ CameraResult.Success(Unit)
150
+ } catch (e: Exception) {
151
+ Log.e(TAG, "Error stopping camera session", e)
152
+ CameraResult.Error(e)
153
+ }
154
+ }
155
+
156
+ /** Stop the camera session and release resources (callback version for backward compatibility). */
157
+ fun stopSession(callback: ((Exception?) -> Unit)? = null) {
158
+ scope.launch {
159
+ stopSessionAsync().fold(
160
+ onSuccess = { callback?.invoke(null) },
161
+ onError = { callback?.invoke(it) }
162
+ )
118
163
  }
119
164
  }
120
165
 
@@ -123,24 +168,24 @@ class CameraView(plugin: Plugin) {
123
168
  return cameraController != null
124
169
  }
125
170
 
126
- /** Capture a photo with the current camera configuration */
127
- fun capturePhoto(
171
+ /** Capture a photo with the current camera configuration. */
172
+ suspend fun capturePhotoAsync(
128
173
  quality: Int,
129
- saveToFile: Boolean = false,
130
- callback: (JSObject?, Exception?) -> Unit
131
- ) {
174
+ saveToFile: Boolean = false
175
+ ): CameraResult<JSObject> = suspendCancellableCoroutine { continuation ->
132
176
  val startTime = System.currentTimeMillis()
177
+
133
178
  val controller = cameraController
134
- ?: run {
135
- callback(null, CameraError.CameraNotInitialized())
136
- return
137
- }
179
+ if (controller == null) {
180
+ continuation.resume(CameraResult.Error(CameraError.CameraNotInitialized()))
181
+ return@suspendCancellableCoroutine
182
+ }
138
183
 
139
184
  val preview = previewView
140
- ?: run {
141
- callback(null, CameraError.PreviewNotInitialized())
142
- return
143
- }
185
+ if (preview == null) {
186
+ continuation.resume(CameraResult.Error(CameraError.PreviewNotInitialized()))
187
+ return@suspendCancellableCoroutine
188
+ }
144
189
 
145
190
  mainHandler.post {
146
191
  val cameraInfo = controller.cameraInfo
@@ -177,12 +222,12 @@ class CameraView(plugin: Plugin) {
177
222
 
178
223
  put("webPath", capacitorFilePath)
179
224
  }
180
- callback(result, null)
225
+ continuation.resume(CameraResult.Success(result))
181
226
  }
182
227
 
183
228
  override fun onError(exception: ImageCaptureException) {
184
229
  Log.e(TAG, "Error saving image to file", exception)
185
- callback(null, exception)
230
+ continuation.resume(CameraResult.Error(exception))
186
231
  }
187
232
  }
188
233
  )
@@ -196,106 +241,108 @@ class CameraView(plugin: Plugin) {
196
241
  TAG,
197
242
  "Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
198
243
  )
199
- handleCaptureSuccess(image, quality, imageRotationDegrees, callback)
244
+ try {
245
+ val base64String = imageProxyToBase64(image, quality, imageRotationDegrees)
246
+ val result = JSObject().apply {
247
+ put("photo", base64String)
248
+ }
249
+ Log.d(TAG, "Image processed to Base64 in ${System.currentTimeMillis() - startTime}ms")
250
+ continuation.resume(CameraResult.Success(result))
251
+ } catch (e: Exception) {
252
+ Log.e(TAG, "Error processing captured image", e)
253
+ continuation.resume(CameraResult.Error(e))
254
+ } finally {
255
+ image.close()
256
+ }
200
257
  }
201
258
 
202
259
  override fun onError(exception: ImageCaptureException) {
203
260
  Log.e(TAG, "Error capturing image", exception)
204
- callback(null, exception)
261
+ continuation.resume(CameraResult.Error(exception))
205
262
  }
206
263
  }
207
264
  )
208
265
  }
209
266
  } catch (e: Exception) {
210
267
  Log.e(TAG, "Error setting up image capture", e)
211
- callback(null, e)
268
+ continuation.resume(CameraResult.Error(e))
212
269
  }
213
270
  }
214
271
  }
215
272
 
216
- /**
217
- * Handles the successful capture of an image for base64 conversion
218
- */
219
- fun handleCaptureSuccess(
220
- image: ImageProxy,
273
+ /** Capture a photo with the current camera configuration (callback version for backward compatibility). */
274
+ fun capturePhoto(
221
275
  quality: Int,
222
- rotationDegrees: Int,
276
+ saveToFile: Boolean = false,
223
277
  callback: (JSObject?, Exception?) -> Unit
224
278
  ) {
225
- val startTime = System.currentTimeMillis()
226
- try {
227
- val base64String = imageProxyToBase64(image, quality, rotationDegrees)
228
- val result = JSObject().apply {
229
- put("photo", base64String)
230
- }
231
- Log.d(TAG, "Image processed to Base64 in ${System.currentTimeMillis() - startTime}ms")
232
- callback(result, null)
233
- } catch (e: Exception) {
234
- Log.e(TAG, "Error processing captured image", e)
235
- callback(null, e)
236
- } finally {
237
- image.close()
279
+ scope.launch {
280
+ capturePhotoAsync(quality, saveToFile).fold(
281
+ onSuccess = { callback(it, null) },
282
+ onError = { callback(null, it) }
283
+ )
238
284
  }
239
285
  }
240
286
 
241
287
  /**
242
- * Capture a frame directly from the preview without using the full photo pipeline which is
243
- * faster but has lower quality.
288
+ * Capture a frame directly from the preview without using the full photo pipeline.
289
+ * Faster but has lower quality than full photo capture.
244
290
  */
245
- fun captureSampleFromPreview(
291
+ suspend fun captureSampleFromPreviewAsync(
246
292
  quality: Int,
247
- saveToFile: Boolean = false,
248
- callback: (JSObject?, Exception?) -> Unit
249
- ) {
250
- val previewView =
251
- this.previewView
252
- ?: run {
253
- callback(null, CameraError.PreviewNotInitialized())
254
- return
255
- }
256
-
257
- mainHandler.post {
258
- try {
259
- val bitmap =
260
- previewView.bitmap
261
- ?: run {
262
- callback(null, Exception("Preview bitmap not available"))
263
- return@post
264
- }
265
-
266
- val result = JSObject()
293
+ saveToFile: Boolean = false
294
+ ): CameraResult<JSObject> = withContext(Dispatchers.Main) {
295
+ val preview = previewView
296
+ ?: return@withContext CameraResult.Error(CameraError.PreviewNotInitialized())
267
297
 
268
- if (saveToFile) {
269
- val tempFile =
270
- File.createTempFile("camera_capture_sample", ".jpg", context.cacheDir)
298
+ try {
299
+ val bitmap = preview.bitmap
300
+ ?: return@withContext CameraResult.Error(Exception("Preview bitmap not available"))
271
301
 
272
- FileOutputStream(tempFile).use { outputStream ->
273
- bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
274
- }
302
+ val result = JSObject()
275
303
 
276
- val capacitorFilePath = FileUtils.getPortablePath(
277
- context,
278
- pluginDelegate.bridge.localUrl,
279
- Uri.fromFile(tempFile)
280
- )
304
+ if (saveToFile) {
305
+ val tempFile =
306
+ File.createTempFile("camera_capture_sample", ".jpg", context.cacheDir)
281
307
 
282
- result.put("webPath", capacitorFilePath)
283
- } else {
284
- // Convert bitmap to Base64
285
- val outputStream = ByteArrayOutputStream()
286
- outputStream.use { stream ->
287
- bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
288
- val byteArray = stream.toByteArray()
289
- val base64String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
290
- result.put("photo", base64String)
291
- }
308
+ FileOutputStream(tempFile).use { outputStream ->
309
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
292
310
  }
293
311
 
294
- callback(result, null)
295
- } catch (e: Exception) {
296
- Log.e(TAG, "Error capturing preview frame", e)
297
- callback(null, e)
312
+ val capacitorFilePath = FileUtils.getPortablePath(
313
+ context,
314
+ pluginDelegate.bridge.localUrl,
315
+ Uri.fromFile(tempFile)
316
+ )
317
+
318
+ result.put("webPath", capacitorFilePath)
319
+ } else {
320
+ // Convert bitmap to Base64 using pooled stream
321
+ val base64String = bitmapToBase64(bitmap, quality)
322
+ result.put("photo", base64String)
298
323
  }
324
+
325
+ CameraResult.Success(result)
326
+ } catch (e: Exception) {
327
+ Log.e(TAG, "Error capturing preview frame", e)
328
+ CameraResult.Error(e)
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Capture a frame directly from the preview without using the full photo pipeline (callback version).
334
+ * Faster but has lower quality than full photo capture.
335
+ */
336
+ fun captureSampleFromPreview(
337
+ quality: Int,
338
+ saveToFile: Boolean = false,
339
+ callback: (JSObject?, Exception?) -> Unit
340
+ ) {
341
+ scope.launch {
342
+ captureSampleFromPreviewAsync(quality, saveToFile).fold(
343
+ onSuccess = { callback(it, null) },
344
+ onError = { callback(null, it) }
345
+ )
299
346
  }
300
347
  }
301
348
 
@@ -481,6 +528,9 @@ class CameraView(plugin: Plugin) {
481
528
 
482
529
  /** Clean up resources when the plugin is being destroyed */
483
530
  fun cleanup() {
531
+ // Cancel all coroutines first
532
+ scope.cancel()
533
+
484
534
  mainHandler.post {
485
535
  try {
486
536
  // Stop camera session
@@ -512,6 +562,14 @@ class CameraView(plugin: Plugin) {
512
562
  }
513
563
  }
514
564
 
565
+ /** Converts a bitmap to Base64 with memory-efficient pooled ByteArrayOutputStream. */
566
+ private fun bitmapToBase64(bitmap: Bitmap, quality: Int): String {
567
+ val outputStream = ByteArrayOutputStream(256 * 1024) // 256KB initial capacity
568
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
569
+ val byteArray = outputStream.toByteArray()
570
+ return Base64.encodeToString(byteArray, Base64.NO_WRAP)
571
+ }
572
+
515
573
  private fun setupPreviewView(context: Context) {
516
574
  // Make WebView transparent
517
575
  webView.setBackgroundColor(android.graphics.Color.TRANSPARENT)
@@ -575,7 +633,7 @@ class CameraView(plugin: Plugin) {
575
633
 
576
634
  // Setup barcode scanning if needed
577
635
  if (config.enableBarcodeDetection) {
578
- setupBarcodeScanner(controller)
636
+ setupBarcodeScanner(controller, config.barcodeTypes)
579
637
  }
580
638
 
581
639
  // Bind to lifecycle
@@ -585,13 +643,33 @@ class CameraView(plugin: Plugin) {
585
643
  this.setZoomFactor(config.zoomFactor, null)
586
644
  }
587
645
 
588
- private fun setupBarcodeScanner(controller: LifecycleCameraController) {
646
+ /**
647
+ * Sets up the barcode scanner with the specified formats.
648
+ *
649
+ * @param controller The camera controller to attach the scanner to.
650
+ * @param barcodeTypes Optional list of specific barcode format codes to detect.
651
+ * If null, all supported formats are detected (backwards compatible).
652
+ */
653
+ private fun setupBarcodeScanner(
654
+ controller: LifecycleCameraController,
655
+ barcodeTypes: List<Int>? = null
656
+ ) {
589
657
  val previewView = this.previewView ?: return
590
658
 
591
- val options =
659
+ // Build scanner options with specified formats or all formats
660
+ val options = if (barcodeTypes != null && barcodeTypes.isNotEmpty()) {
661
+ // Use specific formats - setBarcodeFormats takes first format + vararg rest
662
+ val firstFormat = barcodeTypes.first()
663
+ val restFormats = barcodeTypes.drop(1).toIntArray()
664
+ BarcodeScannerOptions.Builder()
665
+ .setBarcodeFormats(firstFormat, *restFormats)
666
+ .build()
667
+ } else {
668
+ // Default to all formats for backwards compatibility
592
669
  BarcodeScannerOptions.Builder()
593
670
  .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
594
671
  .build()
672
+ }
595
673
 
596
674
  val barcodeScanner = BarcodeScanning.getClient(options)
597
675
  val mainExecutor = ContextCompat.getMainExecutor(previewView.context)
@@ -621,10 +699,18 @@ class CameraView(plugin: Plugin) {
621
699
  topOffset: Int
622
700
  ) {
623
701
  val now = System.currentTimeMillis()
624
- if (now - lastBarcodeDetectionTime < BARCODE_DETECTION_THROTTLE_MS) {
702
+ val lastTime = lastBarcodeDetectionTime.get()
703
+
704
+ // Thread-safe throttle check using atomic compare-and-set
705
+ if (now - lastTime < BARCODE_DETECTION_THROTTLE_MS) {
625
706
  return // Skip this frame
626
707
  }
627
708
 
709
+ // Atomically update the timestamp - if another thread beat us, skip
710
+ if (!lastBarcodeDetectionTime.compareAndSet(lastTime, now)) {
711
+ return
712
+ }
713
+
628
714
  val barcodes = result?.getValue(barcodeScanner) ?: return
629
715
  if (barcodes.isEmpty()) return
630
716
 
@@ -642,8 +728,13 @@ class CameraView(plugin: Plugin) {
642
728
  boundingRect = webBoundingRect
643
729
  )
644
730
 
731
+ // Emit to Flow for reactive subscribers
732
+ scope.launch {
733
+ _barcodeEvents.emit(barcodeResult)
734
+ }
735
+
736
+ // Also notify via callback for backward compatibility
645
737
  notifyBarcodeDetected(barcodeResult)
646
- lastBarcodeDetectionTime = now
647
738
  }
648
739
 
649
740
  private fun notifyBarcodeDetected(result: BarcodeDetectionResult) {