capacitor-camera-view 1.0.1 → 1.0.3
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.
|
@@ -7,22 +7,24 @@ import android.hardware.camera2.CameraCharacteristics
|
|
|
7
7
|
import android.hardware.camera2.CameraManager
|
|
8
8
|
import android.util.Base64
|
|
9
9
|
import android.util.Log
|
|
10
|
+
import android.view.Surface
|
|
10
11
|
import android.view.ViewGroup
|
|
11
12
|
import android.webkit.WebView
|
|
12
13
|
import androidx.annotation.OptIn
|
|
13
14
|
import androidx.camera.camera2.interop.Camera2CameraInfo
|
|
14
15
|
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
|
|
15
16
|
import androidx.camera.core.CameraSelector
|
|
17
|
+
import androidx.camera.core.ExperimentalZeroShutterLag
|
|
16
18
|
import androidx.camera.core.ImageAnalysis
|
|
17
19
|
import androidx.camera.core.ImageCapture
|
|
18
20
|
import androidx.camera.core.ImageCaptureException
|
|
21
|
+
import androidx.camera.core.ImageProxy
|
|
19
22
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
|
20
23
|
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
21
24
|
import androidx.camera.mlkit.vision.MlKitAnalyzer
|
|
22
25
|
import androidx.camera.view.LifecycleCameraController
|
|
23
26
|
import androidx.camera.view.PreviewView
|
|
24
27
|
import androidx.core.content.ContextCompat
|
|
25
|
-
import androidx.exifinterface.media.ExifInterface
|
|
26
28
|
import androidx.lifecycle.LifecycleOwner
|
|
27
29
|
import com.getcapacitor.Plugin
|
|
28
30
|
import com.google.mlkit.vision.barcode.BarcodeScanner
|
|
@@ -34,8 +36,6 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
|
|
|
34
36
|
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
|
|
35
37
|
import com.michaelwolz.capacitorcameraview.model.ZoomFactors
|
|
36
38
|
import java.io.ByteArrayOutputStream
|
|
37
|
-
import java.io.File
|
|
38
|
-
import java.util.UUID
|
|
39
39
|
import java.util.concurrent.ExecutorService
|
|
40
40
|
import java.util.concurrent.Executors
|
|
41
41
|
|
|
@@ -52,9 +52,6 @@ class CameraView(plugin: Plugin) {
|
|
|
52
52
|
private var currentCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
53
53
|
private var currentFlashMode: Int = ImageCapture.FLASH_MODE_OFF
|
|
54
54
|
|
|
55
|
-
// Camera use cases
|
|
56
|
-
private var imageCapture: ImageCapture? = null
|
|
57
|
-
|
|
58
55
|
// Plugin context
|
|
59
56
|
private var lifecycleOwner: LifecycleOwner? = null
|
|
60
57
|
private var pluginDelegate: Plugin = plugin
|
|
@@ -94,8 +91,6 @@ class CameraView(plugin: Plugin) {
|
|
|
94
91
|
cameraController?.unbind()
|
|
95
92
|
|
|
96
93
|
try {
|
|
97
|
-
imageCapture = null
|
|
98
|
-
|
|
99
94
|
previewView?.let { view ->
|
|
100
95
|
try {
|
|
101
96
|
(webView.parent as? ViewGroup)?.removeView(view)
|
|
@@ -124,30 +119,46 @@ class CameraView(plugin: Plugin) {
|
|
|
124
119
|
}
|
|
125
120
|
|
|
126
121
|
/** Capture a photo with the current camera configuration */
|
|
122
|
+
@OptIn(ExperimentalZeroShutterLag::class)
|
|
127
123
|
fun capturePhoto(quality: Int, callback: (String?, Exception?) -> Unit) {
|
|
128
|
-
val
|
|
124
|
+
val startTime = System.currentTimeMillis()
|
|
125
|
+
val controller =
|
|
126
|
+
this.cameraController
|
|
127
|
+
?: run {
|
|
128
|
+
callback(null, Exception("Camera controller not initialized"))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
val preview = previewView
|
|
129
133
|
?: run {
|
|
130
|
-
callback(null, Exception("Camera
|
|
134
|
+
callback(null, Exception("Camera preview not initialized"))
|
|
131
135
|
return
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
mainHandler.post {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
val cameraInfo = controller.cameraInfo
|
|
140
|
+
val isFrontFacing = controller.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA
|
|
141
|
+
val sensorRotationDegrees = cameraInfo?.sensorRotationDegrees ?: 0
|
|
142
|
+
val displayRotationDegrees = preview.display?.rotation ?: Surface.ROTATION_0
|
|
143
|
+
val imageRotationDegrees = calculateImageRotationBasedOnDisplayRotation(
|
|
144
|
+
displayRotationDegrees,
|
|
145
|
+
sensorRotationDegrees,
|
|
146
|
+
isFrontFacing
|
|
147
|
+
)
|
|
140
148
|
|
|
149
|
+
try {
|
|
141
150
|
controller.takePicture(
|
|
142
|
-
outputOptions,
|
|
143
151
|
cameraExecutor,
|
|
144
|
-
object : ImageCapture.
|
|
145
|
-
override fun
|
|
146
|
-
|
|
152
|
+
object : ImageCapture.OnImageCapturedCallback() {
|
|
153
|
+
override fun onCaptureSuccess(image: ImageProxy) {
|
|
154
|
+
Log.d(
|
|
155
|
+
TAG,
|
|
156
|
+
"Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
|
|
157
|
+
)
|
|
158
|
+
handleCaptureSuccess(image, quality, imageRotationDegrees, callback)
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
override fun onError(exception: ImageCaptureException) {
|
|
150
|
-
tempFile.delete()
|
|
151
162
|
Log.e(TAG, "Error capturing image", exception)
|
|
152
163
|
callback(null, exception)
|
|
153
164
|
}
|
|
@@ -161,57 +172,25 @@ class CameraView(plugin: Plugin) {
|
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
/**
|
|
164
|
-
* Handles the
|
|
165
|
-
* and returns the Base64 encoded string as the callback result.
|
|
175
|
+
* Handles the successful capture of an image, converting it to a Base64 string
|
|
166
176
|
*/
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
fun handleCaptureSuccess(
|
|
178
|
+
image: ImageProxy,
|
|
169
179
|
quality: Int,
|
|
180
|
+
rotationDegrees: Int,
|
|
170
181
|
callback: (String?, Exception?) -> Unit
|
|
171
182
|
) {
|
|
172
183
|
val startTime = System.currentTimeMillis()
|
|
173
184
|
try {
|
|
174
|
-
|
|
175
|
-
val base64String =
|
|
176
|
-
|
|
177
|
-
Log.d(TAG, "Encoding original JPEG (quality 100)")
|
|
178
|
-
Base64.encodeToString(jpegBytes, Base64.NO_WRAP)
|
|
179
|
-
} else {
|
|
180
|
-
// Otherwise, re-encode the JPEG with the specified quality
|
|
181
|
-
// which is a little bit more expensive
|
|
182
|
-
Log.d(TAG, "Re-encoding JPEG with quality $quality")
|
|
183
|
-
val originalExif = ExifInterface(tempFile.absolutePath)
|
|
184
|
-
val orientation = originalExif.getAttributeInt(
|
|
185
|
-
ExifInterface.TAG_ORIENTATION,
|
|
186
|
-
ExifInterface.ORIENTATION_UNDEFINED
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
val bitmap =
|
|
190
|
-
android.graphics.BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
|
|
191
|
-
val compressedFile =
|
|
192
|
-
File.createTempFile(UUID.randomUUID().toString(), ".jpg", context.cacheDir)
|
|
193
|
-
val outputStream = compressedFile.outputStream()
|
|
194
|
-
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
|
195
|
-
outputStream.close()
|
|
196
|
-
|
|
197
|
-
val newExif = ExifInterface(compressedFile.absolutePath)
|
|
198
|
-
newExif.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
|
|
199
|
-
newExif.saveAttributes()
|
|
200
|
-
|
|
201
|
-
val compressedBytes = compressedFile.readBytes()
|
|
202
|
-
compressedFile.delete()
|
|
203
|
-
Base64.encodeToString(compressedBytes, Base64.NO_WRAP)
|
|
204
|
-
}
|
|
205
|
-
val endTime = System.currentTimeMillis()
|
|
206
|
-
Log.d(
|
|
207
|
-
TAG,
|
|
208
|
-
"Image processing took ${endTime - startTime} ms (quality: ${quality ?: 100})"
|
|
209
|
-
)
|
|
210
|
-
tempFile.delete()
|
|
185
|
+
// Turn the image into a Base64 encoded string and apply rotation if necessary
|
|
186
|
+
val base64String = imageProxyToBase64(image, quality, rotationDegrees)
|
|
187
|
+
Log.d(TAG, "Image processed to Base64 in ${System.currentTimeMillis() - startTime}ms")
|
|
211
188
|
callback(base64String, null)
|
|
212
189
|
} catch (e: Exception) {
|
|
213
|
-
|
|
190
|
+
Log.e(TAG, "Error processing captured image", e)
|
|
214
191
|
callback(null, e)
|
|
192
|
+
} finally {
|
|
193
|
+
image.close()
|
|
215
194
|
}
|
|
216
195
|
}
|
|
217
196
|
|
|
@@ -219,7 +198,7 @@ class CameraView(plugin: Plugin) {
|
|
|
219
198
|
* Capture a frame directly from the preview without using the full photo pipeline which is
|
|
220
199
|
* faster but has lower quality.
|
|
221
200
|
*/
|
|
222
|
-
fun captureSampleFromPreview(quality: Int
|
|
201
|
+
fun captureSampleFromPreview(quality: Int, callback: (String?, Exception?) -> Unit) {
|
|
223
202
|
val previewView =
|
|
224
203
|
this.previewView
|
|
225
204
|
?: run {
|
|
@@ -238,7 +217,7 @@ class CameraView(plugin: Plugin) {
|
|
|
238
217
|
}
|
|
239
218
|
|
|
240
219
|
// Convert bitmap to Base64
|
|
241
|
-
bitmap.compress(Bitmap.CompressFormat.JPEG, quality
|
|
220
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
|
242
221
|
val byteArray = outputStream.toByteArray()
|
|
243
222
|
val base64String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
|
|
244
223
|
|
|
@@ -255,9 +234,9 @@ class CameraView(plugin: Plugin) {
|
|
|
255
234
|
/** Flip between front and back cameras */
|
|
256
235
|
fun flipCamera(callback: (Exception?) -> Unit) {
|
|
257
236
|
currentCameraSelector = when (currentCameraSelector) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
237
|
+
CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA
|
|
238
|
+
else -> CameraSelector.DEFAULT_FRONT_CAMERA
|
|
239
|
+
}
|
|
261
240
|
|
|
262
241
|
val controller =
|
|
263
242
|
this.cameraController
|
|
@@ -402,7 +381,6 @@ class CameraView(plugin: Plugin) {
|
|
|
402
381
|
|
|
403
382
|
// Clear references
|
|
404
383
|
lifecycleOwner = null
|
|
405
|
-
imageCapture = null
|
|
406
384
|
|
|
407
385
|
// Shutdown executor
|
|
408
386
|
if (!cameraExecutor.isShutdown) {
|
|
@@ -434,7 +412,7 @@ class CameraView(plugin: Plugin) {
|
|
|
434
412
|
(webView.parent as? ViewGroup)?.addView(previewView, 0)
|
|
435
413
|
}
|
|
436
414
|
|
|
437
|
-
@OptIn(ExperimentalCamera2Interop::class)
|
|
415
|
+
@OptIn(ExperimentalCamera2Interop::class, ExperimentalZeroShutterLag::class)
|
|
438
416
|
private fun initializeCamera(
|
|
439
417
|
context: Context,
|
|
440
418
|
lifecycleOwner: LifecycleOwner,
|
|
@@ -444,32 +422,35 @@ class CameraView(plugin: Plugin) {
|
|
|
444
422
|
setupPreviewView(context)
|
|
445
423
|
|
|
446
424
|
currentCameraSelector = if (config.position == "front") {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
425
|
+
CameraSelector.DEFAULT_FRONT_CAMERA
|
|
426
|
+
} else {
|
|
427
|
+
CameraSelector.DEFAULT_BACK_CAMERA
|
|
428
|
+
}
|
|
451
429
|
|
|
452
430
|
if (config.deviceId != null) {
|
|
453
431
|
// Prefer specific device id over position
|
|
454
432
|
currentCameraSelector = CameraSelector.Builder()
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
433
|
+
.addCameraFilter { cameraInfos ->
|
|
434
|
+
cameraInfos.filter { info ->
|
|
435
|
+
val cameraId = Camera2CameraInfo.from(info).cameraId
|
|
436
|
+
cameraId == config.deviceId
|
|
437
|
+
}
|
|
459
438
|
}
|
|
460
|
-
|
|
461
|
-
.build()
|
|
439
|
+
.build()
|
|
462
440
|
}
|
|
463
441
|
|
|
464
442
|
// Initialize camera controller
|
|
465
|
-
val controller =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
443
|
+
val controller =
|
|
444
|
+
LifecycleCameraController(context).apply {
|
|
445
|
+
cameraSelector = currentCameraSelector
|
|
446
|
+
imageCaptureMode = ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG
|
|
447
|
+
imageCaptureResolutionSelector =
|
|
448
|
+
ResolutionSelector.Builder()
|
|
449
|
+
.setAspectRatioStrategy(
|
|
450
|
+
AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
|
|
451
|
+
)
|
|
452
|
+
.build()
|
|
453
|
+
}
|
|
473
454
|
|
|
474
455
|
cameraController = controller
|
|
475
456
|
previewView?.controller = controller
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.michaelwolz.capacitorcameraview
|
|
2
2
|
|
|
3
3
|
import android.Manifest
|
|
4
|
+
import android.util.Log
|
|
4
5
|
import com.getcapacitor.JSArray
|
|
5
6
|
import com.getcapacitor.JSObject
|
|
6
7
|
import com.getcapacitor.PermissionState
|
|
@@ -73,6 +74,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
73
74
|
|
|
74
75
|
@PluginMethod
|
|
75
76
|
fun capture(call: PluginCall) {
|
|
77
|
+
val timeStart = System.currentTimeMillis();
|
|
76
78
|
val quality = call.getInt("quality") ?: 90
|
|
77
79
|
|
|
78
80
|
if (quality !in 0..100) {
|
|
@@ -86,12 +88,14 @@ class CameraViewPlugin : Plugin() {
|
|
|
86
88
|
photo == null -> call.reject("No image data")
|
|
87
89
|
else -> call.resolve(JSObject().apply { put("photo", photo) })
|
|
88
90
|
}
|
|
91
|
+
Log.d(TAG, "capture took ${System.currentTimeMillis() - timeStart}ms")
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
@PluginMethod
|
|
93
96
|
fun captureSample(call: PluginCall) {
|
|
94
|
-
val
|
|
97
|
+
val timeStart = System.currentTimeMillis();
|
|
98
|
+
val quality = call.getInt("quality") ?: 90
|
|
95
99
|
|
|
96
100
|
if (quality !in 0..100) {
|
|
97
101
|
call.reject("Quality must be between 0 and 100")
|
|
@@ -104,6 +108,7 @@ class CameraViewPlugin : Plugin() {
|
|
|
104
108
|
photo == null -> call.reject("No frame data")
|
|
105
109
|
else -> call.resolve(JSObject().apply { put("photo", photo) })
|
|
106
110
|
}
|
|
111
|
+
Log.d(TAG, "captureSample took ${System.currentTimeMillis() - timeStart}ms")
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -224,4 +229,8 @@ class CameraViewPlugin : Plugin() {
|
|
|
224
229
|
implementation.cleanup()
|
|
225
230
|
super.handleOnDestroy()
|
|
226
231
|
}
|
|
232
|
+
|
|
233
|
+
companion object {
|
|
234
|
+
private const val TAG = "CameraViewPlugin"
|
|
235
|
+
}
|
|
227
236
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
package com.michaelwolz.capacitorcameraview
|
|
2
2
|
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.Matrix
|
|
3
6
|
import android.graphics.Rect
|
|
7
|
+
import android.util.Base64
|
|
8
|
+
import android.view.Surface
|
|
4
9
|
import android.view.View
|
|
5
10
|
import android.view.ViewGroup.MarginLayoutParams
|
|
11
|
+
import androidx.camera.core.ImageProxy
|
|
6
12
|
import androidx.camera.view.PreviewView
|
|
7
13
|
import com.getcapacitor.PluginCall
|
|
8
14
|
import com.google.mlkit.vision.barcode.common.Barcode
|
|
9
15
|
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
|
|
10
16
|
import com.michaelwolz.capacitorcameraview.model.WebBoundingRect
|
|
17
|
+
import java.io.ByteArrayOutputStream
|
|
11
18
|
|
|
12
19
|
/** Converts a barcode format code to a readable string. */
|
|
13
20
|
fun getBarcodeFormatString(format: Int): String {
|
|
@@ -84,3 +91,74 @@ fun sessionConfigFromPluginCall(call: PluginCall): CameraSessionConfiguration {
|
|
|
84
91
|
zoomFactor = call.getFloat("zoomFactor") ?: 1.0f
|
|
85
92
|
)
|
|
86
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Calculates the image orientation based on the display rotation and sensor rotation degrees.
|
|
97
|
+
*
|
|
98
|
+
* This is because CameraController will set the image orientation based on the device's
|
|
99
|
+
* motion sensor, which may not match the display rotation and in this case not what we actually
|
|
100
|
+
* want.
|
|
101
|
+
*
|
|
102
|
+
* @param displayRotation The current display rotation (0, 1, 2, or 3).
|
|
103
|
+
* @param sensorRotationDegrees The rotation of the camera sensor in degrees (0, 90, 180, or 270).
|
|
104
|
+
* @param isFrontFacing Whether the camera is front-facing or back-facing.
|
|
105
|
+
* @return The calculated image orientation in degrees.
|
|
106
|
+
*/
|
|
107
|
+
fun calculateImageRotationBasedOnDisplayRotation(
|
|
108
|
+
displayRotation: Int,
|
|
109
|
+
sensorRotationDegrees: Int,
|
|
110
|
+
isFrontFacing: Boolean
|
|
111
|
+
): Int {
|
|
112
|
+
val surfaceRotationDegrees = when (displayRotation) {
|
|
113
|
+
Surface.ROTATION_0 -> 0
|
|
114
|
+
Surface.ROTATION_90 -> 90
|
|
115
|
+
Surface.ROTATION_180 -> 180
|
|
116
|
+
Surface.ROTATION_270 -> 270
|
|
117
|
+
else -> 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return if (isFrontFacing) {
|
|
121
|
+
(sensorRotationDegrees + surfaceRotationDegrees) % 360
|
|
122
|
+
} else {
|
|
123
|
+
(sensorRotationDegrees - surfaceRotationDegrees + 360) % 360
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Converts an ImageProxy to a Base64 encoded string and applies rotation if necessary.
|
|
129
|
+
*
|
|
130
|
+
* @param image The ImageProxy to convert.
|
|
131
|
+
* @param quality The JPEG compression quality (0-100).
|
|
132
|
+
* @param rotationDegrees The degrees to rotate the image (0, 90, 180, 270).
|
|
133
|
+
*/
|
|
134
|
+
fun imageProxyToBase64(image: ImageProxy, quality: Int, rotationDegrees: Int): String {
|
|
135
|
+
val buffer = image.planes[0].buffer
|
|
136
|
+
val bytes = ByteArray(buffer.remaining())
|
|
137
|
+
buffer.get(bytes)
|
|
138
|
+
|
|
139
|
+
var bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
140
|
+
?: throw IllegalArgumentException("Failed to decode image")
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Apply rotation if needed
|
|
144
|
+
if (rotationDegrees != 0) {
|
|
145
|
+
val matrix = Matrix().apply {
|
|
146
|
+
postRotate(rotationDegrees.toFloat())
|
|
147
|
+
}
|
|
148
|
+
val rotatedBitmap =
|
|
149
|
+
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
150
|
+
// Recycle the original bitmap to prevent memory leaks
|
|
151
|
+
bitmap.recycle()
|
|
152
|
+
bitmap = rotatedBitmap
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
val outputStream = ByteArrayOutputStream()
|
|
156
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
|
157
|
+
val byteArray = outputStream.toByteArray()
|
|
158
|
+
|
|
159
|
+
return Base64.encodeToString(byteArray, Base64.NO_WRAP)
|
|
160
|
+
} finally {
|
|
161
|
+
// Ensure bitmap is always recycled
|
|
162
|
+
bitmap.recycle()
|
|
163
|
+
}
|
|
164
|
+
}
|
package/package.json
CHANGED