@@ -7,23 +7,24 @@ import android.hardware.camera2.CameraCharacteristics
77import android.hardware.camera2.CameraManager
88import android.util.Base64
99import android.util.Log
10+ import android.view.Surface
1011import android.view.ViewGroup
1112import android.webkit.WebView
1213import androidx.annotation.OptIn
1314import androidx.camera.camera2.interop.Camera2CameraInfo
1415import androidx.camera.camera2.interop.ExperimentalCamera2Interop
1516import androidx.camera.core.CameraSelector
17+ import androidx.camera.core.ExperimentalZeroShutterLag
1618import androidx.camera.core.ImageAnalysis
1719import androidx.camera.core.ImageCapture
1820import androidx.camera.core.ImageCaptureException
21+ import androidx.camera.core.ImageProxy
1922import androidx.camera.core.resolutionselector.AspectRatioStrategy
2023import androidx.camera.core.resolutionselector.ResolutionSelector
2124import androidx.camera.mlkit.vision.MlKitAnalyzer
22- import androidx.camera.view.CameraController
2325import androidx.camera.view.LifecycleCameraController
2426import androidx.camera.view.PreviewView
2527import androidx.core.content.ContextCompat
26- import androidx.exifinterface.media.ExifInterface
2728import androidx.lifecycle.LifecycleOwner
2829import com.getcapacitor.Plugin
2930import com.google.mlkit.vision.barcode.BarcodeScanner
@@ -35,8 +36,6 @@ import com.michaelwolz.capacitorcameraview.model.CameraDevice
3536import com.michaelwolz.capacitorcameraview.model.CameraSessionConfiguration
3637import com.michaelwolz.capacitorcameraview.model.ZoomFactors
3738import java.io.ByteArrayOutputStream
38- import java.io.File
39- import java.util.UUID
4039import java.util.concurrent.ExecutorService
4140import java.util.concurrent.Executors
4241
@@ -53,9 +52,6 @@ class CameraView(plugin: Plugin) {
5352 private var currentCameraSelector = CameraSelector .DEFAULT_BACK_CAMERA
5453 private var currentFlashMode: Int = ImageCapture .FLASH_MODE_OFF
5554
56- // Camera use cases
57- private var imageCapture: ImageCapture ? = null
58-
5955 // Plugin context
6056 private var lifecycleOwner: LifecycleOwner ? = null
6157 private var pluginDelegate: Plugin = plugin
@@ -95,8 +91,6 @@ class CameraView(plugin: Plugin) {
9591 cameraController?.unbind()
9692
9793 try {
98- imageCapture = null
99-
10094 previewView?.let { view ->
10195 try {
10296 (webView.parent as ? ViewGroup )?.removeView(view)
@@ -125,35 +119,46 @@ class CameraView(plugin: Plugin) {
125119 }
126120
127121 /* * Capture a photo with the current camera configuration */
122+ @OptIn(ExperimentalZeroShutterLag ::class )
128123 fun capturePhoto (quality : Int , callback : (String? , Exception ? ) -> Unit ) {
129- val timeStart = System .currentTimeMillis()
130- val controller = this .cameraController
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
131133 ? : run {
132- callback(null , Exception (" Camera controller not initialized" ))
134+ callback(null , Exception (" Camera preview not initialized" ))
133135 return
134136 }
135137
136138 mainHandler.post {
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()
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+ )
142148
149+ try {
143150 controller.takePicture(
144- outputOptions,
145151 cameraExecutor,
146- object : ImageCapture .OnImageSavedCallback {
147- override fun onImageSaved ( outputFileResults : ImageCapture . OutputFileResults ) {
152+ object : ImageCapture .OnImageCapturedCallback () {
153+ override fun onCaptureSuccess ( image : ImageProxy ) {
148154 Log .d(
149155 TAG ,
150- " Image stored to temp file in ${System .currentTimeMillis() - timeStart} ms"
156+ " Image captured successfully in ${System .currentTimeMillis() - startTime} ms"
151157 )
152- handleImageSaved(tempFile , quality, callback)
158+ handleCaptureSuccess(image , quality, imageRotationDegrees , callback)
153159 }
154160
155161 override fun onError (exception : ImageCaptureException ) {
156- tempFile.delete()
157162 Log .e(TAG , " Error capturing image" , exception)
158163 callback(null , exception)
159164 }
@@ -167,58 +172,25 @@ class CameraView(plugin: Plugin) {
167172 }
168173
169174 /* *
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.
175+ * Handles the successful capture of an image, converting it to a Base64 string
172176 */
173- private fun handleImageSaved (
174- tempFile : File ,
177+ fun handleCaptureSuccess (
178+ image : ImageProxy ,
175179 quality : Int ,
180+ rotationDegrees : Int ,
176181 callback : (String? , Exception ? ) -> Unit
177182 ) {
178183 val startTime = System .currentTimeMillis()
179184 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()
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" )
218188 callback(base64String, null )
219189 } catch (e: Exception ) {
220- tempFile.delete( )
190+ Log .e( TAG , " Error processing captured image " , e )
221191 callback(null , e)
192+ } finally {
193+ image.close()
222194 }
223195 }
224196
@@ -262,9 +234,9 @@ class CameraView(plugin: Plugin) {
262234 /* * Flip between front and back cameras */
263235 fun flipCamera (callback : (Exception ? ) -> Unit ) {
264236 currentCameraSelector = when (currentCameraSelector) {
265- CameraSelector .DEFAULT_FRONT_CAMERA -> CameraSelector .DEFAULT_BACK_CAMERA
266- else -> CameraSelector .DEFAULT_FRONT_CAMERA
267- }
237+ CameraSelector .DEFAULT_FRONT_CAMERA -> CameraSelector .DEFAULT_BACK_CAMERA
238+ else -> CameraSelector .DEFAULT_FRONT_CAMERA
239+ }
268240
269241 val controller =
270242 this .cameraController
@@ -409,7 +381,6 @@ class CameraView(plugin: Plugin) {
409381
410382 // Clear references
411383 lifecycleOwner = null
412- imageCapture = null
413384
414385 // Shutdown executor
415386 if (! cameraExecutor.isShutdown) {
@@ -441,7 +412,7 @@ class CameraView(plugin: Plugin) {
441412 (webView.parent as ? ViewGroup )?.addView(previewView, 0 )
442413 }
443414
444- @OptIn(ExperimentalCamera2Interop ::class )
415+ @OptIn(ExperimentalCamera2Interop ::class , ExperimentalZeroShutterLag :: class )
445416 private fun initializeCamera (
446417 context : Context ,
447418 lifecycleOwner : LifecycleOwner ,
@@ -451,33 +422,35 @@ class CameraView(plugin: Plugin) {
451422 setupPreviewView(context)
452423
453424 currentCameraSelector = if (config.position == " front" ) {
454- CameraSelector .DEFAULT_FRONT_CAMERA
455- } else {
456- CameraSelector .DEFAULT_BACK_CAMERA
457- }
425+ CameraSelector .DEFAULT_FRONT_CAMERA
426+ } else {
427+ CameraSelector .DEFAULT_BACK_CAMERA
428+ }
458429
459430 if (config.deviceId != null ) {
460431 // Prefer specific device id over position
461432 currentCameraSelector = CameraSelector .Builder ()
462- .addCameraFilter { cameraInfos ->
463- cameraInfos.filter { info ->
464- val cameraId = Camera2CameraInfo .from(info).cameraId
465- cameraId == config.deviceId
433+ .addCameraFilter { cameraInfos ->
434+ cameraInfos.filter { info ->
435+ val cameraId = Camera2CameraInfo .from(info).cameraId
436+ cameraId == config.deviceId
437+ }
466438 }
467- }
468- .build()
439+ .build()
469440 }
470441
471442 // Initialize camera controller
472- val controller = LifecycleCameraController (context).apply {
473- cameraSelector = currentCameraSelector
474- imageCapture = ImageCapture .Builder ()
475- .setCaptureMode(ImageCapture .CAPTURE_MODE_MINIMIZE_LATENCY )
476- .build()
477- imageCaptureResolutionSelector = ResolutionSelector .Builder ()
478- .setAspectRatioStrategy(AspectRatioStrategy .RATIO_16_9_FALLBACK_AUTO_STRATEGY )
479- .build()
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+ }
481454
482455 cameraController = controller
483456 previewView?.controller = controller
0 commit comments