capacitor-camera-view 1.0.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 (42) hide show
  1. package/CapacitorCameraView.podspec +17 -0
  2. package/LICENSE +201 -0
  3. package/Package.swift +28 -0
  4. package/README.md +654 -0
  5. package/android/build.gradle +79 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +555 -0
  8. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +227 -0
  9. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/BarcodeDetectionResult.kt +11 -0
  10. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraDevice.kt +14 -0
  11. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +10 -0
  12. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/WebBoundingRect.kt +16 -0
  13. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/ZoomFactors.kt +14 -0
  14. package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +86 -0
  15. package/android/src/main/res/.gitkeep +0 -0
  16. package/dist/docs.json +968 -0
  17. package/dist/esm/definitions.d.ts +378 -0
  18. package/dist/esm/definitions.js +2 -0
  19. package/dist/esm/definitions.js.map +1 -0
  20. package/dist/esm/index.d.ts +7 -0
  21. package/dist/esm/index.js +10 -0
  22. package/dist/esm/index.js.map +1 -0
  23. package/dist/esm/utils.d.ts +45 -0
  24. package/dist/esm/utils.js +108 -0
  25. package/dist/esm/utils.js.map +1 -0
  26. package/dist/esm/web.d.ts +108 -0
  27. package/dist/esm/web.js +406 -0
  28. package/dist/esm/web.js.map +1 -0
  29. package/dist/plugin.cjs.js +530 -0
  30. package/dist/plugin.cjs.js.map +1 -0
  31. package/dist/plugin.js +533 -0
  32. package/dist/plugin.js.map +1 -0
  33. package/ios/Sources/CameraViewPlugin/CameraError.swift +39 -0
  34. package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +32 -0
  35. package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +91 -0
  36. package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +52 -0
  37. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +78 -0
  38. package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +633 -0
  39. package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +295 -0
  40. package/ios/Sources/CameraViewPlugin/Utils.swift +56 -0
  41. package/ios/Tests/CameraViewPluginTests/CameraViewPluginTests.swift +15 -0
  42. package/package.json +94 -0
