Skip to content

Commit a6c8fe6

Browse files
committed
fix(android): much faster photo taking. However to make it fast it is prefered to set properties like pictureSize, captureMode ... on the view instead of as a arg of takePicture. If you do pass them to takePicture the whole configuration needs to be updated/set before taking the photo which is slow
1 parent 6dc57c6 commit a6c8fe6

File tree

10 files changed

+974
-1066
lines changed

10 files changed

+974
-1066
lines changed

packages/ui-cameraview/platforms/android/include.gradle

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import groovy.json.JsonSlurper
44
dependencies {
55
implementation(name:'widgets-release', ext:'aar')
66

7-
def camerax_version = "1.2.3"
8-
implementation "androidx.camera:camera-core:${camerax_version}"
9-
implementation "androidx.camera:camera-camera2:${camerax_version}"
10-
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
11-
implementation "androidx.camera:camera-video:${camerax_version}"
12-
13-
implementation "androidx.camera:camera-view:${camerax_version}"
14-
implementation "androidx.camera:camera-extensions:${camerax_version}"
7+
def cameraxVersion = project.hasProperty("cameraxVersion") ? project.cameraxVersion : "1.2.3"
8+
implementation "androidx.camera:camera-core:${cameraxVersion}"
9+
implementation "androidx.camera:camera-camera2:${cameraxVersion}"
10+
implementation "androidx.camera:camera-lifecycle:${cameraxVersion}"
11+
implementation "androidx.camera:camera-video:${cameraxVersion}"
12+
13+
implementation "androidx.camera:camera-view:${cameraxVersion}"
14+
implementation "androidx.camera:camera-extensions:${cameraxVersion}"
1515

1616
def androidxVersion = project.hasProperty("androidxVersion") ? project.androidxVersion : "1.6.0"
1717
implementation "androidx.core:core:$androidxVersion"
Lines changed: 33 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.nativescript.cameraview
22

3+
import android.annotation.SuppressLint
34
import android.annotation.TargetApi
45
import android.content.ContentResolver
56
import android.graphics.*
@@ -8,9 +9,11 @@ import android.net.Uri
89
import android.os.Build
910
import android.provider.MediaStore
1011
import android.util.Log
12+
import androidx.annotation.OptIn
1113
import androidx.annotation.RequiresApi
1214
import androidx.camera.core.ExperimentalGetImage
1315
import androidx.camera.core.ImageProxy
16+
import androidx.camera.core.internal.utils.ImageUtil
1417
import androidx.exifinterface.media.ExifInterface
1518
import java.io.ByteArrayOutputStream
1619
import java.io.IOException
@@ -39,58 +42,40 @@ import java.nio.ByteBuffer
3942
object BitmapUtils {
4043
private const val TAG = "BitmapUtils"
4144

42-
43-
/** Converts NV21 format byte buffer to bitmap. */
44-
fun getBitmap(data: ByteArray, metadata: FrameMetadata): Bitmap? {
45+
// based on androidx.camera.core.ImageSaver#imageToJpegByteArray(),
46+
// optimized to avoid extracting uncropped image twice and to close ImageProxy sooner
47+
@SuppressLint("RestrictedApi")
48+
@Throws(ImageUtil.CodecFailedException::class)
49+
public fun extractJpegBytes(image: ImageProxy, jpegQuality: Int): ByteArray {
4550
try {
46-
val image = YuvImage(
47-
data, ImageFormat.NV21, metadata.width, metadata.height, null
48-
)
49-
val stream = ByteArrayOutputStream()
50-
image.compressToJpeg(Rect(0, 0, metadata.width, metadata.height), 80, stream)
51-
val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size())
52-
stream.close()
53-
return rotateBitmap(bmp, metadata.rotation, false, false)
54-
} catch (e: Exception) {
55-
Log.e("VisionProcessorBase", "Error: " + e.message)
51+
var cropRect = if (ImageUtil.shouldCropImage(image)) image.cropRect else null
52+
val imageFormat = image.format
53+
54+
var origJpegBytes = if (imageFormat == ImageFormat.JPEG) {
55+
ImageUtil.jpegImageToJpegByteArray(image)
56+
} else if (imageFormat == ImageFormat.YUV_420_888) {
57+
ImageUtil.yuvImageToJpegByteArray(image, cropRect, jpegQuality)
58+
} else {
59+
throw IllegalStateException("unknown imageFormat $imageFormat")
60+
}
61+
return origJpegBytes!!;
62+
} finally {
63+
/*
64+
from javadoc of the Image class:
65+
Since Images are often directly produced or consumed by hardware components, they are
66+
a limited resource shared across the system, and should be closed as soon as
67+
they are no longer needed.
68+
*/
69+
image.close()
5670
}
57-
return null
5871
}
59-
60-
61-
/** Converts NV21 format byte buffer to bitmap. */
62-
fun getBitmap(data: ByteBuffer, metadata: FrameMetadata): Bitmap? {
63-
data.rewind()
64-
val imageInBuffer = ByteArray(data.limit())
65-
data[imageInBuffer, 0, imageInBuffer.size]
66-
try {
67-
val image = YuvImage(
68-
imageInBuffer, ImageFormat.NV21, metadata.width, metadata.height, null
69-
)
70-
val stream = ByteArrayOutputStream()
71-
image.compressToJpeg(Rect(0, 0, metadata.width, metadata.height), 80, stream)
72-
val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size())
73-
stream.close()
74-
return rotateBitmap(bmp, metadata.rotation, false, false)
75-
} catch (e: Exception) {
76-
Log.e("VisionProcessorBase", "Error: " + e.message)
77-
}
78-
return null
72+
@OptIn(ExperimentalGetImage::class) public fun byteArrayFromProxy(image: ImageProxy, jpegQuality: Int): ByteArray? {
73+
return extractJpegBytes(image,jpegQuality);
7974
}
80-
81-
/** Converts a YUV_420_888 image from CameraX API to a bitmap. */
82-
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
83-
@ExperimentalGetImage
84-
fun getBitmap(image: ImageProxy): Bitmap? {
85-
val frameMetadata = FrameMetadata.Builder()
86-
.setWidth(image.width)
87-
.setHeight(image.height)
88-
.setRotation(image.imageInfo.rotationDegrees)
89-
.build()
90-
val nv21Buffer = yuv420ThreePlanesToNV21(
91-
image.image!!.planes, image.width, image.height
92-
)
93-
return getBitmap(nv21Buffer, frameMetadata)
75+
@OptIn(ExperimentalGetImage::class) public fun getBitmap(image: ImageProxy, jpegQuality: Int): Bitmap {
76+
val byteArray = extractJpegBytes(image, jpegQuality);
77+
var bm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size);
78+
return bm;
9479
}
9580

9681
/** Rotates a bitmap if it is converted from a bytebuffer. */
@@ -169,113 +154,4 @@ object BitmapUtils {
169154
}
170155
return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
171156
}
172-
173-
/**
174-
* Converts YUV_420_888 to NV21 bytebuffer.
175-
*
176-
*
177-
* The NV21 format consists of a single byte array containing the Y, U and V values. For an
178-
* image of size S, the first S positions of the array contain all the Y values. The remaining
179-
* positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both
180-
* dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain
181-
* S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU
182-
*
183-
*
184-
* YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled
185-
* by a factor of 2 in both dimensions. [Image.getPlanes] returns an array with the Y, U and
186-
* V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into
187-
* the first part of the NV21 array. The U and V planes may already have the representation in the
188-
* NV21 format. This happens if the planes share the same buffer, the V buffer is one position
189-
* before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy
190-
* them to the NV21 array.
191-
*/
192-
@RequiresApi(Build.VERSION_CODES.KITKAT)
193-
private fun yuv420ThreePlanesToNV21(
194-
yuv420888planes: Array<Image.Plane>, width: Int, height: Int
195-
): ByteBuffer {
196-
val imageSize = width * height
197-
val out = ByteArray(imageSize + 2 * (imageSize / 4))
198-
if (areUVPlanesNV21(yuv420888planes, width, height)) {
199-
// Copy the Y values.
200-
yuv420888planes[0].buffer[out, 0, imageSize]
201-
val uBuffer = yuv420888planes[1].buffer
202-
val vBuffer = yuv420888planes[2].buffer
203-
// Get the first V value from the V buffer, since the U buffer does not contain it.
204-
vBuffer[out, imageSize, 1]
205-
// Copy the first U value and the remaining VU values from the U buffer.
206-
uBuffer[out, imageSize + 1, 2 * imageSize / 4 - 1]
207-
} else {
208-
// Fallback to copying the UV values one by one, which is slower but also works.
209-
// Unpack Y.
210-
unpackPlane(yuv420888planes[0], width, height, out, 0, 1)
211-
// Unpack U.
212-
unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2)
213-
// Unpack V.
214-
unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2)
215-
}
216-
return ByteBuffer.wrap(out)
217-
}
218-
219-
/** Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. */
220-
@RequiresApi(Build.VERSION_CODES.KITKAT)
221-
private fun areUVPlanesNV21(planes: Array<Image.Plane>, width: Int, height: Int): Boolean {
222-
val imageSize = width * height
223-
val uBuffer = planes[1].buffer
224-
val vBuffer = planes[2].buffer
225-
226-
// Backup buffer properties.
227-
val vBufferPosition = vBuffer.position()
228-
val uBufferLimit = uBuffer.limit()
229-
230-
// Advance the V buffer by 1 byte, since the U buffer will not contain the first V value.
231-
vBuffer.position(vBufferPosition + 1)
232-
// Chop off the last byte of the U buffer, since the V buffer will not contain the last U value.
233-
uBuffer.limit(uBufferLimit - 1)
234-
235-
// Check that the buffers are equal and have the expected number of elements.
236-
val areNV21 =
237-
vBuffer.remaining() == 2 * imageSize / 4 - 2 && vBuffer.compareTo(uBuffer) == 0
238-
239-
// Restore buffers to their initial state.
240-
vBuffer.position(vBufferPosition)
241-
uBuffer.limit(uBufferLimit)
242-
return areNV21
243-
}
244-
245-
/**
246-
* Unpack an image plane into a byte array.
247-
*
248-
*
249-
* The input plane data will be copied in 'out', starting at 'offset' and every pixel will be
250-
* spaced by 'pixelStride'. Note that there is no row padding on the output.
251-
*/
252-
@TargetApi(Build.VERSION_CODES.KITKAT)
253-
private fun unpackPlane(
254-
plane: Image.Plane, width: Int, height: Int, out: ByteArray, offset: Int, pixelStride: Int
255-
) {
256-
val buffer = plane.buffer
257-
buffer.rewind()
258-
259-
// Compute the size of the current plane.
260-
// We assume that it has the aspect ratio as the original image.
261-
val numRow = (buffer.limit() + plane.rowStride - 1) / plane.rowStride
262-
if (numRow == 0) {
263-
return
264-
}
265-
val scaleFactor = height / numRow
266-
val numCol = width / scaleFactor
267-
268-
// Extract the data in the output buffer.
269-
var outputPos = offset
270-
var rowStart = 0
271-
for (row in 0 until numRow) {
272-
var inputPos = rowStart
273-
for (col in 0 until numCol) {
274-
out[outputPos] = buffer[inputPos]
275-
outputPos += pixelStride
276-
inputPos += plane.pixelStride
277-
}
278-
rowStart += plane.rowStride
279-
}
280-
}
281157
}

