当前位置: 首页 > news >正文

Camera2 API拍照失败问题实录:从错误码到格式转换的排坑之旅

一、问题背景

在开发基于Camera2 API的相机应用时,我们遇到了一个棘手的问题:预览功能在所有设备上工作正常,但在某特定安卓设备上点击拍照按钮后无任何响应。值得注意的是,使用旧版Camera API时该设备可以正常拍照。本文记录了完整的排查过程和解决方案。

二、问题现象与初步分析

2.1 异常现象特征

  • 设备特定性:仅在某一品牌设备出现(其他手机/平板正常)
  • 错误静默:无崩溃日志,但捕获失败回调触发
  • 兼容性矛盾:旧版Camera API工作正常

2.2 初始日志定位

    // 提交拍照请求
    captureSession?.apply {
        stopRepeating()
        abortCaptures()

        capture(captureRequest.build(), object : CameraCaptureSession.CaptureCallback() {
            override fun onCaptureCompleted(
                session: CameraCaptureSession,
                request: CaptureRequest,
                result: TotalCaptureResult
            ) {
                super.onCaptureCompleted(session, request, result)
                Log.e(TAG, "onCaptureCompleted!!!!")
                // 恢复预览
            }

            override fun onCaptureFailed(
                session: CameraCaptureSession,
                request: CaptureRequest,
                failure: CaptureFailure
            ) {
                super.onCaptureFailed(session, request, failure)
                Log.e(TAG, "Capture failed with reason: ${failure.reason}")
                Log.e(TAG, "Failed frame number: ${failure.frameNumber}")
                Log.e(TAG, "Failure is sequence aborted: ${failure.sequenceId}")
            }
        }, null)
    } ?: Log.e(TAG, "Capture session is null")
} catch (e: CameraAccessException) {
    Log.e(TAG, "Camera access error: ${e.message}")
} catch (e: IllegalStateException) {
    Log.e(TAG, "Invalid session state: ${e.message}")
} catch (e: Exception) {
    Log.e(TAG, "Unexpected error: ${e.message}")
}

在onCaptureFailed回调中发现关键日志:

Capture failed with reason: 1 // ERROR_CAMERA_DEVICE

三、深度排查过程

3.1 对焦模式兼容性验证

通过CameraCharacteristics查询设备支持的自动对焦模式:

// 在初始化相机时检查支持的 AF 模式
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val afModes = characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES) ?: emptyArray()

// 选择优先模式
val afMode = when {
    afModes.contains(CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) -> 
        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
    afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO) -> 
        CaptureRequest.CONTROL_AF_MODE_AUTO
    else -> CaptureRequest.CONTROL_AF_MODE_OFF
}

// 在拍照请求中设置
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, afMode)

调整代码逻辑后错误码变为:

Capture failed with reason: 0 // ERROR_CAMERA_REQUEST
Failed frame number: 1949

3.2 HAL层日志分析

通过ADB获取底层日志:

adb shell setprop persist.camera.hal.debug 3
adb shell logcat -b all -c
adb logcat -v threadtime > camera_log.txt

上述命令运行后,即可操作拍照,然后中断上述命令,调查camera_log.txt中对应时间点的日志。
找到关键错误信息

 V4L2 format conversion failed (res -1)
Pixel format conflict: BLOB(JPEG) & YV12 mixed
SW conversion not supported from current sensor format

3.3 输出格式兼容性验证

通过StreamConfigurationMap查询设备支持格式:

val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val configMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedFormats = configMap?.outputFormats?.toList() ?: emptyList()

Log.d(TAG, "Supported formats: ${supportedFormats.joinToString()}")

// 检查是否支持 NV21
if (!supportedFormats.contains(ImageFormat.NV21)) {
    Log.e(TAG, "NV21 is NOT supported on this device")
}

// 输出结果为 [256, 34, 35]
我使用python来做个转换,很舒适:

>>> hex(34)
'0x22'
>>> hex(35)
'0x23'
>>> hex(256)
'0x100'
>>>

格式解码对照表(请查ImageFormat.java源文件):

十进制十六进制Android格式
2560x100ImageFormat.PRIVATE
340x22ImageFormat.YV12
350x23ImageFormat.YUV_420_888

四、核心问题定位

4.1 格式转换失败原因

  1. 硬件限制:设备不支持YU12格式的软件转换
  2. 格式冲突:JPEG(BLOB)与YV12格式混合使用导致HAL层异常

4.2 YUV格式转换关键点

YUV_420_888与NV21格式对比:
冷知识:NV21是Camera API默认的格式;YUV_420_888是Camera2 API默认的格式。而且不能直接将 YUV 原始数据保存为 JPG,必须经过格式转换。

