mirror of
				https://github.com/SimpleMobileTools/Simple-Camera.git
				synced 2025-06-27 09:02:59 +02:00 
			
		
		
		
	Merge pull request #350 from KryptKode/feat/camera-x-fixes
FIX: properly play media sound on image capture
This commit is contained in:
		| @@ -65,7 +65,7 @@ android { | ||||
| dependencies { | ||||
|     implementation 'com.github.SimpleMobileTools:Simple-Commons:2e9ca234a7' | ||||
|     implementation 'androidx.documentfile:documentfile:1.0.1' | ||||
|     implementation "androidx.exifinterface:exifinterface:1.3.3" | ||||
|     implementation "androidx.exifinterface:exifinterface:1.3.4" | ||||
|     implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" | ||||
|     implementation 'androidx.window:window:1.1.0-alpha03' | ||||
|  | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| package com.simplemobiletools.camera.extensions | ||||
|  | ||||
| import androidx.camera.core.ImageProxy | ||||
|  | ||||
| // Only for JPEG format | ||||
| fun ImageProxy.toJpegByteArray(): ByteArray { | ||||
|     val buffer = planes.first().buffer | ||||
|     val jpegImageData = ByteArray(buffer.remaining()) | ||||
|     buffer[jpegImageData] | ||||
|     return jpegImageData | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| package com.simplemobiletools.camera.helpers | ||||
|  | ||||
| import android.content.ContentResolver | ||||
| import android.net.Uri | ||||
| import androidx.annotation.WorkerThread | ||||
| import androidx.exifinterface.media.ExifInterface | ||||
| import com.simplemobiletools.commons.extensions.removeValues | ||||
| import java.io.IOException | ||||
|  | ||||
| class ExifRemover(private val contentResolver: ContentResolver) { | ||||
|     companion object { | ||||
|         private const val MODE = "rw" | ||||
|     } | ||||
|  | ||||
|     @WorkerThread | ||||
|     fun removeExif(uri: Uri) { | ||||
|         try { | ||||
|             val fileDescriptor = contentResolver.openFileDescriptor(uri, MODE) | ||||
|             if (fileDescriptor != null) { | ||||
|                 val exifInterface = ExifInterface(fileDescriptor.fileDescriptor) | ||||
|                 exifInterface.removeValues() | ||||
|             } | ||||
|         } catch (e: IOException) { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,300 @@ | ||||
| package com.simplemobiletools.camera.helpers | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.ContentResolver | ||||
| import android.content.ContentValues | ||||
| import android.net.Uri | ||||
| import android.provider.MediaStore | ||||
| import androidx.camera.core.ImageCapture | ||||
| import androidx.camera.core.ImageCapture.Metadata | ||||
| import androidx.camera.core.ImageCaptureException | ||||
| import androidx.camera.core.ImageProxy | ||||
| import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability | ||||
| import androidx.exifinterface.media.ExifInterface | ||||
| import com.simplemobiletools.camera.helpers.ImageUtil.CodecFailedException | ||||
| import com.simplemobiletools.camera.helpers.ImageUtil.imageToJpegByteArray | ||||
| import com.simplemobiletools.camera.helpers.ImageUtil.jpegImageToJpegByteArray | ||||
| import com.simplemobiletools.camera.models.MediaOutput | ||||
| import com.simplemobiletools.commons.extensions.copyTo | ||||
| import com.simplemobiletools.commons.helpers.ensureBackgroundThread | ||||
| import com.simplemobiletools.commons.helpers.isQPlus | ||||
| import java.io.* | ||||
| import java.util.UUID | ||||
|  | ||||
| /** | ||||
|  * Inspired by | ||||
|  * @see androidx.camera.core.ImageSaver | ||||
|  * */ | ||||
| class ImageSaver private constructor( | ||||
|     private val contentResolver: ContentResolver, | ||||
|     private val image: ImageProxy, | ||||
|     private val mediaOutput: MediaOutput.ImageCaptureOutput, | ||||
|     private val metadata: Metadata, | ||||
|     private val jpegQuality: Int, | ||||
|     private val saveExifAttributes: Boolean, | ||||
|     private val onImageSaved: (Uri) -> Unit, | ||||
|     private val onError: (ImageCaptureException) -> Unit, | ||||
| ) { | ||||
|  | ||||
|     companion object { | ||||
|         private const val TEMP_FILE_PREFIX = "SimpleCamera" | ||||
|         private const val TEMP_FILE_SUFFIX = ".tmp" | ||||
|         private const val COPY_BUFFER_SIZE = 1024 | ||||
|         private const val PENDING = 1 | ||||
|         private const val NOT_PENDING = 0 | ||||
|  | ||||
|         fun saveImage( | ||||
|             contentResolver: ContentResolver, | ||||
|             image: ImageProxy, | ||||
|             mediaOutput: MediaOutput.ImageCaptureOutput, | ||||
|             metadata: Metadata, | ||||
|             jpegQuality: Int, | ||||
|             saveExifAttributes: Boolean, | ||||
|             onImageSaved: (Uri) -> Unit, | ||||
|             onError: (ImageCaptureException) -> Unit, | ||||
|         ) = ImageSaver( | ||||
|             contentResolver = contentResolver, | ||||
|             image = image, | ||||
|             mediaOutput = mediaOutput, | ||||
|             metadata = metadata, | ||||
|             jpegQuality = jpegQuality, | ||||
|             saveExifAttributes = saveExifAttributes, | ||||
|             onImageSaved = onImageSaved, | ||||
|             onError = onError, | ||||
|         ).saveImage() | ||||
|     } | ||||
|  | ||||
|     fun saveImage() { | ||||
|         ensureBackgroundThread { | ||||
|             // Save the image to a temp file first. This is necessary because ExifInterface only | ||||
|             // supports saving to File. | ||||
|             val tempFile = saveImageToTempFile() | ||||
|             if (tempFile != null) { | ||||
|                 copyTempFileToDestination(tempFile) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @SuppressLint("RestrictedApi") | ||||
|     private fun saveImageToTempFile(): File? { | ||||
|         var saveError: SaveError? = null | ||||
|         var errorMessage: String? = null | ||||
|         var exception: Exception? = null | ||||
|  | ||||
|         val tempFile = try { | ||||
|             if (mediaOutput is MediaOutput.FileMediaOutput) { | ||||
|                 // For saving to file, write to the target folder and rename for better performance. | ||||
|                 File( | ||||
|                     mediaOutput.file.parent, | ||||
|                     TEMP_FILE_PREFIX + UUID.randomUUID().toString() + TEMP_FILE_SUFFIX | ||||
|                 ) | ||||
|             } else { | ||||
|                 File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX) | ||||
|             } | ||||
|  | ||||
|         } catch (e: IOException) { | ||||
|             postError(SaveError.FILE_IO_FAILED, "Error saving temp file", e) | ||||
|             return null | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             val output = FileOutputStream(tempFile) | ||||
|             val byteArray: ByteArray = imageToJpegByteArray(image, jpegQuality) | ||||
|             output.write(byteArray) | ||||
|  | ||||
|             if (saveExifAttributes) { | ||||
|                 val exifInterface = ExifInterface(tempFile) | ||||
|                 val imageByteArray = jpegImageToJpegByteArray(image) | ||||
|                 val inputStream: InputStream = ByteArrayInputStream(imageByteArray) | ||||
|                 ExifInterface(inputStream).copyTo(exifInterface) | ||||
|  | ||||
|                 // Overwrite the original orientation if the quirk exists. | ||||
|                 if (!ExifRotationAvailability().shouldUseExifOrientation(image)) { | ||||
|                     exifInterface.rotate(image.imageInfo.rotationDegrees) | ||||
|                 } | ||||
|  | ||||
|                 if (metadata.isReversedHorizontal) { | ||||
|                     exifInterface.flipHorizontally() | ||||
|                 } | ||||
|                 if (metadata.isReversedVertical) { | ||||
|                     exifInterface.flipVertically() | ||||
|                 } | ||||
|                 if (metadata.location != null) { | ||||
|                     exifInterface.setGpsInfo(metadata.location) | ||||
|                 } | ||||
|  | ||||
|                 exifInterface.saveAttributes() | ||||
|             } | ||||
|         } catch (e: IOException) { | ||||
|             saveError = SaveError.FILE_IO_FAILED | ||||
|             errorMessage = "Failed to write temp file" | ||||
|             exception = e | ||||
|         } catch (e: IllegalArgumentException) { | ||||
|             saveError = SaveError.FILE_IO_FAILED | ||||
|             errorMessage = "Failed to write temp file" | ||||
|             exception = e | ||||
|         } catch (e: CodecFailedException) { | ||||
|             when (e.failureType) { | ||||
|                 CodecFailedException.FailureType.ENCODE_FAILED -> { | ||||
|                     saveError = SaveError.ENCODE_FAILED | ||||
|                     errorMessage = "Failed to encode Image" | ||||
|                 } | ||||
|                 CodecFailedException.FailureType.DECODE_FAILED -> { | ||||
|                     saveError = SaveError.CROP_FAILED | ||||
|                     errorMessage = "Failed to crop Image" | ||||
|                 } | ||||
|                 CodecFailedException.FailureType.UNKNOWN -> { | ||||
|                     saveError = SaveError.UNKNOWN | ||||
|                     errorMessage = "Failed to transcode Image" | ||||
|                 } | ||||
|             } | ||||
|             exception = e | ||||
|         } | ||||
|  | ||||
|         if (saveError != null) { | ||||
|             postError(saveError, errorMessage, exception) | ||||
|             tempFile.delete() | ||||
|             return null | ||||
|         } | ||||
|  | ||||
|         return tempFile | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Copy the temp file to user specified destination. | ||||
|      * | ||||
|      * | ||||
|      *  The temp file will be deleted afterwards. | ||||
|      */ | ||||
|     private fun copyTempFileToDestination(tempFile: File) { | ||||
|         var saveError: SaveError? = null | ||||
|         var errorMessage: String? = null | ||||
|         var exception: java.lang.Exception? = null | ||||
|         var outputUri: Uri? = null | ||||
|         try { | ||||
|             when (mediaOutput) { | ||||
|                 is MediaOutput.MediaStoreOutput -> { | ||||
|                     val values = mediaOutput.contentValues | ||||
|                     setContentValuePending(values, PENDING) | ||||
|                     outputUri = contentResolver.insert( | ||||
|                         mediaOutput.contentUri, values | ||||
|                     ) | ||||
|                     if (outputUri == null) { | ||||
|                         saveError = SaveError.FILE_IO_FAILED | ||||
|                         errorMessage = "Failed to insert URI." | ||||
|                     } else { | ||||
|                         if (!copyTempFileToUri(tempFile, outputUri)) { | ||||
|                             saveError = SaveError.FILE_IO_FAILED | ||||
|                             errorMessage = "Failed to save to URI." | ||||
|                         } | ||||
|                         setUriNotPending(outputUri) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 is MediaOutput.OutputStreamMediaOutput -> { | ||||
|                     copyTempFileToOutputStream(tempFile, mediaOutput.outputStream) | ||||
|                     outputUri = mediaOutput.uri | ||||
|                 } | ||||
|  | ||||
|                 is MediaOutput.FileMediaOutput -> { | ||||
|                     val targetFile: File = mediaOutput.file | ||||
|                     // Normally File#renameTo will overwrite the targetFile even if it already exists. | ||||
|                     // Just in case of unexpected behavior on certain platforms or devices, delete the | ||||
|                     // target file before renaming. | ||||
|                     if (targetFile.exists()) { | ||||
|                         targetFile.delete() | ||||
|                     } | ||||
|                     if (!tempFile.renameTo(targetFile)) { | ||||
|                         saveError = SaveError.FILE_IO_FAILED | ||||
|                         errorMessage = "Failed to rename file." | ||||
|                     } | ||||
|                     outputUri = Uri.fromFile(targetFile) | ||||
|                 } | ||||
|  | ||||
|                 MediaOutput.BitmapOutput -> throw UnsupportedOperationException("Bitmap output cannot be saved to disk") | ||||
|             } | ||||
|         } catch (e: IOException) { | ||||
|             saveError = SaveError.FILE_IO_FAILED | ||||
|             errorMessage = "Failed to write destination file." | ||||
|             exception = e | ||||
|         } catch (e: IllegalArgumentException) { | ||||
|             saveError = SaveError.FILE_IO_FAILED | ||||
|             errorMessage = "Failed to write destination file." | ||||
|             exception = e | ||||
|         } finally { | ||||
|             tempFile.delete() | ||||
|         } | ||||
|  | ||||
|         outputUri?.let(onImageSaved) ?: postError(saveError!!, errorMessage, exception) | ||||
|     } | ||||
|  | ||||
|     private fun postError(saveError: SaveError, errorMessage: String?, exception: Exception?) { | ||||
|         val imageCaptureError = if (saveError == SaveError.FILE_IO_FAILED) { | ||||
|             ImageCapture.ERROR_FILE_IO | ||||
|         } else { | ||||
|             ImageCapture.ERROR_UNKNOWN | ||||
|         } | ||||
|  | ||||
|         onError.invoke(ImageCaptureException(imageCaptureError, errorMessage!!, exception!!)) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Removes IS_PENDING flag during the writing to [Uri]. | ||||
|      */ | ||||
|     private fun setUriNotPending(outputUri: Uri) { | ||||
|         if (isQPlus()) { | ||||
|             val values = ContentValues() | ||||
|             setContentValuePending(values, NOT_PENDING) | ||||
|             contentResolver.update(outputUri, values, null, null) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** Set IS_PENDING flag to [ContentValues].  */ | ||||
|     private fun setContentValuePending(values: ContentValues, isPending: Int) { | ||||
|         if (isQPlus()) { | ||||
|             values.put(MediaStore.Images.Media.IS_PENDING, isPending) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Copies temp file to [Uri]. | ||||
|      * | ||||
|      * @return false if the [Uri] is not writable. | ||||
|      */ | ||||
|     @Throws(IOException::class) | ||||
|     private fun copyTempFileToUri(tempFile: File, uri: Uri): Boolean { | ||||
|         contentResolver.openOutputStream(uri).use { outputStream -> | ||||
|             if (outputStream == null) { | ||||
|                 // The URI is not writable. | ||||
|                 return false | ||||
|             } | ||||
|             copyTempFileToOutputStream(tempFile, outputStream) | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     @Throws(IOException::class) | ||||
|     private fun copyTempFileToOutputStream( | ||||
|         tempFile: File, outputStream: OutputStream | ||||
|     ) { | ||||
|         FileInputStream(tempFile).use { inputStream -> | ||||
|             val buf = ByteArray(COPY_BUFFER_SIZE) | ||||
|             var len: Int | ||||
|             while (inputStream.read(buf).also { len = it } > 0) { | ||||
|                 outputStream.write(buf, 0, len) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** Type of error that occurred during save  */ | ||||
|     enum class SaveError { | ||||
|         /** Failed to write to or close the file  */ | ||||
|         FILE_IO_FAILED, | ||||
|  | ||||
|         /** Failure when attempting to encode image  */ | ||||
|         ENCODE_FAILED, | ||||
|  | ||||
|         /** Failure when attempting to crop image  */ | ||||
|         CROP_FAILED, UNKNOWN | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,231 @@ | ||||
| package com.simplemobiletools.camera.helpers | ||||
|  | ||||
| import android.graphics.* | ||||
| import androidx.annotation.IntRange | ||||
| import androidx.camera.core.ImageProxy | ||||
| import java.io.ByteArrayOutputStream | ||||
| import java.io.IOException | ||||
|  | ||||
| /** | ||||
|  * Utility class for image related operations. | ||||
|  * @see androidx.camera.core.internal.utils.ImageUtil | ||||
|  */ | ||||
| object ImageUtil { | ||||
|  | ||||
|     @Throws(CodecFailedException::class) | ||||
|     fun imageToJpegByteArray(image: ImageProxy, jpegQuality: Int): ByteArray { | ||||
|         val shouldCropImage = shouldCropImage(image) | ||||
|         return when (image.format) { | ||||
|             ImageFormat.JPEG -> { | ||||
|                 if (!shouldCropImage) { | ||||
|                     // When cropping is unnecessary, the byte array doesn't need to be decoded and | ||||
|                     // re-encoded again. Therefore, jpegQuality is unnecessary in this case. | ||||
|                     jpegImageToJpegByteArray(image) | ||||
|                 } else { | ||||
|                     jpegImageToJpegByteArray(image, image.cropRect, jpegQuality) | ||||
|                 } | ||||
|             } | ||||
|             ImageFormat.YUV_420_888 -> { | ||||
|                 yuvImageToJpegByteArray(image, if (shouldCropImage) image.cropRect else null, jpegQuality) | ||||
|             } | ||||
|             else -> { | ||||
|                 // Unrecognized image format | ||||
|                 byteArrayOf() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts JPEG [ImageProxy] to JPEG byte array. | ||||
|      */ | ||||
|     fun jpegImageToJpegByteArray(image: ImageProxy): ByteArray { | ||||
|         require(image.format == ImageFormat.JPEG) { "Incorrect image format of the input image proxy: " + image.format } | ||||
|         val planes = image.planes | ||||
|         val buffer = planes[0].buffer | ||||
|         val data = ByteArray(buffer.capacity()) | ||||
|         buffer.rewind() | ||||
|         buffer[data] | ||||
|         return data | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts JPEG [ImageProxy] to JPEG byte array. The input JPEG image will be cropped | ||||
|      * by the specified crop rectangle and compressed by the specified quality value. | ||||
|      */ | ||||
|     @Throws(CodecFailedException::class) | ||||
|     private fun jpegImageToJpegByteArray( | ||||
|         image: ImageProxy, | ||||
|         cropRect: Rect, | ||||
|         @IntRange(from = 1, to = 100) jpegQuality: Int, | ||||
|     ): ByteArray { | ||||
|         require(image.format == ImageFormat.JPEG) { "Incorrect image format of the input image proxy: " + image.format } | ||||
|         var data = jpegImageToJpegByteArray(image) | ||||
|         data = cropJpegByteArray(data, cropRect, jpegQuality) | ||||
|         return data | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts YUV_420_888 [ImageProxy] to JPEG byte array. The input YUV_420_888 image | ||||
|      * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will | ||||
|      * be compressed by the specified quality value. | ||||
|      */ | ||||
|     @Throws(CodecFailedException::class) | ||||
|     private fun yuvImageToJpegByteArray( | ||||
|         image: ImageProxy, | ||||
|         cropRect: Rect?, | ||||
|         @IntRange(from = 1, to = 100) jpegQuality: Int, | ||||
|     ): ByteArray { | ||||
|         require(image.format == ImageFormat.YUV_420_888) { "Incorrect image format of the input image proxy: " + image.format } | ||||
|         return nv21ToJpeg( | ||||
|             yuv_420_888toNv21(image), | ||||
|             image.width, | ||||
|             image.height, | ||||
|             cropRect, | ||||
|             jpegQuality | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * [android.media.Image] to NV21 byte array. | ||||
|      */ | ||||
|     private fun yuv_420_888toNv21(image: ImageProxy): ByteArray { | ||||
|         val yPlane = image.planes[0] | ||||
|         val uPlane = image.planes[1] | ||||
|         val vPlane = image.planes[2] | ||||
|         val yBuffer = yPlane.buffer | ||||
|         val uBuffer = uPlane.buffer | ||||
|         val vBuffer = vPlane.buffer | ||||
|         yBuffer.rewind() | ||||
|         uBuffer.rewind() | ||||
|         vBuffer.rewind() | ||||
|         val ySize = yBuffer.remaining() | ||||
|         var position = 0 | ||||
|         val nv21 = ByteArray(ySize + image.width * image.height / 2) | ||||
|  | ||||
|         // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped. | ||||
|         for (row in 0 until image.height) { | ||||
|             yBuffer[nv21, position, image.width] | ||||
|             position += image.width | ||||
|             yBuffer.position( | ||||
|                 Math.min(ySize, yBuffer.position() - image.width + yPlane.rowStride) | ||||
|             ) | ||||
|         } | ||||
|         val chromaHeight = image.height / 2 | ||||
|         val chromaWidth = image.width / 2 | ||||
|         val vRowStride = vPlane.rowStride | ||||
|         val uRowStride = uPlane.rowStride | ||||
|         val vPixelStride = vPlane.pixelStride | ||||
|         val uPixelStride = uPlane.pixelStride | ||||
|  | ||||
|         // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to | ||||
|         // perform faster bulk gets from the byte buffers. | ||||
|         val vLineBuffer = ByteArray(vRowStride) | ||||
|         val uLineBuffer = ByteArray(uRowStride) | ||||
|         for (row in 0 until chromaHeight) { | ||||
|             vBuffer[vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())] | ||||
|             uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())] | ||||
|             var vLineBufferPosition = 0 | ||||
|             var uLineBufferPosition = 0 | ||||
|             for (col in 0 until chromaWidth) { | ||||
|                 nv21[position++] = vLineBuffer[vLineBufferPosition] | ||||
|                 nv21[position++] = uLineBuffer[uLineBufferPosition] | ||||
|                 vLineBufferPosition += vPixelStride | ||||
|                 uLineBufferPosition += uPixelStride | ||||
|             } | ||||
|         } | ||||
|         return nv21 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Crops JPEG byte array with given [android.graphics.Rect]. | ||||
|      */ | ||||
|     @Throws(CodecFailedException::class) | ||||
|     private fun cropJpegByteArray( | ||||
|         data: ByteArray, | ||||
|         cropRect: Rect, | ||||
|         @IntRange(from = 1, to = 100) jpegQuality: Int, | ||||
|     ): ByteArray { | ||||
|         val bitmap: Bitmap | ||||
|         try { | ||||
|             val decoder = BitmapRegionDecoder.newInstance( | ||||
|                 data, 0, data.size, | ||||
|                 false | ||||
|             ) | ||||
|             bitmap = decoder.decodeRegion(cropRect, BitmapFactory.Options()) | ||||
|             decoder.recycle() | ||||
|         } catch (e: IllegalArgumentException) { | ||||
|             throw CodecFailedException( | ||||
|                 "Decode byte array failed with illegal argument.$e", | ||||
|                 CodecFailedException.FailureType.DECODE_FAILED | ||||
|             ) | ||||
|         } catch (e: IOException) { | ||||
|             throw CodecFailedException( | ||||
|                 "Decode byte array failed.", | ||||
|                 CodecFailedException.FailureType.DECODE_FAILED | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         val out = ByteArrayOutputStream() | ||||
|         val success = bitmap.compress(Bitmap.CompressFormat.JPEG, jpegQuality, out) | ||||
|         if (!success) { | ||||
|             throw CodecFailedException( | ||||
|                 "Encode bitmap failed.", | ||||
|                 CodecFailedException.FailureType.ENCODE_FAILED | ||||
|             ) | ||||
|         } | ||||
|         bitmap.recycle() | ||||
|         return out.toByteArray() | ||||
|     } | ||||
|  | ||||
|     @Throws(CodecFailedException::class) | ||||
|     private fun nv21ToJpeg( | ||||
|         nv21: ByteArray, | ||||
|         width: Int, | ||||
|         height: Int, | ||||
|         cropRect: Rect?, | ||||
|         @IntRange(from = 1, to = 100) jpegQuality: Int, | ||||
|     ): ByteArray { | ||||
|         val out = ByteArrayOutputStream() | ||||
|         val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null) | ||||
|         val success = yuv.compressToJpeg( | ||||
|             cropRect ?: Rect(0, 0, width, height), | ||||
|             jpegQuality, out | ||||
|         ) | ||||
|         if (!success) { | ||||
|             throw CodecFailedException( | ||||
|                 "YuvImage failed to encode jpeg.", | ||||
|                 CodecFailedException.FailureType.ENCODE_FAILED | ||||
|             ) | ||||
|         } | ||||
|         return out.toByteArray() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks whether the image's crop rectangle is the same as the source image size. | ||||
|      */ | ||||
|     private fun shouldCropImage(image: ImageProxy): Boolean { | ||||
|         return shouldCropImage( | ||||
|             image.width, image.height, image.cropRect.width(), | ||||
|             image.cropRect.height() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Checks whether the image's crop rectangle is the same as the source image size. | ||||
|      */ | ||||
|     private fun shouldCropImage( | ||||
|         sourceWidth: Int, sourceHeight: Int, cropRectWidth: Int, | ||||
|         cropRectHeight: Int | ||||
|     ): Boolean { | ||||
|         return sourceWidth != cropRectWidth || sourceHeight != cropRectHeight | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Exception for error during transcoding image. | ||||
|      */ | ||||
|     class CodecFailedException internal constructor(message: String, val failureType: FailureType) : Exception(message) { | ||||
|         enum class FailureType { | ||||
|             ENCODE_FAILED, DECODE_FAILED, UNKNOWN | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -33,7 +33,7 @@ class MediaOutputHelper( | ||||
|     private val mediaStorageDir = activity.config.savePhotosFolder | ||||
|     private val contentResolver = activity.contentResolver | ||||
|  | ||||
|     fun getImageMediaOutput(): MediaOutput { | ||||
|     fun getImageMediaOutput(): MediaOutput.ImageCaptureOutput { | ||||
|         return try { | ||||
|             if (is3rdPartyIntent) { | ||||
|                 if (outputUri != null) { | ||||
| @@ -56,7 +56,7 @@ class MediaOutputHelper( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getVideoMediaOutput(): MediaOutput { | ||||
|     fun getVideoMediaOutput(): MediaOutput.VideoCaptureOutput { | ||||
|         return try { | ||||
|             if (is3rdPartyIntent) { | ||||
|                 if (outputUri != null) { | ||||
|   | ||||
| @@ -57,7 +57,6 @@ class CameraXPreview( | ||||
|     private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() | ||||
|     private val videoQualityManager = VideoQualityManager(activity) | ||||
|     private val imageQualityManager = ImageQualityManager(activity) | ||||
|     private val exifRemover = ExifRemover(contentResolver) | ||||
|     private val mediaSizeStore = MediaSizeStore(config) | ||||
|  | ||||
|     private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { | ||||
| @@ -422,12 +421,14 @@ class CameraXPreview( | ||||
|         } | ||||
|  | ||||
|         val mediaOutput = mediaOutputHelper.getImageMediaOutput() | ||||
|         if (mediaOutput is MediaOutput.BitmapOutput) { | ||||
|             imageCapture.takePicture(mainExecutor, object : OnImageCapturedCallback() { | ||||
|                 override fun onCaptureSuccess(image: ImageProxy) { | ||||
|                     ensureBackgroundThread { | ||||
|                         image.use { | ||||
|                             val bitmap = BitmapUtils.makeBitmap(image.toJpegByteArray()) | ||||
|         imageCapture.takePicture(mainExecutor, object : OnImageCapturedCallback() { | ||||
|             override fun onCaptureSuccess(image: ImageProxy) { | ||||
|                 playShutterSoundIfEnabled() | ||||
|                 ensureBackgroundThread { | ||||
|                     image.use { | ||||
|                         if (mediaOutput is MediaOutput.BitmapOutput) { | ||||
|                             val imageBytes = ImageUtil.jpegImageToJpegByteArray(image) | ||||
|                             val bitmap = BitmapUtils.makeBitmap(imageBytes) | ||||
|                             activity.runOnUiThread { | ||||
|                                 listener.toggleBottomButtons(false) | ||||
|                                 if (bitmap != null) { | ||||
| @@ -436,44 +437,31 @@ class CameraXPreview( | ||||
|                                     cameraErrorHandler.handleImageCaptureError(ERROR_CAPTURE_FAILED) | ||||
|                                 } | ||||
|                             } | ||||
|                         } else { | ||||
|                             ImageSaver.saveImage( | ||||
|                                 contentResolver = contentResolver, | ||||
|                                 image = image, | ||||
|                                 mediaOutput = mediaOutput, | ||||
|                                 metadata = metadata, | ||||
|                                 jpegQuality = config.photoQuality, | ||||
|                                 saveExifAttributes = config.savePhotoMetadata, | ||||
|                                 onImageSaved = { savedUri -> | ||||
|                                     activity.runOnUiThread { | ||||
|                                         listener.toggleBottomButtons(false) | ||||
|                                         listener.onMediaSaved(savedUri) | ||||
|                                     } | ||||
|                                 }, | ||||
|                                 onError = ::handleImageCaptureError | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 override fun onError(exception: ImageCaptureException) { | ||||
|                     handleImageCaptureError(exception) | ||||
|                 } | ||||
|             }) | ||||
|         } else { | ||||
|             val outputOptionsBuilder = when (mediaOutput) { | ||||
|                 is MediaOutput.MediaStoreOutput -> OutputFileOptions.Builder(contentResolver, mediaOutput.contentUri, mediaOutput.contentValues) | ||||
|                 is MediaOutput.OutputStreamMediaOutput -> OutputFileOptions.Builder(mediaOutput.outputStream) | ||||
|                 else -> throw IllegalArgumentException("Unexpected option for image ") | ||||
|             } | ||||
|  | ||||
|             val outputOptions = outputOptionsBuilder.setMetadata(metadata).build() | ||||
|  | ||||
|             imageCapture.takePicture(outputOptions, mainExecutor, object : OnImageSavedCallback { | ||||
|                 override fun onImageSaved(outputFileResults: OutputFileResults) { | ||||
|                     ensureBackgroundThread { | ||||
|                         val savedUri = mediaOutput.uri ?: outputFileResults.savedUri!! | ||||
|                         if (!config.savePhotoMetadata) { | ||||
|                             exifRemover.removeExif(savedUri) | ||||
|                         } | ||||
|  | ||||
|                         activity.runOnUiThread { | ||||
|                             listener.toggleBottomButtons(false) | ||||
|                             listener.onMediaSaved(savedUri) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 override fun onError(exception: ImageCaptureException) { | ||||
|                     handleImageCaptureError(exception) | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|         playShutterSoundIfEnabled() | ||||
|             override fun onError(exception: ImageCaptureException) { | ||||
|                 handleImageCaptureError(exception) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     private fun handleImageCaptureError(exception: ImageCaptureException) { | ||||
| @@ -518,7 +506,6 @@ class CameraXPreview( | ||||
|                 MediaStoreOutputOptions.Builder(contentResolver, mediaOutput.contentUri).setContentValues(mediaOutput.contentValues).build() | ||||
|                     .let { videoCapture.output.prepareRecording(activity, it) } | ||||
|             } | ||||
|             else -> throw IllegalArgumentException("Unexpected output option for video $mediaOutput") | ||||
|         } | ||||
|  | ||||
|         currentRecording = recording.withAudioEnabled() | ||||
| @@ -540,7 +527,7 @@ class CameraXPreview( | ||||
|                         if (recordEvent.hasError()) { | ||||
|                             cameraErrorHandler.handleVideoRecordingError(recordEvent.error) | ||||
|                         } else { | ||||
|                             listener.onMediaSaved(mediaOutput.uri ?: recordEvent.outputResults.outputUri) | ||||
|                             listener.onMediaSaved(recordEvent.outputResults.outputUri) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|   | ||||
| @@ -9,25 +9,28 @@ import java.io.OutputStream | ||||
| sealed class MediaOutput( | ||||
|     open val uri: Uri?, | ||||
| ) { | ||||
|     sealed interface ImageCaptureOutput | ||||
|     sealed interface VideoCaptureOutput | ||||
|  | ||||
|     data class MediaStoreOutput( | ||||
|         val contentValues: ContentValues, | ||||
|         val contentUri: Uri, | ||||
|     ) : MediaOutput(null) | ||||
|     ) : MediaOutput(null), ImageCaptureOutput, VideoCaptureOutput | ||||
|  | ||||
|     data class OutputStreamMediaOutput( | ||||
|         val outputStream: OutputStream, | ||||
|         override val uri: Uri, | ||||
|     ) : MediaOutput(uri) | ||||
|     ) : MediaOutput(uri), ImageCaptureOutput | ||||
|  | ||||
|     data class FileDescriptorMediaOutput( | ||||
|         val fileDescriptor: ParcelFileDescriptor, | ||||
|         override val uri: Uri, | ||||
|     ) : MediaOutput(uri) | ||||
|     ) : MediaOutput(uri), VideoCaptureOutput | ||||
|  | ||||
|     data class FileMediaOutput( | ||||
|         val file: File, | ||||
|         override val uri: Uri, | ||||
|     ) : MediaOutput(uri) | ||||
|     ) : MediaOutput(uri), VideoCaptureOutput, ImageCaptureOutput | ||||
|  | ||||
|     object BitmapOutput : MediaOutput(null) | ||||
|     object BitmapOutput : MediaOutput(null), ImageCaptureOutput | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user