packages/ui-cameraview/platforms/android/java/com/nativescript/cameraview/CameraBase.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import android.util.AttributeSet
1515
import android.util.Log
1616
import android.view.OrientationEventListener
1717
import android.view.Surface
18+
import android.widget.FrameLayout
19+
import android.widget.LinearLayout
1820
import androidx.core.app.ActivityCompat
1921
import androidx.core.content.ContextCompat
2022
import java.io.IOException
@@ -60,11 +62,11 @@ abstract class CameraBase @JvmOverloads constructor(
6062
abstract fun stopPreview()
6163
abstract fun startRecording()
6264
abstract fun stopRecording()
63-
abstract fun takePhoto(jsonStringOptions: String)
65+
abstract fun takePhoto(jsonStringOptions: String? = null)
6466
abstract fun hasFlash(): Boolean
6567
abstract fun cameraRecording(): Boolean
6668
abstract fun toggleCamera()
67-
abstract fun getSupportedRatios(): Array<String>
69+
// abstract fun getSupportedRatios(): Array<String>
6870
abstract fun getAvailablePictureSizes(ratio: String): Array<Size>
6971
abstract fun getAllAvailablePictureSizes(): Array<Size>
7072
abstract var displayRatio: String

packages/ui-cameraview/platforms/android/java/com/nativescript/cameraview/CameraEventListener.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package com.nativescript.cameraview
99

1010
import android.media.Image
11+
import android.graphics.Bitmap
1112
import androidx.camera.core.ImageInfo
1213
import java.io.File
1314
import java.lang.Exception
@@ -17,7 +18,7 @@ interface CameraEventListener {
1718
fun onCameraOpen()
1819
fun onCameraClose()
1920
fun onCameraPhoto(file: File?)
20-
fun onCameraPhotoImage(image: Image?, info: ImageInfo, processor: ImageAsyncProcessor)
21+
fun onCameraPhotoImage(image: Bitmap?, info: ImageInfo/* , processor: ImageAsyncProcessor */)
2122
fun onCameraVideo(file: File?)
2223
fun onCameraAnalysis(analysis: ImageAnalysis)
2324
fun onCameraError(message: String, ex: Exception)

0 commit comments

Comments
 (0)