特征YUV_420_888NV21
平面排列半平面+全平面半平面
内存布局Y + U + V平面Y + VU交错
色度采样4:2:04:2:0
Android支持API 21+API 1+

五、解决方案实现

5.1 格式转换核心代码

  //  将 YUV_420_888 转换为 NV21 格式的字节数组
    private fun convertYUV420ToNV21(image: Image): ByteArray {
        val planes = image.planes
        val yBuffer = planes[0].buffer
        val uBuffer = planes[1].buffer
        val vBuffer = planes[2].buffer

        val ySize = yBuffer.remaining()
        val uSize = uBuffer.remaining()
        val vSize = vBuffer.remaining()

        val nv21 = ByteArray(ySize + uSize + vSize)
        yBuffer.get(nv21, 0, ySize)
        vBuffer.get(nv21, ySize, vSize)
        uBuffer.get(nv21, ySize + vSize, uSize)

        return nv21
    }

    /* 将 YUV_420_888 转换为 JPEG 字节数组 */
    private fun convertYUVtoJPEG(image: Image): ByteArray {
        val nv21Data = convertYUV420ToNV21(image)  
        val yuvImage = YuvImage(
            nv21Data,
            ImageFormat.NV21,
            image.width,
            image.height,
            null
        )

        // 将 JPEG 数据写入 ByteArrayOutputStream
        val outputStream = ByteArrayOutputStream()
        yuvImage.compressToJpeg(
            Rect(0, 0, image.width, image.height),
            90,
            outputStream
        )
        return outputStream.toByteArray()
    }

5.2 保存系统相册示例:

   /* 保存到系统相册 */
    private fun saveToGallery(jpegBytes: ByteArray) {
        val contentValues = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
                put(MediaStore.Images.Media.IS_PENDING, 1)  // Android 10+ 需要
            }
        }

        try {
            // 插入媒体库
            val uri = contentResolver.insert(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues
            ) ?: throw IOException("Failed to create media store entry")

            contentResolver.openOutputStream(uri)?.use { os ->
                os.write(jpegBytes)
                os.flush()

                // 更新媒体库(Android 10+ 需要)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    contentValues.clear()
                    contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
                    contentResolver.update(uri, contentValues, null, null)
                }

                runOnUiThread {
                    Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show()
                    // 触发媒体扫描(针对旧版本)
                    sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri))
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "保存失败: ${e.message}")
            runOnUiThread {
                Toast.makeText(this, "保存失败", Toast.LENGTH_SHORT).show()
            }
        }
    }

上述修改后,再次测试验证,这次是可以拍照成功的,并且相册中也会新增刚刚的照片。

六、最后的小经验

排错时别忘记:
设备兼容性检查清单

  • 输出格式支持性验证
  • 对焦模式白名单检查
  • 最大分辨率兼容测试
  • HAL层日志的输出

相关文章:

  • Python实现生产者消费者模型-多进程与多线程处理
  • 基于Redis分布锁+事务补偿解决数据不一致性问题
  • 大数据E10:基于Spark和Scala编程解决一些基本的数据处理和统计分析,去重、排序等
  • 论文阅读:2023 EMNLP SeqXGPT: Sentence-level AI-generated text detection
  • 盛铂科技国产SLMF315超低相位噪声频率综合器介绍
  • SpringBoot有几种获取Request对象的方法
  • 龙虎榜——20250321
  • 第五章 起航18 管理会议信息同步
  • 计算机操作系统(三) 操作系统的特性、运行环境与核心功能(附带图谱更好对比理解))
  • 游戏引擎学习第173天
  • JAVA————十五万字汇总
  • QPrintDialog弹出慢的问题
  • 图表的黄金比例
  • clamav服务器杀毒(Linux服务器断网状态下如何进行clamav安装、查杀)
  • Office 2024 专业版系统安装
  • 黑马程序员-微服务开发-MybatisPlus的使用
  • 【LLM学习】论文学习-Qlora: QLoRA: Efficient Finetuning of Quantized LLMs
  • docker compose部署minio报错
  • 到底爱不爱我
  • 【数据挖掘】数据预处理——以鸢尾花数据集为例
  • 上海首发经济“卷”到会展业,浦东签约三个年度“首展”
  • 江西吉水通报一男子拒服兵役:不得考公,两年内经商、升学等受限
  • 上海下周最高气温在30℃附近徘徊,夏天越来越近
  • 第19届威尼斯建筑双年展开幕,中国案例呈现“容·智慧”
  • 婚姻登记“全国通办”首日观察:数据多跑路,群众少跑腿
  • 国家主席习近平会见斯洛伐克总理菲佐