capacitor-camera-view 1.0.2 → 1.0.4
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)
|
|
@@ -126,34 +120,44 @@ class CameraView(plugin: Plugin) {
|
|
|
126
120
|
|
|
127
121
|
/** Capture a photo with the current camera configuration */
|
|
128
122
|
fun capturePhoto(quality: Int, callback: (String?, Exception?) -> Unit) {
|
|
129
|
-
val
|
|
130
|
-
val controller =
|
|
123
|
+
val startTime = System.currentTimeMillis()
|
|
124
|
+
val controller =
|
|
125
|
+
this.cameraController
|
|
126
|
+
?: run {
|
|
127
|
+
callback(null, Exception("Camera controller not initialized"))
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
val preview = previewView
|
|
131
132
|
?: run {
|
|
132
|
-
callback(null, Exception("Camera
|
|
133
|
+
callback(null, Exception("Camera preview not initialized"))
|
|
133
134
|
return
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
mainHandler.post {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
val cameraInfo = controller.cameraInfo
|
|
139
|
+
val isFrontFacing = controller.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA
|
|
140
|
+
val sensorRotationDegrees = cameraInfo?.sensorRotationDegrees ?: 0
|
|
141
|
+
val displayRotationDegrees = preview.display?.rotation ?: Surface.ROTATION_0
|
|
142
|
+
val imageRotationDegrees = calculateImageRotationBasedOnDisplayRotation(
|
|
143
|
+
displayRotationDegrees,
|
|
144
|
+
sensorRotationDegrees,
|
|
145
|
+
isFrontFacing
|
|
146
|
+
)
|
|
142
147
|
|
|
148
|
+
try {
|
|
143
149
|
controller.takePicture(
|
|
144
|
-
outputOptions,
|
|
145
150
|
cameraExecutor,
|
|
146
|
-
object : ImageCapture.
|
|
147
|
-
override fun
|
|
151
|
+
object : ImageCapture.OnImageCapturedCallback() {
|
|
152
|
+
override fun onCaptureSuccess(image: ImageProxy) {
|
|
148
153
|
Log.d(
|
|
149
154
|
TAG,
|
|
150
|
-
"Image
|
|
155
|
+
"Image captured successfully in ${System.currentTimeMillis() - startTime}ms"
|
|
151
156
|
)
|
|
152
|
-
|
|
157
|
+
handleCaptureSuccess(image, quality, imageRotationDegrees, callback)
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
override fun onError(exception: ImageCaptureException) {
|
|
156
|
-
tempFile.delete()
|
|
157
161
|
Log.e(TAG, "Error capturing image", exception)
|
|
158
162
|
callback(null, exception)
|
|
159
163
|
}
|
|
@@ -167,58 +171,25 @@ class CameraView(plugin: Plugin) {
|
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
/**
|
|
170
|
-
* Handles the
|
|
171
|
-
* and returns the Base64 encoded string as the callback result.
|
|
174
|
+
* Handles the successful capture of an image, converting it to a Base64 string
|
|
172
175
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
fun handleCaptureSuccess(
|
|
177
|
+
image: ImageProxy,
|
|
175
178
|
quality: Int,
|
|
179
|
+
rotationDegrees: Int,
|
|
176
180
|
callback: (String?, Exception?) -> Unit
|
|
177
181
|
) {
|
|
178
182
|
val startTime = System.currentTimeMillis()
|
|
179
183
|
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()
|
|
184
|
+
// Turn the image into a Base64 encoded string and apply rotation if necessary
|
|
185
|
+
val base64String = imageProxyToBase64(image, quality, rotationDegrees)
|
|
186
|
+
Log.d(TAG, "Image processed to Base64 in ${System.currentTimeMillis() - startTime}ms")
|
|
218
187
|
callback(base64String, null)
|
|
219
188
|
} catch (e: Exception) {
|
|
220
|
-
|
|
189
|
+
Log.e(TAG, "Error processing captured image", e)
|
|
221
190
|
callback(null, e)
|
|
191
|
+
} finally {
|
|
192
|
+
image.close()
|
|
222
193
|
}
|
|
223
194
|
}
|
|
224
195
|
|
|
@@ -262,9 +233,9 @@ class CameraView(plugin: Plugin) {
|
|
|
262
233
|
/** Flip between front and back cameras */
|
|
263
234
|
fun flipCamera(callback: (Exception?) -> Unit) {
|
|
264
235
|
currentCameraSelector = when (currentCameraSelector) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
236
|
+
CameraSelector.DEFAULT_FRONT_CAMERA -> CameraSelector.DEFAULT_BACK_CAMERA
|
|
237
|
+
else -> CameraSelector.DEFAULT_FRONT_CAMERA
|
|
238
|
+
}
|
|
268
239
|
|
|
269
240
|
val controller =
|
|
270
241
|
this.cameraController
|
|
@@ -409,7 +380,6 @@ class CameraView(plugin: Plugin) {
|
|
|
409
380
|
|
|
410
381
|
// Clear references
|
|
411
382
|
lifecycleOwner = null
|
|
412
|
-
imageCapture = null
|
|
413
383
|
|
|
414
384
|
// Shutdown executor
|
|
415
385
|
if (!cameraExecutor.isShutdown) {
|
|
@@ -451,33 +421,35 @@ class CameraView(plugin: Plugin) {
|
|
|
451
421
|
setupPreviewView(context)
|
|
452
422
|
|
|
453
423
|
currentCameraSelector = if (config.position == "front") {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
424
|
+
CameraSelector.DEFAULT_FRONT_CAMERA
|
|
425
|
+
} else {
|
|
426
|
+
CameraSelector.DEFAULT_BACK_CAMERA
|
|
427
|
+
}
|
|
458
428
|
|
|
459
429
|
if (config.deviceId != null) {
|
|
460
430
|
// Prefer specific device id over position
|
|
461
431
|
currentCameraSelector = CameraSelector.Builder()
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
432
|
+
.addCameraFilter { cameraInfos ->
|
|
433
|
+
cameraInfos.filter { info ->
|
|
434
|
+
val cameraId = Camera2CameraInfo.from(info).cameraId
|
|
435
|
+
cameraId == config.deviceId
|
|
436
|
+
}
|
|
466
437
|
}
|
|
467
|
-
|
|
468
|
-
.build()
|
|
438
|
+
.build()
|
|
469
439
|
}
|
|
470
440
|
|
|
471
441
|
// Initialize camera controller
|
|
472
|
-
val controller =
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
442
|
+
val controller =
|
|
443
|
+
LifecycleCameraController(context).apply {
|
|
444
|
+
cameraSelector = currentCameraSelector
|
|
445
|
+
imageCaptureMode = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
|
|
446
|
+
imageCaptureResolutionSelector =
|
|
447
|
+
ResolutionSelector.Builder()
|
|
448
|
+
.setAspectRatioStrategy(
|
|
449
|
+
AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
|
|
450
|
+
)
|
|
451
|
+
.build()
|
|
452
|
+
}
|
|
481
453
|
|
|
482
454
|
cameraController = controller
|
|
483
455
|
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