4. Stream 配置与协商
Stream 配置与协商,说白了就是 Camera HAL 层和上层 Framework 之间的一场「谈判」。
上层说:我要 4K 60fps,还要 YUV 和 JPEG 两条流。HAL 层得掂量掂量自己的硬件能不能扛得住。扛不住怎么办?降分辨率、降帧率,或者干脆拒绝。
我早期做高通平台的时候,就遇到过上层硬要 4K 60fps 同时出三路流,结果 ISP 带宽直接爆了,画面卡成 PPT。从那以后,我对 Stream 协商这块就格外上心。
4.1 Stream 数据结构解析
先看核心数据结构。Android Camera HAL 里,Stream 的定义在 camera3_stream_t 里。我习惯把它拆成三块来看:
- 身份信息:
stream_type(CAMERA3_STREAM_OUTPUT / INPUT)、id - 格式与尺寸:
format、width、height - 使用方式:
usage、max_buffers、rotation
嗯,这里要注意 usage 这个字段。它是个位掩码,告诉 HAL 层 buffer 会被怎么用。比如 GRALLOC_USAGE_HW_TEXTURE 表示要给 GPU 做纹理,GRALLOC_USAGE_HW_CAMERA_WRITE 表示 Camera 要往里写数据。
核心要点:max_buffers 不是 HAL 层随便填的。填少了,上层 deque buffer 会卡;填多了,内存吃紧。我一般建议根据 pipeline 深度 + 2 来估算。
// 典型 Stream 配置示例
camera3_stream_t stream;
stream.stream_type = CAMERA3_STREAM_OUTPUT;
stream.width = 1920;
stream.height = 1080;
stream.format = HAL_PIXEL_FORMAT_YCbCr_420_888;
stream.usage = GRALLOC_USAGE_HW_CAMERA_WRITE | GRALLOC_USAGE_HW_VIDEO_ENCODER;
stream.max_buffers = 6; // pipeline 深度 4 + 2 安全余量
stream.rotation = CAMERA3_STREAM_ROTATION_0;
4.2 分辨率/帧率协商策略
协商过程发生在 configure_streams() 调用里。上层把需求列表扔下来,HAL 层得返回一个「我能做到的最佳方案」。
我个人习惯把协商策略分成三个等级:
- 完全满足:硬件资源够,直接通过。
- 降级协商:比如 4K 60fps 跑不动,我主动降成 4K 30fps 或者 1080p 60fps。
- 拒绝:实在不行,返回 -EINVAL,让上层重新提需求。
为什么会降级?说白了就是 ISP 带宽、DDR 带宽、MIPI 速率这三样东西在打架。你想想看,4K 60fps 的 YUV 流,一秒钟的数据量大概是:
3840 * 2160 * 1.5 (YUV420) * 60 = ~712 MB/s
这还没算上 JPEG 编码和预览流的开销。如果 DDR 带宽只有 1.2 GB/s,那两路流就快撑爆了。
我的经验:协商时别只盯着分辨率。帧率降一半,带宽压力能降 50%。很多时候降帧率比降分辨率更划算,用户感知也更小。
我曾经在一个项目里,上层坚持要 4K 60fps 做电子防抖。我算了一下,ISP 的 warp 模块根本跟不上这个吞吐量。最后协商成 4K 30fps + 裁剪到 1080p 输出,防抖效果反而更好了。
4.3 Buffer 队列管理
Buffer 队列是 Stream 的「血管」。血流不畅,整个系统就瘫了。
Android 的 BufferQueue 机制,在 HAL 层体现为 camera3_stream_buffer 的流转。我把它总结为三个状态:
| 状态 | 说明 | 谁在操作 |
|---|---|---|
| IDLE | buffer 空闲,在队列里等着 | HAL / Framework |
| DEQUEUED | HAL 层取走了,正在填数据 | HAL |
| QUEUED | 数据填完了,还给 Framework | Framework |
这里有个坑:Buffer 泄漏。我曾经调试过一个 bug,预览画面每隔几秒就卡一下。抓 trace 一看,有个 buffer 被 HAL 层 deque 之后,因为某个条件分支没走到 release,直接「失踪」了。队列里的 buffer 越来越少,最后 deque 超时。
避坑指南:我曾经在某个平台上,因为 flush() 时没有正确归还所有 dequeued buffer,导致下次 configure_streams() 时 buffer 数量对不上,直接 crash。记得在 flush() 和 close() 里做一次 buffer 全量回收。
Buffer 队列管理的另一个重点是 Pipeline 深度控制。我一般用环形缓冲区来管理:
// 伪代码:环形 buffer 管理
#define PIPELINE_DEPTH 4
struct buffer_slot {
int state; // 0:空闲, 1:填充中, 2:已提交
camera3_stream_buffer_t buf;
};
struct buffer_slot slots[PIPELINE_DEPTH + 2]; // +2 安全余量
int dequeue_free_slot() {
for (int i = 0; i < PIPELINE_DEPTH + 2; i++) {
if (slots[i].state == 0) {
slots[i].state = 1;
return i;
}
}
return -1; // 没有空闲 slot,需要等待
}
嗯,这里要注意:Pipeline 深度不是越大越好。深度大了,latency 会变高。我一般控制在 4~6 之间,既能保证吞吐,又不会让延迟太难看。
4.4 核心逻辑流程图
下面这张图,是我自己总结的 Stream 配置与协商的完整流程。从上层下发需求,到 HAL 层逐级决策,最后落地到 Buffer 队列的运转:
这张图把整个流程串起来了。从上层的需求下发,到 HAL 层的资源评估,再到降级决策,最后落到 Buffer 队列的日常运转。我个人觉得,只要把这张图吃透了,Stream 配置这块基本就稳了。
小技巧:调试 Stream 协商问题时,我习惯在 configure_streams() 入口和出口各打一条 log,把每个 stream 的 width、height、format、max_buffers 都打出来。对比一下「上层要什么」和「HAL 给了什么」,问题往往一目了然。
最后说一句:Stream 配置与协商,看似是初始化阶段的一次性工作。但 Buffer 队列的管理,贯穿了整个 Camera 生命周期。这块做扎实了,后面很多性能问题都能提前规避。
可可精品资料库