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格式 |
---|---|---|
256 | 0x100 | ImageFormat.PRIVATE |
34 | 0x22 | ImageFormat.YV12 |
35 | 0x23 | ImageFormat.YUV_420_888 |
四、核心问题定位
4.1 格式转换失败原因
- 硬件限制:设备不支持YU12格式的软件转换
- 格式冲突:JPEG(BLOB)与YV12格式混合使用导致HAL层异常
4.2 YUV格式转换关键点
YUV_420_888与NV21格式对比:
冷知识:NV21是Camera API默认的格式;YUV_420_888是Camera2 API默认的格式。而且不能直接将 YUV 原始数据保存为 JPG,必须经过格式转换。
特征 | YUV_420_888 | NV21 |
---|---|---|
平面排列 | 半平面+全平面 | 半平面 |
内存布局 | Y + U + V平面 | Y + VU交错 |
色度采样 | 4:2:0 | 4: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层日志的输出