capacitor-camera-view 1.0.0 → 1.0.2
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.
|
@@ -3,8 +3,6 @@ package com.michaelwolz.capacitorcameraview
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.content.Context.CAMERA_SERVICE
|
|
5
5
|
import android.graphics.Bitmap
|
|
6
|
-
import android.graphics.BitmapFactory
|
|
7
|
-
import android.graphics.Matrix
|
|
8
6
|
import android.hardware.camera2.CameraCharacteristics
|
|
9
7
|
import android.hardware.camera2.CameraManager
|
|
10
8
|
import android.util.Base64
|
|
@@ -18,13 +16,14 @@ import androidx.camera.core.CameraSelector
|
|
|
18
16
|
import androidx.camera.core.ImageAnalysis
|
|
19
17
|
import androidx.camera.core.ImageCapture
|
|
20
18
|
import androidx.camera.core.ImageCaptureException
|
|
21
|
-
import androidx.camera.core.ImageProxy
|
|
22
19
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy
|
|
23
20
|
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
24
21
|
import androidx.camera.mlkit.vision.MlKitAnalyzer
|
|
22
|
+
import androidx.camera.view.CameraController
|
|
25
23
|
import androidx.camera.view.LifecycleCameraController
|
|
26
24
|
import androidx.camera.view.PreviewView
|
|
27
25
|
import androidx.core.content.ContextCompat
|
|
26
|
+
import androidx.exifinterface.media.ExifInterface
|
|
28
27
|
import androidx.lifecycle.LifecycleOwner
|
|
29
28
|
import com.getcapacitor.Plugin
|
|
30
29
|
import com.google.mlkit.vision.barcode.BarcodeScanner
|
|
@@ -36,6 +35,8 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
|
|
|
36
35
|
import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
|
|
37
36
|
import com.michaelwolz.capacitorcameraview.model.ZoomFactors
|
|
38
37
|
import java.io.ByteArrayOutputStream
|
|
38
|
+
import java.io.File
|
|
39
|
+
import java.util.UUID
|
|
39
40
|
import java.util.concurrent.ExecutorService
|
|
40
41
|
import java.util.concurrent.Executors
|
|
41
42
|
|
|
@@ -124,32 +125,35 @@ class CameraView(plugin: Plugin) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/** Capture a photo with the current camera configuration */
|
|
127
|
-
fun capturePhoto(quality: Int
|
|
128
|
-
val
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
fun capturePhoto(quality: Int, callback: (String?, Exception?) -> Unit) {
|
|
129
|
+
val timeStart = System.currentTimeMillis()
|
|
130
|
+
val controller = this.cameraController
|
|
131
|
+
?: run {
|
|
132
|
+
callback(null, Exception("Camera controller not initialized"))
|
|
133
|
+
return
|
|
134
|
+
}
|
|
134
135
|
|
|
135
136
|
mainHandler.post {
|
|
136
137
|
try {
|
|
138
|
+
// Create temporary file for the captured image
|
|
139
|
+
val tempFile =
|
|
140
|
+
File.createTempFile(UUID.randomUUID().toString(), ".jpg", context.cacheDir)
|
|
141
|
+
val outputOptions = ImageCapture.OutputFileOptions.Builder(tempFile).build()
|
|
142
|
+
|
|
137
143
|
controller.takePicture(
|
|
144
|
+
outputOptions,
|
|
138
145
|
cameraExecutor,
|
|
139
|
-
object : ImageCapture.
|
|
140
|
-
override fun
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
callback(null, e)
|
|
147
|
-
} finally {
|
|
148
|
-
image.close()
|
|
149
|
-
}
|
|
146
|
+
object : ImageCapture.OnImageSavedCallback {
|
|
147
|
+
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
|
148
|
+
Log.d(
|
|
149
|
+
TAG,
|
|
150
|
+
"Image stored to temp file in ${System.currentTimeMillis() - timeStart} ms"
|
|
151
|
+
)
|
|
152
|
+
handleImageSaved(tempFile, quality, callback)
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
override fun onError(exception: ImageCaptureException) {
|
|
156
|
+
tempFile.delete()
|
|
153
157
|
Log.e(TAG, "Error capturing image", exception)
|
|
154
158
|
callback(null, exception)
|
|
155
159
|
}
|
|
@@ -162,11 +166,67 @@ class CameraView(plugin: Plugin) {
|
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Handles the image saved callback, re-encodes the JPEG if quality is specified
|
|
171
|
+
* and returns the Base64 encoded string as the callback result.
|
|
172
|
+
*/
|
|
173
|
+
private fun handleImageSaved(
|
|
174
|
+
tempFile: File,
|
|
175
|
+
quality: Int,
|
|
176
|
+
callback: (String?, Exception?) -> Unit
|
|
177
|
+
) {
|
|
178
|
+
val startTime = System.currentTimeMillis()
|
|
179
|
+
try {
|
|
180
|
+
val jpegBytes = tempFile.readBytes()
|
|
181
|
+
Log.d(TAG, "Image size : ${jpegBytes.size} bytes")
|
|
182
|
+
val base64String = if (quality == 100) {
|
|
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()
|
|
218
|
+
callback(base64String, null)
|
|
219
|
+
} catch (e: Exception) {
|
|
220
|
+
tempFile.delete()
|
|
221
|
+
callback(null, e)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
165
225
|
/**
|
|
166
226
|
* Capture a frame directly from the preview without using the full photo pipeline which is
|
|
167
227
|
* faster but has lower quality.
|
|
168
228
|
*/
|
|
169
|
-
fun captureSampleFromPreview(quality: Int
|
|
229
|
+
fun captureSampleFromPreview(quality: Int, callback: (String?, Exception?) -> Unit) {
|
|
170
230
|
val previewView =
|
|
171
231
|
this.previewView
|
|
172
232
|
?: run {
|
|
@@ -185,7 +245,7 @@ class CameraView(plugin: Plugin) {
|
|
|
185
245
|
}
|
|
186
246
|
|
|
187
247
|
// Convert bitmap to Base64
|
|
188
|
-
bitmap.compress(Bitmap.CompressFormat.JPEG, quality
|
|
248
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
|
|
189
249
|
val byteArray = outputStream.toByteArray()
|
|
190
250
|
val base64String = Base64.encodeToString(byteArray, Base64.NO_WRAP)
|
|
191
251
|
|
|
@@ -411,10 +471,11 @@ class CameraView(plugin: Plugin) {
|
|
|
411
471
|
// Initialize camera controller
|
|
412
472
|
val controller = LifecycleCameraController(context).apply {
|
|
413
473
|
cameraSelector = currentCameraSelector
|
|
474
|
+
imageCapture = ImageCapture.Builder()
|
|
475
|
+
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
476
|
+
.build()
|
|
414
477
|
imageCaptureResolutionSelector = ResolutionSelector.Builder()
|
|
415
|
-
.setAspectRatioStrategy(
|
|
416
|
-
AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY
|
|
417
|
-
)
|
|
478
|
+
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
|
|
418
479
|
.build()
|
|
419
480
|
}
|
|
420
481
|
|
|
@@ -494,36 +555,6 @@ class CameraView(plugin: Plugin) {
|
|
|
494
555
|
lastBarcodeDetectionTime = now
|
|
495
556
|
}
|
|
496
557
|
|
|
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
558
|
private fun notifyBarcodeDetected(result: BarcodeDetectionResult) {
|
|
528
559
|
pluginDelegate.let { plugin ->
|
|
529
560
|
if (plugin is CameraViewPlugin) {
|
|
@@ -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,7 +74,8 @@ class CameraViewPlugin : Plugin() {
|
|
|
73
74
|
|
|
74
75
|
@PluginMethod
|
|
75
76
|
fun capture(call: PluginCall) {
|
|
76
|
-
val
|
|
77
|
+
val timeStart = System.currentTimeMillis();
|
|
78
|
+
val quality = call.getInt("quality") ?: 90
|
|
77
79
|
|
|
78
80
|
if (quality !in 0..100) {
|
|
79
81
|
call.reject("Quality must be between 0 and 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
|
}
|
package/package.json
CHANGED