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?, callback: (String?, Exception?) -> Unit) {
128
- val controller =
129
- this.cameraController
130
- ?: run {
131
- callback(null, Exception("Camera controller not initialized"))
132
- return
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.OnImageCapturedCallback() {
140
- override fun onCaptureSuccess(image: ImageProxy) {
141
- try {
142
- val base64String = imageProxyToBase64(image, quality)
143
- callback(base64String, null)
144
- } catch (e: Exception) {
145
- Log.e(TAG, "Error processing captured image", e)
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?, callback: (String?, Exception?) -> Unit) {
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 ?: 90, outputStream)
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 quality = call.getInt("quality", 90)
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 quality = call.getInt("quality", 90)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-camera-view",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A Capacitor plugin for embedding a live camera feed directly into your app.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",