首页 > 解决方案 > ImageReader 的 onImageAvailable 方法未调用,预览仅显示 8 帧慢动作并冻结(Camera2)

问题描述

我注意到小米红米 Note 9 Pro 的异常行为。我在数百部手机上测试了该应用程序,但此问题仅出现在此设备上,并且仅在使用具有YUV_420_888格式和 176*144 预览分辨率的 ImageReader 时(例如,使用 320 * 240 或 JPEG 或不ImageReader作为捕获表面一切正常)。onImageAvailable方法未调用,预览仅显示 8 帧慢动作并冻结,应用程序变慢。onCaptureCompleted()inCameraCurrentParamsReceiver也只调用了 8 次。

getMinPreviewSize我通过使用(这款小米手机为 176 * 144)获得了最小的分辨率。

const val PREVIEW_IMAGE_FORMAT = ImageFormat.YUV_420_888
const val IMAGE_READER_MAX_SIMULTANEOUS_IMAGES = 4
val previewCaptureCallback = CameraCurrentParamsReceiver(this)

private fun startPreview(cameraDevice: CameraDevice, cameraProperties: CameraProperties)
{
    val imageReader = ImageReader.newInstance(cameraProperties.previewSize.width,
            cameraProperties.previewSize.height,
            PREVIEW_IMAGE_FORMAT,
            IMAGE_READER_MAX_SIMULTANEOUS_IMAGES)

    this.imageReader = imageReader
    bufferedImageConverter = BufferedImageConverter(cameraProperties.previewSize.width, cameraProperties.previewSize.height)

    val previewSurface = previewSurface
    val previewSurfaceForCamera =
            if (previewSurface != null)
            {
                if (previewSurface.isValid)
                {
                    previewSurface
                }
                else
                {
                    Log.w(TAG, "Invalid preview surface - camera preview display is not available")
                    null
                }
            }
            else
            {
                null
            }

    val captureSurfaces = listOfNotNull(imageReader.surface, previewSurfaceForCamera)

    cameraDevice.createCaptureSession(
            captureSurfaces,
            object : CameraCaptureSession.StateCallback()
            {
                override fun onConfigureFailed(cameraCaptureSession: CameraCaptureSession)
                {
                    Log.e(TAG, "onConfigureFailed() cannot configure camera")
                    if (isCameraOpened(cameraDevice))
                    {
                        shutDown("onConfigureFailed")
                    }
                }

                override fun onConfigured(cameraCaptureSession: CameraCaptureSession)
                {
                    Log.d(TAG, "onConfigured()")

                    if (!isCameraOpened(cameraDevice))
                    {
                        cameraCaptureSession.close()
                        shutDown("onConfigured.isCameraOpened")

                        return
                    }

                    captureSession = cameraCaptureSession
                    try
                    {
                        val request = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
                        captureSurfaces.forEach { request.addTarget(it) }
                        CameraPreviewRequestInitializer.initializePreviewRequest(request, cameraProperties, controlParams, isControlParamsStrict)
                        captureRequestBuilder = request

                        val previewCallback = PreviewFrameHandler(this@Camera2)
                        this@Camera2.previewFrameHandler = previewCallback
                        imageReader.setOnImageAvailableListener(previewCallback, previewCallback.backgroundHandler)
                        
                        cameraCaptureSession.setRepeatingRequest(request.build(), previewCaptureCallback, null)
                    }
                    catch (ex: CameraAccessException)
                    {
                        Log.e(TAG, "onConfigured() failed with exception", ex)

                        shutDown("onConfigured.CameraAccessException")
                    }
                }
            },
            null)
}

private fun chooseCamera(manager: CameraManager): CameraProperties?
{
    val cameraIdList = manager.cameraIdList
    if (cameraIdList.isEmpty())
    {
        return null
    }

    for (cameraId in cameraIdList)
    {
        val characteristics = manager.getCameraCharacteristics(cameraId)
        val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
        if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK)
        {
            val minPreviewSize = getMinPreviewSize(characteristics)
            if (minPreviewSize == null)
            {
                Log.e(TAG, "chooseCamera() Cannot determine the preview size")

                return null
            }

            Log.d(TAG, "chooseCamera() chosen camera id: $cameraId, preview size: $minPreviewSize")

            return CameraProperties(cameraId,
                    minPreviewSize,
                    characteristics)
        }
    }

    return null
}

private fun getMinPreviewSize(characteristics: CameraCharacteristics): Size?
{
    val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    if (map == null)
    {
        Log.e(TAG, "getMinPreviewSize() Map is empty")

        return null
    }

    return map.getOutputSizes(Constants.Camera.PREVIEW_IMAGE_FORMAT)?.minBy { it.width * it.height }
}

