4. get_number_of_cameras()函数详解:静态与动态获取摄像头数量、多摄像头场景下的计数逻辑、常见实现陷阱
好,咱们接着往下聊。上一节我们把 HAL 设备的打开和关闭流程走了一遍,这一节我打算单独拎出来一个函数好好讲讲——get_number_of_cameras()。
为什么单独讲它?
因为我在项目中踩过它的坑,而且不止一次。你想想看,这个函数是整个 Camera HAL 的“门面”——上层框架第一个调用的就是它。如果它返回的数字不对,后面所有逻辑全乱套。
4.1 静态获取:最简单,也最容易出错
早期 Android 版本里,get_number_of_cameras() 的实现方式很直接。HAL 层在初始化时,通过读取硬件配置文件或者直接硬编码,返回一个固定值。
比如这样:
int get_number_of_cameras() {
// 硬编码返回2,表示有前后两个摄像头
return 2;
}
嗯,简单粗暴。但问题来了——如果设备实际只有 1 个摄像头呢?或者有 3 个呢?
我曾经接手过一个项目,前任工程师就是这么写的。结果客户定制了一款只有前置摄像头的平板,固件刷上去直接崩溃。为什么?因为上层框架以为有后置摄像头,尝试打开时发现设备节点不存在,直接报错。
4.2 动态获取:更灵活,但需要小心
后来 Android 引入了动态获取机制。说白了,就是 HAL 层在运行时去探测实际有多少个摄像头可用。
常见的做法是扫描设备节点:
int get_number_of_cameras() {
int count = 0;
// 扫描 /dev/video* 节点
for (int i = 0; i < MAX_CAMERAS; i++) {
char node_path[64];
snprintf(node_path, sizeof(node_path), "/dev/video%d", i);
if (access(node_path, F_OK) == 0) {
count++;
}
}
return count;
}
这样看起来不错吧?但我在项目中遇到过一个问题——有些 SoC 会把非 Camera 的 Video 节点也放在 /dev/video* 下,比如 MIPI CSI 的测试节点、ISP 的调试节点。结果这个函数返回了 5,实际摄像头只有 2 个。
上层框架拿到 5 个摄像头 ID,一个个去打开,结果后面 3 个全部失败。用户体验极差。
4.3 多摄像头场景下的计数逻辑
现在手机动辄三摄、四摄,甚至五摄。多摄像头场景下,get_number_of_cameras() 的计数逻辑就变得复杂了。
我把它归纳为三种模式:
| 模式 | 说明 | 典型场景 |
|---|---|---|
| 物理摄像头计数 | 每个物理 Sensor 算一个 | 基础三摄:广角、主摄、长焦 |
| 逻辑摄像头计数 | 多个物理 Sensor 组合成一个逻辑摄像头 | 双摄虚化、超分融合 |
| 混合计数 | 物理 + 逻辑同时存在 | 主摄 + 广角 + 长焦 + 双摄虚化组合 |
举个例子。一台手机有 4 个物理 Sensor:主摄、广角、长焦、ToF。如果只做物理计数,返回 4 就行。但如果把主摄和广角组合成一个“超广角融合”逻辑摄像头,那返回的数字可能是 5(4 个物理 + 1 个逻辑)。
这里有个关键点:逻辑摄像头不能重复计数物理 Sensor。我曾经见过一个实现,把物理 Sensor 和逻辑摄像头都算进去了,结果总数变成了 8。上层框架一脸懵,打开摄像头 ID 3 时发现它和 ID 0 是同一个 Sensor,直接冲突。
4.4 常见实现陷阱
好了,说了这么多,我把这些年踩过的坑总结一下。你写代码时对照着检查,能省不少 debug 时间。
- 陷阱一:热插拔场景下计数不变
USB Camera 支持热插拔。但有些 HAL 实现只在初始化时扫描一次,之后插拔 USB 摄像头,get_number_of_cameras() 返回的数字不变。上层框架缓存了这个值,导致新插入的摄像头无法使用。
我建议:对于支持热插拔的设备,每次调用 get_number_of_cameras() 时都重新扫描。当然,要做好性能优化,别每次都去读文件系统。
- 陷阱二:Camera ID 不连续
假设你有 3 个摄像头,但 ID 分别是 0、1、3。中间跳过了 2。有些上层框架会遍历 0 到 N-1 去打开摄像头,结果 ID 2 打开失败,直接认为系统异常。
嗯,这里要注意:返回的摄像头数量必须等于最大 ID + 1,或者保证 ID 从 0 开始连续。否则,你就得在 HAL 层做映射。
- 陷阱三:多进程并发调用
get_number_of_cameras() 可能被多个进程同时调用。如果你的实现里有全局变量或者动态分配内存,记得加锁。我见过一个 case,两个进程同时调用,结果计数变量被写乱了,返回了负数。
- 陷阱四:返回 0 的特殊含义
有些 HAL 实现会在初始化失败时返回 0。但上层框架拿到 0 后,会认为系统没有摄像头,直接禁用所有 Camera 功能。这其实是个设计缺陷——你应该返回一个错误码,而不是用 0 来“伪装”正常情况。
camera_module_t::init() 的返回值来报告错误。
4.5 一个相对稳妥的实现思路
说了这么多问题,那到底怎么写才靠谱?我分享一个我目前在用的模式:
static int g_camera_count = -1;
int get_number_of_cameras() {
// 如果已经初始化过,直接返回缓存值
if (g_camera_count >= 0) {
return g_camera_count;
}
// 加锁,防止并发
pthread_mutex_lock(&g_count_lock);
// 双重检查
if (g_camera_count >= 0) {
pthread_mutex_unlock(&g_count_lock);
return g_camera_count;
}
// 实际扫描逻辑
g_camera_count = 0;
for (int i = 0; i < MAX_PHYSICAL_SENSORS; i++) {
if (is_sensor_present(i)) {
g_camera_count++;
}
}
// 加上逻辑摄像头(不重复计数)
for (int j = 0; j < MAX_LOGICAL_CAMERAS; j++) {
if (is_logical_camera_valid(j)) {
g_camera_count++;
}
}
pthread_mutex_unlock(&g_count_lock);
return g_camera_count;
}
这个实现有几个好处:
- 延迟初始化,只在第一次调用时扫描
- 加锁防止并发问题
- 物理和逻辑摄像头分开计数,避免重复
- 缓存结果,提高性能
当然,如果你要支持热插拔,就不能缓存了。那就每次调用都重新扫描,但要做好性能优化。
好了,这一节就到这里。下一节我们聊聊 get_camera_info() 的实现细节——这个函数坑也不少,尤其是多摄像头场景下的信息填充。