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.
- package/README.md +19 -9
- package/android/build.gradle +8 -5
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +217 -126
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +70 -30
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +94 -5
- package/dist/docs.json +81 -0
- package/dist/esm/definitions.d.ts +44 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +7 -1
- package/dist/esm/web.js +67 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +68 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +68 -2
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +97 -2
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +29 -2
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +45 -13
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +193 -59
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +83 -84
- package/ios/Sources/CameraViewPlugin/TempFileManager.swift +181 -0
- package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
- 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
|
-
| **`
|
|
557
|
-
| **`
|
|
558
|
-
| **`
|
|
559
|
-
| **`
|
|
560
|
-
| **`
|
|
561
|
-
| **`
|
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -16,7 +16,7 @@ buildscript {
|
|
|
16
16
|
mavenCentral()
|
|
17
17
|
}
|
|
18
18
|
dependencies {
|
|
19
|
-
classpath 'com.android.tools.build:gradle:8.13.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
63
|
+
const val BARCODE_DETECTION_THROTTLE_MS = 100L
|
|
49
64
|
|
|
50
65
|
class CameraView(plugin: Plugin) {
|
|
51
|
-
//
|
|
52
|
-
private
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
context as? LifecycleOwner
|
|
74
|
-
?:
|
|
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
|
-
|
|
80
|
-
this.lifecycleOwner = lifecycleOwner
|
|
109
|
+
this@CameraView.lifecycleOwner = lifecycleOwner
|
|
81
110
|
|
|
82
|
-
mainHandler.post {
|
|
83
111
|
try {
|
|
84
112
|
initializeCamera(context, lifecycleOwner, config)
|
|
85
|
-
|
|
113
|
+
CameraResult.Success(Unit)
|
|
86
114
|
} catch (e: Exception) {
|
|
87
115
|
Log.e(TAG, "Error in camera setup", e)
|
|
88
|
-
|
|
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
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
145
|
+
webView.setLayerType(WebView.LAYER_TYPE_NONE, null)
|
|
146
|
+
webView.setBackgroundColor(android.graphics.Color.WHITE)
|
|
111
147
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
171
|
+
/** Capture a photo with the current camera configuration. */
|
|
172
|
+
suspend fun capturePhotoAsync(
|
|
128
173
|
quality: Int,
|
|
129
|
-
saveToFile: Boolean = false
|
|
130
|
-
|
|
131
|
-
) {
|
|
174
|
+
saveToFile: Boolean = false
|
|
175
|
+
): CameraResult<JSObject> = suspendCancellableCoroutine { continuation ->
|
|
132
176
|
val startTime = System.currentTimeMillis()
|
|
177
|
+
|
|
133
178
|
val controller = cameraController
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
179
|
+
if (controller == null) {
|
|
180
|
+
continuation.resume(CameraResult.Error(CameraError.CameraNotInitialized()))
|
|
181
|
+
return@suspendCancellableCoroutine
|
|
182
|
+
}
|
|
138
183
|
|
|
139
184
|
val preview = previewView
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
+
continuation.resume(CameraResult.Error(e))
|
|
212
269
|
}
|
|
213
270
|
}
|
|
214
271
|
}
|
|
215
272
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
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
|
-
|
|
276
|
+
saveToFile: Boolean = false,
|
|
223
277
|
callback: (JSObject?, Exception?) -> Unit
|
|
224
278
|
) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
243
|
-
*
|
|
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
|
|
291
|
+
suspend fun captureSampleFromPreviewAsync(
|
|
246
292
|
quality: Int,
|
|
247
|
-
saveToFile: Boolean = false
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
298
|
+
try {
|
|
299
|
+
val bitmap = preview.bitmap
|
|
300
|
+
?: return@withContext CameraResult.Error(Exception("Preview bitmap not available"))
|
|
271
301
|
|
|
272
|
-
|
|
273
|
-
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
|
274
|
-
}
|
|
302
|
+
val result = JSObject()
|
|
275
303
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Uri.fromFile(tempFile)
|
|
280
|
-
)
|
|
304
|
+
if (saveToFile) {
|
|
305
|
+
val tempFile =
|
|
306
|
+
File.createTempFile("camera_capture_sample", ".jpg", context.cacheDir)
|
|
281
307
|
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|