PreviewFrameHandlerCameraCurrentParamsReceiverpreviewCaptureCallback变量)

private class PreviewFrameHandler(private val parent: Camera2) : ImageReader.OnImageAvailableListener, Handler.Callback
{
    val backgroundHandler: Handler
    private val backgroundHandlerThread: HandlerThread = HandlerThread("Camera2.PreviewFrame.HandlerThread")
    private val mainHandler: Handler = Handler(Looper.getMainLooper(), this)

    /**
     * Main thread.
     */

    init
    {
        backgroundHandlerThread.start()
        backgroundHandler = Handler(backgroundHandlerThread.looper)
    }

    fun shutDown()
    {
        backgroundHandlerThread.quit()
        mainHandler.removeMessages(0)
    }

    override fun handleMessage(msg: Message?): Boolean
    {
        msg ?: return false

        parent.cameraFrameListener.onFrame(msg.obj as RGBImage)

        return true
    }

    /**
     * Background thread.
     */

    private val relativeTimestamp = RelativeTimestamp()

    override fun onImageAvailable(reader: ImageReader)
    {
        var image: Image? = null
        try
        {
            image = reader.acquireNextImage()
            image ?: return

            val rgbImage = parent.bufferedImageConverter?.convertYUV420spToRGB(image, relativeTimestamp.updateAndGetSeconds(image.timestamp))
            rgbImage ?: return

            mainHandler.sendMessage(mainHandler.obtainMessage(0, rgbImage))
        }
        catch (ex: Exception)
        {
            Log.e(TAG, "onImageAvailable()", ex)
        }
        finally
        {
            image?.close()
        }
    }

    private class RelativeTimestamp
    {
        private var initialNanos = 0L

        fun updateAndGetSeconds(currentNanos: Long): Double
        {
            if (initialNanos == 0L)
            {
                initialNanos = currentNanos
            }

            return nanosToSeconds(currentNanos - initialNanos)
        }
    }
}

/**
 * Class used to read current camera params.
 */
private class CameraCurrentParamsReceiver(private val parent: Camera2) : CameraCaptureSession.CaptureCallback()
{
    private var isExposureTimeExceptionLogged = false
    private var isIsoExceptionLogged = false

    override fun onCaptureSequenceAborted(session: CameraCaptureSession, sequenceId: Int)
    {
    }

    override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult)
    {
        try
        {
            val exposureTimeNanos = result.get(CaptureResult.SENSOR_EXPOSURE_TIME)
            if (exposureTimeNanos != null)
            {
                parent.currentExposureTimeNanos = exposureTimeNanos
            }
        }
        catch (ex: IllegalArgumentException)
        {
            if (!isExposureTimeExceptionLogged)
            {
                isExposureTimeExceptionLogged = true
            }
        }

        try
        {
            val iso = result.get(CaptureResult.SENSOR_SENSITIVITY)
            if (iso != null)
            {
                parent.currentIso = iso
            }
        }
        catch (ex: IllegalArgumentException)
        {
            if (!isIsoExceptionLogged)
            {
                Log.i(TAG, "Cannot get current SENSOR_SENSITIVITY, exception: " + ex.message)
                isIsoExceptionLogged = true
            }
        }
    }

    override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure)
    {
    }

    override fun onCaptureSequenceCompleted(session: CameraCaptureSession, sequenceId: Int, frameNumber: Long)
    {
    }

    override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long)
    {
    }

    override fun onCaptureProgressed(session: CameraCaptureSession, request: CaptureRequest, partialResult: CaptureResult)
    {
    }

    override fun onCaptureBufferLost(session: CameraCaptureSession, request: CaptureRequest, target: Surface, frameNumber: Long)
    {
    }
}

据我了解,预览大小有问题,但我找不到正确的方法来获取这个值,最奇怪的是这个问题只出现在这个小米设备上。有什么想法吗?

标签: androidkotlinandroid-cameraandroid-camera2xiaomi

解决方案


176x144 有时对设备来说是一个有问题的分辨率。它实际上仅由相机设备列出,因为有时需要为 MMS(多媒体文本消息)消息录制视频。坦率地说,这些视频看起来很糟糕,但移动运营商仍然经常要求它们工作。

但是在配备 12 - 50 MP 摄像头的现代设备上,摄像头硬件实际上很难将图像从传感器全分辨率缩小到 176x144(> 缩小 20 倍!),因此有时某些尺寸组合可能会导致问题。

我通常建议不要使用低于 320x240 的预览分辨率,以尽量减少问题,并且绝对不要将 176x144 预览与高分辨率静止图像混合使用。


推荐阅读