capacitor-camera-view 1.0.2 → 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,23 +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
|
-
import androidx.camera.view.CameraController
|
|
23
25
|
import androidx.camera.view.LifecycleCameraController
|
|
24
26
|
import androidx.camera.view.PreviewView
|
|
25
27
|
import androidx.core.content.ContextCompat
|
|
26
|
-
import androidx.exifinterface.media.ExifInterface
|
|
27
28
|
import androidx.lifecycle.LifecycleOwner
|
|
28
29
|
import com.getcapacitor.Plugin
|
|
29
30
|
import com.google.mlkit.vision.barcode.BarcodeScanner
|
|
@@ -35,8 +36,6 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
|
|
|
35
36
|
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
|
|
36
37
|
import com.michaelwolz.capacitorcameraview.model.ZoomFactors
|
|
37
38
|
import java.io.ByteArrayOutputStream
|
|
38
|
-
import java.io.File
|
|
39
|
-
import java.util.UUID
|
|
40
39
|
import java.util.concurrent.ExecutorService
|
|
41
40
|
import java.util.concurrent.Executors
|
|
42
41
|
|
|
@@ -53,9 +52,6 @@ class CameraView(plugin: Plugin) {
|
|
|
53
52
|
private var currentCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
54
53
|
private var currentFlashMode: Int = ImageCapture.FLASH_MODE_OFF
|
|
55
54
|
|
|
56
|
-
// Camera use cases
|
|
57
|
-
private var imageCapture: ImageCapture? = null
|
|
58
|
-
|
|
59
55
|
// Plugin context
|
|
60
56
|
private var lifecycleOwner: LifecycleOwner? = null
|
|
61
57
|
private var pluginDelegate: Plugin = plugin
|
|
@@ -95,8 +91,6 @@ class CameraView(plugin: Plugin) {
|
|
|
95
91
|
cameraController?.unbind()
|
|
96
92
|
|
|
97
93
|
try {
|
|
98
|
-
imageCapture = null
|
|
99
|
-
|
|
100
94
|
previewView?.let { view ->
|
|
101
95
|
try {
|
|
102
96
|
(webView.parent as? ViewGroup)?.removeView(view)
|
|
@@ -125,35 +119,46 @@ class CameraView(plugin: Plugin) {
|
|
|
125
119
|
}
|
|
126
120
|
|
|
127
121
|
/** Capture a photo with the current camera configuration */
|
|
122
|
+
@OptIn(ExperimentalZeroShutterLag::class)
|
|
128
123
|
fun capturePhoto(quality: Int, callback: (String?, Exception?) -> Unit) {
|
|
129
|
-
val
|
|
130
|
-
val controller =
|
|
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
|
|
131
133
|
?: run {
|
|
132
|
-
callback(null, Exception("Camera
|
|
134
|
+
callback(null, Exception("Camera preview not initialized"))
|
|
133
135
|
return
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
mainHandler.post {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
)
|
|
142
148
|
|
|
149
|
+
try {
|
|
143
150
|
controller.takePicture(
|
|
144
|
-
outputOptions,
|
|
145
151
|
cameraExecutor,
|
|
146
|
-
object : ImageCapture.
|
|
147
|
-
override fun
|
|
152
|
+
object : ImageCapture.OnImageCapturedCallback() {
|
|
153
|
+
override fun onCaptureSuccess(image: ImageProxy) {
|
|
148
154
|
Log.d(
|
|
149
155
|
TAG,
|
|
150
|
-
"Image
|
|
156
|
+
"Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
|
|
151
157
|
)
|
|
152
|
-
|
|
158
|
+
handleCaptureSuccess(image, quality, imageRotationDegrees, callback)
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
override fun onError(exception: ImageCaptureException) {
|
|
156
|
-
tempFile.delete()
|
|
157
162
|
Log.e(TAG, "Error capturing image", exception)
|
|
158
163
|
callback(null, exception)
|
|
159
164
|
}
|
|
@@ -167,58 +172,25 @@ class CameraView(plugin: Plugin) {
|
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
/**
|
|
170
|
-
* Handles the
|
|
171
|
-
* and returns the Base64 encoded string as the callback result.
|
|
175
|
+
* Handles the successful capture of an image, converting it to a Base64 string
|
|
172
176
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
fun handleCaptureSuccess(
|
|
178
|
+
image: ImageProxy,
|
|
175
179
|
quality: Int,
|
|
180
|
+
rotationDegrees: Int,
|
|
176
181
|
callback: (String?, Exception?) -> Unit
|
|
177
182
|
) {
|
|
178
183
|
val startTime = System.currentTimeMillis()
|
|
179
184
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// If quality is 100, return the original JPEG without re-encoding
|
|
184
|
-
Log.d(TAG, "Encoding original JPEG with quality 100")
|
|
185
|
-
Base64.encodeToString(jpegBytes, Base64.NO_WRAP)
|
|
186
|
-
} else {
|
|
187
|
-
// Otherwise, re-encode the JPEG with the specified quality
|
|
188
|
-
// which is a little bit more expensive
|
|
189
|
-
Log.d(TAG, "Re-encoding JPEG with quality $quality")
|
|
190
|
-
val originalExif = ExifInterface(tempFile.absolutePath)
|
|
191
|
-
val orientation = originalExif.getAttributeInt(
|
|
192
|
-
ExifInterface.TAG_ORIENTATION,
|
|
193
|
-
ExifInterface.ORIENTATION_UNDEFINED
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
val bitmap =
|
|
197
|
-
android.graphics.BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
|
|
198
|
-
val compressedFile =
|
|
199
|
-
File.createTempFile(UUID.randomUUID().toString(), ".jpg", context.cacheDir)
|
|
200
|
-
val outputStream = compressedFile.outputStream()
|
|
201
|
-
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
|
202
|
-
outputStream.close()
|
|
203
|
-
|
|
204
|
-
val newExif = ExifInterface(compressedFile.absolutePath)
|
|
205
|
-
newExif.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
|
|
206
|
-
newExif.saveAttributes()
|
|
207
|
-
|
|
208
|
-
val compressedBytes = compressedFile.readBytes()
|
|
209
|
-
compressedFile.delete()
|
|
210
|
-
Log.d(TAG, "Re-encoded image size: ${compressedBytes.size} bytes")
|
|
211
|
-
Base64.encodeToString(compressedBytes, Base64.NO_WRAP)
|
|
212
|
-
}
|
|
213
|
-
Log.d(
|
|
214
|
-
TAG,
|
|
215
|
-
"Image processing took ${System.currentTimeMillis() - startTime} ms (quality: $quality)"
|
|
216
|
-
)
|
|
217
|
-
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")
|
|
218
188
|
callback(base64String, null)
|
|
219
189
|
} catch (e: Exception) {
|
|
220
|
-
|
|
190
|
+
Log.e(TAG, "Error processing captured image", e)
|
|
221
191
|
callback(null, e)
|
|
192
|
+
} finally {
|
|
193
|
+
image.close()
|
|
222
194
|
}
|
|
223
195
|
}
|
|
224
196
|
|
|
@@ -262,9 +234,9 @@ class CameraView(plugin: Plugin) {
|
|
|
262
234
|
/** Flip between front and back cameras */
|
|
263
235
|
fun flipCamera(callback: (Exception?) -> Unit) {
|
|
264
236
|
currentCameraSelector = when (currentCameraSelector) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
237
|
+
CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA
|
|
238
|
+
else -> CameraSelector.DEFAULT_FRONT_CAMERA
|
|
239
|
+
}
|
|
268
240
|
|
|
269
241
|
val controller =
|
|
270
242
|
this.cameraController
|
|
@@ -409,7 +381,6 @@ class CameraView(plugin: Plugin) {
|
|
|
409
381
|
|
|
410
382
|
// Clear references
|
|
411
383
|
lifecycleOwner = null
|
|
412
|
-
imageCapture = null
|
|
413
384
|
|
|
414
385
|
// Shutdown executor
|
|
415
386
|
if (!cameraExecutor.isShutdown) {
|
|
@@ -441,7 +412,7 @@ class CameraView(plugin: Plugin) {
|
|
|
441
412
|
(webView.parent as? ViewGroup)?.addView(previewView, 0)
|
|
442
413
|
}
|
|
443
414
|
|
|
444
|
-
@OptIn(ExperimentalCamera2Interop::class)
|
|
415
|
+
@OptIn(ExperimentalCamera2Interop::class, ExperimentalZeroShutterLag::class)
|
|
445
416
|
private fun initializeCamera(
|
|
446
417
|
context: Context,
|
|
447
418
|
lifecycleOwner: LifecycleOwner,
|
|
@@ -451,33 +422,35 @@ class CameraView(plugin: Plugin) {
|
|
|
451
422
|
setupPreviewView(context)
|
|
452
423
|
|
|
453
424
|
currentCameraSelector = if (config.position == "front") {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
425
|
+
CameraSelector.DEFAULT_FRONT_CAMERA
|
|
426
|
+
} else {
|
|
427
|
+
CameraSelector.DEFAULT_BACK_CAMERA
|
|
428
|
+
}
|
|
458
429
|
|
|
459
430
|
if (config.deviceId != null) {
|
|
460
431
|
// Prefer specific device id over position
|
|
461
432
|
currentCameraSelector = CameraSelector.Builder()
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
433
|
+
.addCameraFilter { cameraInfos ->
|
|
434
|
+
cameraInfos.filter { info ->
|
|
435
|
+
val cameraId = Camera2CameraInfo.from(info).cameraId
|
|
436
|
+
cameraId == config.deviceId
|
|
437
|
+
}
|
|
466
438
|
}
|
|
467
|
-
|
|
468
|
-
.build()
|
|
439
|
+
.build()
|
|
469
440
|
}
|
|
470
441
|
|
|
471
442
|
// Initialize camera controller
|
|
472
|
-
val controller =
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
}
|
|
481
454
|
|
|
482
455
|
cameraController = controller
|
|
483
456
|
previewView?.controller = controller
|
|
@@ -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