@@ -0,0 +1,79 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
6
+ }
7
+
8
+ buildscript {
9
+ ext {
10
+ kotlin_version = '1.9.24'
11
+ }
12
+ repositories {
13
+ google()
14
+ mavenCentral()
15
+ }
16
+ dependencies {
17
+ classpath 'com.android.tools.build:gradle:8.7.3'
18
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
19
+ }
20
+ }
21
+
22
+ apply plugin: 'com.android.library'
23
+ apply plugin: 'org.jetbrains.kotlin.android'
24
+
25
+ android {
26
+ namespace "com.michaelwolz.capacitorcameraview"
27
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
28
+ defaultConfig {
29
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
30
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
31
+ versionCode 1
32
+ versionName "1.0"
33
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
34
+ }
35
+ buildTypes {
36
+ release {
37
+ minifyEnabled false
38
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
39
+ }
40
+ }
41
+ lintOptions {
42
+ abortOnError false
43
+ }
44
+ compileOptions {
45
+ sourceCompatibility JavaVersion.VERSION_21
46
+ targetCompatibility JavaVersion.VERSION_21
47
+ }
48
+ kotlinOptions {
49
+ jvmTarget = '21'
50
+ }
51
+ }
52
+
53
+ repositories {
54
+ google()
55
+ mavenCentral()
56
+ }
57
+
58
+
59
+ dependencies {
60
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
61
+ implementation project(':capacitor-android')
62
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
63
+ implementation 'androidx.core:core-ktx:1.16.0'
64
+ implementation 'androidx.compose.material3:material3-android:1.3.2'
65
+ testImplementation "junit:junit:$junitVersion"
66
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
67
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
68
+
69
+ // CameraX dependencies
70
+ def camerax_version = "1.4.2"
71
+ implementation "androidx.camera:camera-core:${camerax_version}"
72
+ implementation "androidx.camera:camera-camera2:${camerax_version}"
73
+ implementation "androidx.camera:camera-lifecycle:${camerax_version}"
74
+ implementation "androidx.camera:camera-view:${camerax_version}"
75
+ implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
76
+
77
+ // ML Kit for barcode scanning
78
+ implementation "com.google.mlkit:barcode-scanning:17.3.0"
79
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,555 @@
1
+ package com.michaelwolz.capacitorcameraview
2
+
3
+ import android.content.Context
4
+ import android.content.Context.CAMERA_SERVICE
5
+ import android.graphics.Bitmap
6
+ import android.graphics.BitmapFactory
7
+ import android.graphics.Matrix
8
+ import android.hardware.camera2.CameraCharacteristics
9
+ import android.hardware.camera2.CameraManager
10
+ import android.util.Base64
11
+ import android.util.Log
12
+ import android.view.ViewGroup
13
+ import android.webkit.WebView
14
+ import androidx.annotation.OptIn
15
+ import androidx.camera.camera2.interop.Camera2CameraInfo
16
+ import androidx.camera.camera2.interop.ExperimentalCamera2Interop
17
+ import androidx.camera.core.CameraSelector
18
+ import androidx.camera.core.ImageAnalysis
19
+ import androidx.camera.core.ImageCapture
20
+ import androidx.camera.core.ImageCaptureException
21
+ import androidx.camera.core.ImageProxy
22
+ import androidx.camera.core.resolutionselector.AspectRatioStrategy
23
+ import androidx.camera.core.resolutionselector.ResolutionSelector
24
+ import androidx.camera.mlkit.vision.MlKitAnalyzer
25
+ import androidx.camera.view.LifecycleCameraController
26
+ import androidx.camera.view.PreviewView
27
+ import androidx.core.content.ContextCompat
28
+ import androidx.lifecycle.LifecycleOwner
29
+ import com.getcapacitor.Plugin
30
+ import com.google.mlkit.vision.barcode.BarcodeScanner
31
+ import com.google.mlkit.vision.barcode.BarcodeScannerOptions
32
+ import com.google.mlkit.vision.barcode.BarcodeScanning
33
+ import com.google.mlkit.vision.barcode.common.Barcode
34
+ import com.michaelwolz.capacitorcameraview.model.BarcodeDetectionResult
35
+ import com.michaelwolz.capacitorcameraview.model.CameraDevice
36
+ import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
37
+ import com.michaelwolz.capacitorcameraview.model.ZoomFactors
38
+ import java.io.ByteArrayOutputStream
39
+ import java.util.concurrent.ExecutorService
40
+ import java.util.concurrent.Executors
41
+
42
+ /** Throttle time for barcode detection in milliseconds. */
43
+ const val BARCODE_DETECTION_THROTTLE_MS = 100
44
+
45
+ class CameraView(plugin: Plugin) {
46
+ // Camera components
47
+ private var cameraController: LifecycleCameraController? = null
48
+ private val cameraExecutor: ExecutorService by lazy { Executors.newSingleThreadExecutor() }
49
+ private var previewView: PreviewView? = null
50
+
51
+ // Camera state
52
+ private var currentCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
53
+ private var currentFlashMode: Int = ImageCapture.FLASH_MODE_OFF
54
+
55
+ // Camera use cases
56
+ private var imageCapture: ImageCapture? = null
57
+
58
+ // Plugin context
59
+ private var lifecycleOwner: LifecycleOwner? = null
60
+ private var pluginDelegate: Plugin = plugin
61
+ private var webView: WebView = plugin.bridge.webView
62
+ private var context: Context = webView.context
63
+
64
+ private val mainHandler by lazy { android.os.Handler(android.os.Looper.getMainLooper()) }
65
+
66
+ private var lastBarcodeDetectionTime = 0L
67
+
68
+ /** Starts a camera session with the provided configuration. */
69
+ fun startSession(config: CameraSessionConfiguration, callback: (Exception?) -> Unit) {
70
+ val lifecycleOwner =
71
+ context as? LifecycleOwner
72
+ ?: run {
73
+ callback(Exception("WebView context must be a LifecycleOwner"))
74
+ return
75
+ }
76
+
77
+ // Store references for later use
78
+ this.lifecycleOwner = lifecycleOwner
79
+
80
+ mainHandler.post {
81
+ try {
82
+ initializeCamera(context, lifecycleOwner, config)
83
+ callback(null)
84
+ } catch (e: Exception) {
85
+ Log.e(TAG, "Error in camera setup", e)
86
+ callback(e)
87
+ }
88
+ }
89
+ }
90
+
91
+ /** Stop the camera session and release resources */
92
+ fun stopSession(callback: ((Exception?) -> Unit)? = null) {
93
+ mainHandler.post {
94
+ cameraController?.unbind()
95
+
96
+ try {
97
+ imageCapture = null
98
+
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
+ }
107
+ }
108
+
109
+ webView.setLayerType(WebView.LAYER_TYPE_NONE, null)
110
+ webView.setBackgroundColor(android.graphics.Color.WHITE)
111
+
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
+ }
118
+ }
119
+ }
120
+
121
+ /** Checks if the camera session is running */
122
+ fun isRunning(): Boolean {
123
+ return cameraController != null
124
+ }
125
+
126
+ /** Capture a photo with the current camera configuration */
127
+ fun capturePhoto(quality: Int?, callback: (String?, Exception?) -> Unit) {
128
+ val controller =
129
+ this.cameraController
130
+ ?: run {
131
+ callback(null, Exception("Camera controller not initialized"))
132
+ return
133
+ }
134
+
135
+ mainHandler.post {
136
+ try {
137
+ controller.takePicture(
138
+ cameraExecutor,
139
+ object : ImageCapture.OnImageCapturedCallback() {
140
+ override fun onCaptureSuccess(image: ImageProxy) {
141
+ try {
142
+ val base64String = imageProxyToBase64(image, quality)
143
+ callback(base64String, null)
144
+ } catch (e: Exception) {
145
+ Log.e(TAG, "Error processing captured image", e)
146
+ callback(null, e)
147
+ } finally {
148
+ image.close()
149
+ }
150
+ }
151
+
152
+ override fun onError(exception: ImageCaptureException) {
153
+ Log.e(TAG, "Error capturing image", exception)
154
+ callback(null, exception)
155
+ }
156
+ }
157
+ )
158
+ } catch (e: Exception) {
159
+ Log.e(TAG, "Error setting up image capture", e)
160
+ callback(null, e)
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Capture a frame directly from the preview without using the full photo pipeline which is
167
+ * faster but has lower quality.
168
+ */
169
+ fun captureSampleFromPreview(quality: Int?, callback: (String?, Exception?) -> Unit) {
170
+ val previewView =
171
+ this.previewView
172
+ ?: run {
173
+ callback(null, Exception("Camera preview not initialized"))
174
+ return
175
+ }
176
+
177
+ mainHandler.post {
178
+ val outputStream = ByteArrayOutputStream()
179
+ try {
180
+ val bitmap =
181
+ previewView.bitmap
182
+ ?: run {
183
+ callback(null, Exception("Preview bitmap not available"))
184
+ return@post
185
+ }
186
+
187
+ // Convert bitmap to Base64
188
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality ?: 90, outputStream)
189
+ val byteArray = outputStream.toByteArray()
190
+ val base64String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
191
+
192
+ callback(base64String, null)
193
+ } catch (e: Exception) {
194
+ Log.e(TAG, "Error capturing preview frame", e)
195
+ callback(null, e)
196
+ } finally {
197
+ outputStream.close()
198
+ }
199
+ }
200
+ }
201
+
202
+ /** Flip between front and back cameras */
203
+ fun flipCamera(callback: (Exception?) -> Unit) {
204
+ currentCameraSelector = when (currentCameraSelector) {
205
+ CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA
206
+ else -> CameraSelector.DEFAULT_FRONT_CAMERA
207
+ }
208
+
209
+ val controller =
210
+ this.cameraController
211
+ ?: run {
212
+ callback(Exception("Camera controller not initialized"))
213
+ return
214
+ }
215
+
216
+ mainHandler.post { controller.cameraSelector = currentCameraSelector }
217
+ }
218
+
219
+ /** Get the min, max, and current zoom values */
220
+ fun getSupportedZoomFactors(callback: (ZoomFactors) -> Unit) {
221
+ mainHandler.post { callback(getZoomFactorsInternal()) }
222
+ }
223
+
224
+ /** Set the zoom factor for the camera */
225
+ fun setZoomFactor(zoomFactor: Float, callback: (((Exception?) -> Unit)?) = null) {
226
+ mainHandler.post {
227
+ val cameraControl =
228
+ cameraController?.cameraControl
229
+ ?: run {
230
+ callback?.invoke(Exception("Camera controller not initialized"))
231
+ return@post
232
+ }
233
+
234
+ val availableZoomFactors = getZoomFactorsInternal()
235
+
236
+ if (zoomFactor !in availableZoomFactors.min..availableZoomFactors.max) {
237
+ callback?.invoke(Exception("The requested zoom factor is out of range."))
238
+ return@post
239
+ }
240
+
241
+ Log.d(TAG, "Setting zoom factor to $zoomFactor")
242
+ val zoomFuture = cameraControl.setZoomRatio(zoomFactor)
243
+
244
+ zoomFuture.addListener(
245
+ {
246
+ try {
247
+ zoomFuture.get()
248
+ Log.d(TAG, "Zoom factor set successfully to $zoomFactor")
249
+ callback?.invoke(null)
250
+ } catch (e: Exception) {
251
+ Log.e(TAG, "Failed to set zoom factor", e)
252
+ callback?.invoke(Exception(e.message))
253
+ }
254
+ },
255
+ ContextCompat.getMainExecutor(context)
256
+ )
257
+ }
258
+ }
259
+
260
+ /** Get the current flash mode */
261
+ fun getFlashMode(): String {
262
+ return when (currentFlashMode) {
263
+ ImageCapture.FLASH_MODE_ON -> "on"
264
+ ImageCapture.FLASH_MODE_AUTO -> "auto"
265
+ else -> "off"
266
+ }
267
+ }
268
+
269
+ /** Get supported flash modes */
270
+ fun getSupportedFlashModes(callback: (supportedFlashModes: List<String>) -> Unit) {
271
+ mainHandler.post {
272
+ val cameraInfo =
273
+ cameraController?.cameraInfo
274
+ ?: run {
275
+ callback(listOf("off"))
276
+ return@post
277
+ }
278
+
279
+ callback(
280
+ if (cameraInfo.hasFlashUnit()) {
281
+ listOf("off", "on", "auto")
282
+ } else {
283
+ listOf("off")
284
+ }
285
+ )
286
+ }
287
+ }
288
+
289
+ /** Set the flash mode */
290
+ fun setFlashMode(mode: String) {
291
+ val controller =
292
+ this.cameraController
293
+ ?: run { throw Exception("Camera controller not initialized") }
294
+
295
+ currentFlashMode =
296
+ when (mode) {
297
+ "on" -> ImageCapture.FLASH_MODE_ON
298
+ "auto" -> ImageCapture.FLASH_MODE_AUTO
299
+ else -> ImageCapture.FLASH_MODE_OFF
300
+ }
301
+
302
+ mainHandler.post { controller.imageCaptureFlashMode = currentFlashMode }
303
+ }
304
+
305
+ /** Get a list of available camera devices */
306
+ fun getAvailableDevices(): List<CameraDevice> {
307
+ try {
308
+ val cameraManager =
309
+ context.getSystemService(CAMERA_SERVICE) as? CameraManager ?: return emptyList()
310
+
311
+ return cameraManager.cameraIdList.mapNotNull { cameraId ->
312
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
313
+ val facing =
314
+ characteristics.get(CameraCharacteristics.LENS_FACING)
315
+ ?: return@mapNotNull null
316
+
317
+ val position =
318
+ when (facing) {
319
+ CameraCharacteristics.LENS_FACING_FRONT -> "front"
320
+ CameraCharacteristics.LENS_FACING_BACK -> "back"
321
+ else -> "external"
322
+ }
323
+
324
+ CameraDevice(id = cameraId, name = cameraId, position = position)
325
+ }
326
+ } catch (e: Exception) {
327
+ Log.e(TAG, "Error getting camera devices", e)
328
+ return emptyList()
329
+ }
330
+ }
331
+
332
+ /** Clean up resources when the plugin is being destroyed */
333
+ fun cleanup() {
334
+ mainHandler.post {
335
+ try {
336
+ // Stop camera session
337
+ cameraController?.unbind()
338
+ cameraController = null
339
+
340
+ // Remove preview view
341
+ previewView?.let { view ->
342
+ (webView.parent as? ViewGroup)?.removeView(view)
343
+ previewView = null
344
+ }
345
+
346
+ // Reset WebView properties
347
+ webView.setLayerType(WebView.LAYER_TYPE_NONE, null)
348
+ webView.setBackgroundColor(android.graphics.Color.WHITE)
349
+
350
+ // Clear references
351
+ lifecycleOwner = null
352
+ imageCapture = null
353
+
354
+ // Shutdown executor
355
+ if (!cameraExecutor.isShutdown) {
356
+ cameraExecutor.shutdown()
357
+ }
358
+
359
+ Log.d(TAG, "Camera resources cleaned up successfully")
360
+ } catch (e: Exception) {
361
+ Log.e(TAG, "Error during cleanup", e)
362
+ }
363
+ }
364
+ }
365
+
366
+ private fun setupPreviewView(context: Context) {
367
+ // Make WebView transparent
368
+ webView.setBackgroundColor(android.graphics.Color.TRANSPARENT)
369
+ webView.setLayerType(WebView.LAYER_TYPE_HARDWARE, null)
370
+
371
+ previewView =
372
+ PreviewView(context).apply {
373
+ layoutParams =
374
+ ViewGroup.LayoutParams(
375
+ ViewGroup.LayoutParams.MATCH_PARENT,
376
+ ViewGroup.LayoutParams.MATCH_PARENT
377
+ )
378
+ scaleType = PreviewView.ScaleType.FILL_CENTER
379
+ }
380
+
381
+ (webView.parent as? ViewGroup)?.addView(previewView, 0)
382
+ }
383
+
384
+ @OptIn(ExperimentalCamera2Interop::class)
385
+ private fun initializeCamera(
386
+ context: Context,
387
+ lifecycleOwner: LifecycleOwner,
388
+ config: CameraSessionConfiguration,
389
+ ) {
390
+ // Setup preview view
391
+ setupPreviewView(context)
392
+
393
+ currentCameraSelector = if (config.position == "front") {
394
+ CameraSelector.DEFAULT_FRONT_CAMERA
395
+ } else {
396
+ CameraSelector.DEFAULT_BACK_CAMERA
397
+ }
398
+
399
+ if (config.deviceId != null) {
400
+ // Prefer specific device id over position
401
+ currentCameraSelector = CameraSelector.Builder()
402
+ .addCameraFilter { cameraInfos ->
403
+ cameraInfos.filter { info ->
404
+ val cameraId = Camera2CameraInfo.from(info).cameraId
405
+ cameraId == config.deviceId
406
+ }
407
+ }
408
+ .build()
409
+ }
410
+
411
+ // Initialize camera controller
412
+ val controller = LifecycleCameraController(context).apply {
413
+ cameraSelector = currentCameraSelector
414
+ imageCaptureResolutionSelector = ResolutionSelector.Builder()
415
+ .setAspectRatioStrategy(
416
+ AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
417
+ )
418
+ .build()
419
+ }
420
+
421
+ cameraController = controller
422
+ previewView?.controller = controller
423
+
424
+ // Setup barcode scanning if needed
425
+ if (config.enableBarcodeDetection) {
426
+ setupBarcodeScanner(controller)
427
+ }
428
+
429
+ // Bind to lifecycle
430
+ controller.bindToLifecycle(lifecycleOwner)
431
+
432
+ // Set initial zoom factor
433
+ this.setZoomFactor(config.zoomFactor, null)
434
+ }
435
+
436
+ private fun setupBarcodeScanner(controller: LifecycleCameraController) {
437
+ val previewView = this.previewView ?: return
438
+
439
+ val options =
440
+ BarcodeScannerOptions.Builder()
441
+ .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
442
+ .build()
443
+
444
+ val barcodeScanner = BarcodeScanning.getClient(options)
445
+ val mainExecutor = ContextCompat.getMainExecutor(previewView.context)
446
+
447
+ // Calculate a possible top offset of the webView which is not applied to the previewView
448
+ // and might break the positioning of the bounding box of the barcode in relation to the
449
+ // webView. This is due to capacitors required hack around the edge-to-edge behavior of web
450
+ // views on android
451
+ val topOffset = calculateTopOffset(webView)
452
+
453
+ controller.setImageAnalysisAnalyzer(
454
+ mainExecutor,
455
+ MlKitAnalyzer(
456
+ listOf(barcodeScanner),
457
+ ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED,
458
+ mainExecutor
459
+ ) { result: MlKitAnalyzer.Result? ->
460
+ processBarcodeResults(result, barcodeScanner, previewView, topOffset)
461
+ }
462
+ )
463
+ }
464
+
465
+ private fun processBarcodeResults(
466
+ result: MlKitAnalyzer.Result?,
467
+ barcodeScanner: BarcodeScanner,
468
+ previewView: PreviewView,
469
+ topOffset: Int
470
+ ) {
471
+ val now = System.currentTimeMillis()
472
+ if (now - lastBarcodeDetectionTime < BARCODE_DETECTION_THROTTLE_MS) {
473
+ return // Skip this frame
474
+ }
475
+
476
+ val barcodes = result?.getValue(barcodeScanner) ?: return
477
+ if (barcodes.isEmpty()) return
478
+
479
+ val barcode = barcodes.firstOrNull() ?: return
480
+
481
+ // Adjust bounding box to webView coordinates
482
+ val webBoundingRect =
483
+ boundingBoxToWebBoundingRect(previewView, barcode.boundingBox, topOffset)
484
+
485
+ val barcodeResult =
486
+ BarcodeDetectionResult(
487
+ value = barcode.rawValue ?: "",
488
+ displayValue = barcode.displayValue ?: "",
489
+ type = getBarcodeFormatString(barcode.format),
490
+ boundingRect = webBoundingRect
491
+ )
492
+
493
+ notifyBarcodeDetected(barcodeResult)
494
+ lastBarcodeDetectionTime = now
495
+ }
496
+
497
+ /** Converts an ImageProxy to a Base64 encoded string */
498
+ private fun imageProxyToBase64(image: ImageProxy, quality: Int?): String {
499
+ val buffer = image.planes[0].buffer
500
+ val bytes = ByteArray(buffer.remaining())
501
+ buffer.get(bytes)
502
+
503
+ var bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
504
+
505
+ try {
506
+ // Apply rotation if needed
507
+ if (image.imageInfo.rotationDegrees != 0) {
508
+ val matrix = Matrix()
509
+ matrix.postRotate(image.imageInfo.rotationDegrees.toFloat())
510
+ val rotatedBitmap =
511
+ Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
512
+ // Recycle the original bitmap to prevent memory leaks
513
+ bitmap.recycle()
514
+ bitmap = rotatedBitmap
515
+ }
516
+
517
+ val outputStream = ByteArrayOutputStream()
518
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality ?: 90, outputStream)
519
+ val byteArray = outputStream.toByteArray()
520
+ return Base64.encodeToString(byteArray, Base64.NO_WRAP)
521
+ } finally {
522
+ // Ensure bitmap is always recycled
523
+ bitmap.recycle()
524
+ }
525
+ }
526
+
527
+ private fun notifyBarcodeDetected(result: BarcodeDetectionResult) {
528
+ pluginDelegate.let { plugin ->
529
+ if (plugin is CameraViewPlugin) {
530
+ plugin.notifyBarcodeDetected(result)
531
+ }
532
+ }
533
+ }
534
+
535
+ /** Get the current zoom factors */
536
+ private fun getZoomFactorsInternal(): ZoomFactors {
537
+ cameraController?.let { controller ->
538
+ val zoomState = controller.zoomState
539
+ val zoomFactors =
540
+ ZoomFactors(
541
+ min = zoomState.value?.minZoomRatio ?: 1.0f,
542
+ max = zoomState.value?.maxZoomRatio ?: 1.0f,
543
+ current = zoomState.value?.zoomRatio ?: 1.0f
544
+ )
545
+
546
+ return zoomFactors
547
+ }
548
+
549
+ return ZoomFactors(1.0f, 1.0f, 1.0f)
550
+ }
551
+
552
+ companion object {
553
+ private const val TAG = "CameraView"
554
+ }
555
+ }