一、問題背景
在開發基于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_PICTUREafModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO) -> CaptureRequest.CONTROL_AF_MODE_AUTOelse -> 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.planesval yBuffer = planes[0].bufferval uBuffer = planes[1].bufferval vBuffer = planes[2].bufferval 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 數據寫入 ByteArrayOutputStreamval 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層日志的輸出