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 { | dependencies { | ||||||
|     implementation 'com.github.SimpleMobileTools:Simple-Commons:2e9ca234a7' |     implementation 'com.github.SimpleMobileTools:Simple-Commons:2e9ca234a7' | ||||||
|     implementation 'androidx.documentfile:documentfile:1.0.1' |     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.lifecycle:lifecycle-runtime-ktx:2.5.1" | ||||||
|     implementation 'androidx.window:window:1.1.0-alpha03' |     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 mediaStorageDir = activity.config.savePhotosFolder | ||||||
|     private val contentResolver = activity.contentResolver |     private val contentResolver = activity.contentResolver | ||||||
|  |  | ||||||
|     fun getImageMediaOutput(): MediaOutput { |     fun getImageMediaOutput(): MediaOutput.ImageCaptureOutput { | ||||||
|         return try { |         return try { | ||||||
|             if (is3rdPartyIntent) { |             if (is3rdPartyIntent) { | ||||||
|                 if (outputUri != null) { |                 if (outputUri != null) { | ||||||
| @@ -56,7 +56,7 @@ class MediaOutputHelper( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun getVideoMediaOutput(): MediaOutput { |     fun getVideoMediaOutput(): MediaOutput.VideoCaptureOutput { | ||||||
|         return try { |         return try { | ||||||
|             if (is3rdPartyIntent) { |             if (is3rdPartyIntent) { | ||||||
|                 if (outputUri != null) { |                 if (outputUri != null) { | ||||||
|   | |||||||
| @@ -57,7 +57,6 @@ class CameraXPreview( | |||||||
|     private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() |     private val windowMetricsCalculator = WindowMetricsCalculator.getOrCreate() | ||||||
|     private val videoQualityManager = VideoQualityManager(activity) |     private val videoQualityManager = VideoQualityManager(activity) | ||||||
|     private val imageQualityManager = ImageQualityManager(activity) |     private val imageQualityManager = ImageQualityManager(activity) | ||||||
|     private val exifRemover = ExifRemover(contentResolver) |  | ||||||
|     private val mediaSizeStore = MediaSizeStore(config) |     private val mediaSizeStore = MediaSizeStore(config) | ||||||
|  |  | ||||||
|     private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { |     private val orientationEventListener = object : OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { | ||||||
| @@ -422,12 +421,14 @@ class CameraXPreview( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         val mediaOutput = mediaOutputHelper.getImageMediaOutput() |         val mediaOutput = mediaOutputHelper.getImageMediaOutput() | ||||||
|         if (mediaOutput is MediaOutput.BitmapOutput) { |  | ||||||
|         imageCapture.takePicture(mainExecutor, object : OnImageCapturedCallback() { |         imageCapture.takePicture(mainExecutor, object : OnImageCapturedCallback() { | ||||||
|             override fun onCaptureSuccess(image: ImageProxy) { |             override fun onCaptureSuccess(image: ImageProxy) { | ||||||
|  |                 playShutterSoundIfEnabled() | ||||||
|                 ensureBackgroundThread { |                 ensureBackgroundThread { | ||||||
|                     image.use { |                     image.use { | ||||||
|                             val bitmap = BitmapUtils.makeBitmap(image.toJpegByteArray()) |                         if (mediaOutput is MediaOutput.BitmapOutput) { | ||||||
|  |                             val imageBytes = ImageUtil.jpegImageToJpegByteArray(image) | ||||||
|  |                             val bitmap = BitmapUtils.makeBitmap(imageBytes) | ||||||
|                             activity.runOnUiThread { |                             activity.runOnUiThread { | ||||||
|                                 listener.toggleBottomButtons(false) |                                 listener.toggleBottomButtons(false) | ||||||
|                                 if (bitmap != null) { |                                 if (bitmap != null) { | ||||||
| @@ -436,35 +437,24 @@ class CameraXPreview( | |||||||
|                                     cameraErrorHandler.handleImageCaptureError(ERROR_CAPTURE_FAILED) |                                     cameraErrorHandler.handleImageCaptureError(ERROR_CAPTURE_FAILED) | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 override fun onError(exception: ImageCaptureException) { |  | ||||||
|                     handleImageCaptureError(exception) |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|                         } else { |                         } else { | ||||||
|             val outputOptionsBuilder = when (mediaOutput) { |                             ImageSaver.saveImage( | ||||||
|                 is MediaOutput.MediaStoreOutput -> OutputFileOptions.Builder(contentResolver, mediaOutput.contentUri, mediaOutput.contentValues) |                                 contentResolver = contentResolver, | ||||||
|                 is MediaOutput.OutputStreamMediaOutput -> OutputFileOptions.Builder(mediaOutput.outputStream) |                                 image = image, | ||||||
|                 else -> throw IllegalArgumentException("Unexpected option for image ") |                                 mediaOutput = mediaOutput, | ||||||
|             } |                                 metadata = metadata, | ||||||
|  |                                 jpegQuality = config.photoQuality, | ||||||
|             val outputOptions = outputOptionsBuilder.setMetadata(metadata).build() |                                 saveExifAttributes = config.savePhotoMetadata, | ||||||
|  |                                 onImageSaved = { savedUri -> | ||||||
|             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 { |                                     activity.runOnUiThread { | ||||||
|                                         listener.toggleBottomButtons(false) |                                         listener.toggleBottomButtons(false) | ||||||
|                                         listener.onMediaSaved(savedUri) |                                         listener.onMediaSaved(savedUri) | ||||||
|                                     } |                                     } | ||||||
|  |                                 }, | ||||||
|  |                                 onError = ::handleImageCaptureError | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -473,8 +463,6 @@ class CameraXPreview( | |||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|         playShutterSoundIfEnabled() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private fun handleImageCaptureError(exception: ImageCaptureException) { |     private fun handleImageCaptureError(exception: ImageCaptureException) { | ||||||
|         listener.toggleBottomButtons(false) |         listener.toggleBottomButtons(false) | ||||||
| @@ -518,7 +506,6 @@ class CameraXPreview( | |||||||
|                 MediaStoreOutputOptions.Builder(contentResolver, mediaOutput.contentUri).setContentValues(mediaOutput.contentValues).build() |                 MediaStoreOutputOptions.Builder(contentResolver, mediaOutput.contentUri).setContentValues(mediaOutput.contentValues).build() | ||||||
|                     .let { videoCapture.output.prepareRecording(activity, it) } |                     .let { videoCapture.output.prepareRecording(activity, it) } | ||||||
|             } |             } | ||||||
|             else -> throw IllegalArgumentException("Unexpected output option for video $mediaOutput") |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         currentRecording = recording.withAudioEnabled() |         currentRecording = recording.withAudioEnabled() | ||||||
| @@ -540,7 +527,7 @@ class CameraXPreview( | |||||||
|                         if (recordEvent.hasError()) { |                         if (recordEvent.hasError()) { | ||||||
|                             cameraErrorHandler.handleVideoRecordingError(recordEvent.error) |                             cameraErrorHandler.handleVideoRecordingError(recordEvent.error) | ||||||
|                         } else { |                         } else { | ||||||
|                             listener.onMediaSaved(mediaOutput.uri ?: recordEvent.outputResults.outputUri) |                             listener.onMediaSaved(recordEvent.outputResults.outputUri) | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -9,25 +9,28 @@ import java.io.OutputStream | |||||||
| sealed class MediaOutput( | sealed class MediaOutput( | ||||||
|     open val uri: Uri?, |     open val uri: Uri?, | ||||||
| ) { | ) { | ||||||
|  |     sealed interface ImageCaptureOutput | ||||||
|  |     sealed interface VideoCaptureOutput | ||||||
|  |  | ||||||
|     data class MediaStoreOutput( |     data class MediaStoreOutput( | ||||||
|         val contentValues: ContentValues, |         val contentValues: ContentValues, | ||||||
|         val contentUri: Uri, |         val contentUri: Uri, | ||||||
|     ) : MediaOutput(null) |     ) : MediaOutput(null), ImageCaptureOutput, VideoCaptureOutput | ||||||
|  |  | ||||||
|     data class OutputStreamMediaOutput( |     data class OutputStreamMediaOutput( | ||||||
|         val outputStream: OutputStream, |         val outputStream: OutputStream, | ||||||
|         override val uri: Uri, |         override val uri: Uri, | ||||||
|     ) : MediaOutput(uri) |     ) : MediaOutput(uri), ImageCaptureOutput | ||||||
|  |  | ||||||
|     data class FileDescriptorMediaOutput( |     data class FileDescriptorMediaOutput( | ||||||
|         val fileDescriptor: ParcelFileDescriptor, |         val fileDescriptor: ParcelFileDescriptor, | ||||||
|         override val uri: Uri, |         override val uri: Uri, | ||||||
|     ) : MediaOutput(uri) |     ) : MediaOutput(uri), VideoCaptureOutput | ||||||
|  |  | ||||||
|     data class FileMediaOutput( |     data class FileMediaOutput( | ||||||
|         val file: File, |         val file: File, | ||||||
|         override val uri: Uri, |         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