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.
- package/CapacitorCameraView.podspec +17 -0
- package/LICENSE +201 -0
- package/Package.swift +28 -0
- package/README.md +654 -0
- package/android/build.gradle +79 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +555 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +227 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/BarcodeDetectionResult.kt +11 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraDevice.kt +14 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +10 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/WebBoundingRect.kt +16 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/ZoomFactors.kt +14 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +86 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +968 -0
- package/dist/esm/definitions.d.ts +378 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/utils.d.ts +45 -0
- package/dist/esm/utils.js +108 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/web.d.ts +108 -0
- package/dist/esm/web.js +406 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +530 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +533 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/CameraViewPlugin/CameraError.swift +39 -0
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +32 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +91 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +52 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +78 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +633 -0
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +295 -0
- package/ios/Sources/CameraViewPlugin/Utils.swift +56 -0
- package/ios/Tests/CameraViewPluginTests/CameraViewPluginTests.swift +15 -0
- 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,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
|
+
}
|