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 层得返回一个「我能做到的最佳方案」。

我个人习惯把协商策略分成三个等级:

  1. 完全满足:硬件资源够,直接通过。
  2. 降级协商:比如 4K 60fps 跑不动,我主动降成 4K 30fps 或者 1080p 60fps。
  3. 拒绝:实在不行,返回 -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 队列的运转:

Stream 配置与协商核心流程 Framework 下发 Stream 需求 HAL 层接收 configure_streams() 资源评估:ISP / DDR / MIPI 带宽 资源 足够? 是 完全满足需求 直接通过 否 降级协商 降分辨率/帧率 Buffer 队列初始化 & Pipeline 深度设定 运行态:Dequeue → Fill → Queue 循环

这张图把整个流程串起来了。从上层的需求下发,到 HAL 层的资源评估,再到降级决策,最后落到 Buffer 队列的日常运转。我个人觉得,只要把这张图吃透了,Stream 配置这块基本就稳了。

小技巧:调试 Stream 协商问题时,我习惯在 configure_streams() 入口和出口各打一条 log,把每个 stream 的 width、height、format、max_buffers 都打出来。对比一下「上层要什么」和「HAL 给了什么」,问题往往一目了然。

最后说一句:Stream 配置与协商,看似是初始化阶段的一次性工作。但 Buffer 队列的管理,贯穿了整个 Camera 生命周期。这块做扎实了,后面很多性能问题都能提前规避。


可可精品资料库