15. 多摄像头协同:逻辑摄像头与物理摄像头切换、多路预览流启动优化
多摄像头协同,这话题我太熟了。当年做某款旗舰机的时候,客户要求「三摄同时预览,切换零延迟」。嗯,理想很丰满,现实很骨感。今天咱们就聊聊,怎么把多摄像头这块硬骨头啃下来。
15.1 逻辑摄像头 vs 物理摄像头:别再傻傻分不清
先搞清楚概念。Android 从 API 21 开始引入逻辑摄像头(Logical Camera),说白了就是一个「虚拟摄像头」。它背后可能藏着 2 个、3 个甚至更多物理摄像头。
举个例子:你打开手机相机,看到「1x」「2x」「3x」三个按钮。你点 1x,系统可能用的是广角主摄;点 3x,系统切到了长焦。但对你来说,你操作的始终是同一个 Camera ID。这就是逻辑摄像头在背后帮你做切换。
核心区别:
- 物理摄像头:真实的硬件设备,每个有独立的 Camera ID
- 逻辑摄像头:虚拟组合,对外暴露一个 ID,内部管理多个物理摄像头
我建议你在做多摄方案前,先用 CameraManager.getCameraIdList() 打印一下设备上所有摄像头。你会发现有些 ID 是 0, 1, 2,有些是 BACK, FRONT, EXTERNAL。逻辑摄像头的特征就是 INFO_SUPPORTED_HARDWARE_LEVEL 为 LEVEL_3 或更高。
15.2 多路预览流启动:别让用户等太久
多路预览流启动,说白了就是同时打开多个摄像头,把画面实时显示出来。比如画中画、双摄虚化、多视角直播,都是这个场景。
我踩过最大的坑是什么?同时打开两个摄像头,结果第二个摄像头启动花了 800ms。用户都开始骂街了。为什么会这样?因为每个摄像头打开都要经历:
- HAL 层初始化(最耗时)
- ISP 管线配置
- Sensor 上电 + 输出流配置
- Surface 绑定 + 第一帧回调
这些步骤,如果串行执行,第二个摄像头就得等第一个完全启动完才开始。那怎么优化?
我的优化三板斧:
- 并行打开:用
CameraDevice.StateCallback同时发起多个openCamera请求。HAL 层会并行处理,节省 30%-50% 的时间。 - 预热 Sensor:在用户进入相机界面时,提前给 Sensor 上电。用
CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE判断哪些 Sensor 可能被用到,提前初始化。 - 复用 Session:如果两个摄像头共用同一个 ISP 管线,尽量用
createCaptureSession的OutputConfiguration列表一次性配置多个输出流。
15.3 逻辑摄像头切换:零延迟的秘密
逻辑摄像头的切换,本质上是「关闭一个物理摄像头,打开另一个」。但用户感知到的应该是「无缝切换」。怎么做?
我记得在某个项目里,我们用了「双流并行」的方案:
- 主摄和长焦同时保持预览流
- 用户滑动变焦条时,动态调整两个流的权重
- 当变焦倍数超过阈值,瞬间切换主摄流为长焦流
这样用户根本感觉不到「切换」这个过程。代价是什么?两个摄像头同时工作,功耗翻倍。所以需要配合场景判断,比如光线充足时才启用双流并行。
注意:逻辑摄像头的 SESSION_OPERATION_MODE 必须设置为 CONSTRAINED_HIGH_SPEED 或 ZERO_SHUTTER_LAG,否则切换时会有明显的黑屏或卡顿。
15.4 实战:多路预览流启动代码骨架
直接上代码。这是我个人习惯的写法,用 CountDownLatch 控制并行打开:
// 伪代码,展示核心逻辑
val cameraIds = listOf("0", "2") // 主摄 + 长焦
val latch = CountDownLatch(cameraIds.size)
val devices = mutableListOf<CameraDevice>()
cameraIds.forEach { id ->
cameraManager.openCamera(id, object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
devices.add(camera)
latch.countDown()
}
override fun onError(camera: CameraDevice, error: Int) {
latch.countDown()
}
}, backgroundHandler)
}
// 等待所有摄像头打开
latch.await(2, TimeUnit.SECONDS)
// 创建 Session
val outputs = devices.map { device ->
val surface = createPreviewSurface(device.id)
OutputConfiguration(surface)
}
val session = devices[0].createCaptureSession(outputs, ...)
这里有个细节:createCaptureSession 只能由一个 CameraDevice 发起。如果你需要同时控制多个物理摄像头,得用 CameraDevice.createConstrainedHighSpeedCaptureSession 或者自己管理多个 Session。
15.5 知识体系图:多摄像头协同全景
下面这张图,是我总结的多摄像头协同核心逻辑。你看一眼就能明白整体架构:
15.6 避坑指南:我踩过的那些坑
我曾经在一个项目里,同时打开三个摄像头做多视角直播。结果发现第三个摄像头死活打不开,报 CAMERA_ERROR_DISABLED。查了半天,原来是设备只支持同时打开两个摄像头。这个限制写在 CameraCharacteristics.REQUEST_PARTIAL_RESULT_COUNT 里,但很多人会忽略。
还有一次,逻辑摄像头切换时出现了「鬼影」——画面重叠。后来发现是 CaptureRequest.CONTROL_ZOOM_RATIO 设置不当,导致两个物理摄像头的画面没有正确对齐。解决方案是:切换前先获取两个摄像头的 LENS_INTRINSIC_CALIBRATION,做一次几何校正。
总结几个关键点:
- 先查设备支持的最大并发摄像头数,别硬来
- 逻辑摄像头切换时,务必做画面对齐
- 多路预览流启动,优先用并行方案
- 功耗和性能要平衡,别为了流畅把电池干没了
好了,多摄像头协同这块,核心就是「并行 + 预热 + 对齐」。你想想看,如果每次切换都要重新初始化,用户不骂你才怪。嗯,今天就聊到这儿,下次咱们聊聊 Camera HAL 层的那些坑。
小提示:如果你在调试多摄像头时遇到奇怪的问题,试试用 adb shell dumpsys media.camera 查看当前所有摄像头的状态。这个命令能帮你快速定位是哪个摄像头卡住了。