MediaCodec 全链路深度剖析(四):CCodec 与 BufferChannel 的分工
系列导读:第三篇把视角停在了
MediaCodec.cpp的状态机,所有动作最后都落在一个sp<CodecBase> mCodec上。这一篇接着往下走,回答两个问题:
- mCodec 到底是 CCodec 还是 ACodec?谁来选、什么时候选?
- 进了 CCodec 以后,状态切换、buffer 进出、Surface 渲染——为什么要拆给两个类来做?
主角是
CCodec与CCodecBufferChannel,加上贯穿全文的BufferChannelBase这个抽象。
CCodec 和 ACodec
第三篇里反复出现的mCodec,类型是抽象基类sp<CodecBase>
// MediaCodec.h |
CodecBase在 AOSP 里有两个实现:旧路径的ACodec(OMX)和新路径的CCodec(Codec2)。具体走哪条由组件名决定:
// MediaCodec.cpp::GetCodecBase(约 L2358) |
规则只有三条:
- 名字以
c2.开头(例如c2.android.avc.decoder)走CCodec; - 名字以
omx.开头走ACodec; MediaCodecList里的 owner 字段也能直接指定。
Android 10 以后绝大多数 vendor 已经迁到 Codec2,本篇之后只看CCodec这条线。
CCodec 结构
打开mCodec之后,里面也不是一个职责无所不包的CCodec——MediaCodec真正持有的引用其实有两条:
// MediaCodec.h |
mBufferChannel由CCodec::getBufferChannel()创建并返回,CCodec内部用成员mChannel持有同一个对象。Java 一侧通过 JNI 看到的”一个 codec 实例”,对应到 native 是两个对象、三条引用、一个工厂关系:
flowchart LR
MC["MediaCodec
(状态机)"]
CC["CCodec
(控制面)"]
BC["CCodecBufferChannel
(数据面)"]
HAL["Codec2 HAL
(vendor 进程)"]
MC -- "mCodec
start/stop/flush" --> CC
MC -- "mBufferChannel
queue/render/discard" --> BC
CC -- "mChannel
同一个实例" --> BC
CC -- "下发控制指令" --> HAL
BC -- "queue C2Work / 收 onWorkDone" --> HAL
MediaCodec不会通过mCodec间接搬 buffer——每帧queueInputBuffer直接打到mBufferChannel上;start / stop这种则走mCodec。两层在MediaCodec这里就分开了。
三个名字最容易混,先一次说清:
| 名字 | 角色 |
|---|---|
BufferChannelBase |
抽象接口,定义queueInputBuffer / renderOutputBuffer / discardBuffer等数据面方法 |
CCodecBufferChannel |
BufferChannelBase在 Codec2 路径下的具体实现 |
ACodecBufferChannel |
BufferChannelBase在 OMX 路径下的具体实现 |
MediaCodec持有的类型是抽象BufferChannelBase,所以同一段上层代码在 OMX/Codec2 两条路径上是通用的——这条对偶后面会用到。
回到”为什么要拆”。CCodec和CCodecBufferChannel的职责切得很干净:
| 类 | 职责 |
|---|---|
CCodec |
生命周期管理(allocate / configure / start / stop / release),不直接管理数据流 |
CCodecBufferChannel |
数据流管理(input/output buffer 进出、Surface 对接、HDR 侧带数据) |
拆成两个类,背后有三条理由:
- 变化频率不同。生命周期事件少且粗(一次
start、一次stop),buffer 流转高频且细(每帧两次:queue + dequeue)。两者放一起会让加锁、等待、回调逻辑互相挤占。 - 线程模型不同。
CCodec跑在CCodecLooper这条配置线程上;CCodecBufferChannel的onWorkDone来自 HAL 回调线程;queueInputBuffer来自 App 线程。三条线索塞进同一个类里很难管。 - 可替换性。
MediaCodec持有的是BufferChannelBase这个接口,OMX 路径换成ACodecBufferChannel上层不用动一行。
打个比方:CCodec是发动机工厂的厂长,决定开工、停工、招人;CCodecBufferChannel是流水线,负责原料进入、成品产出。MediaCodec是总公司,下指令给厂长的同时,也要直接对接流水线的进出货。
CCodec 的初始化四步
从被构造出来到能跑第一帧,关键是四步:选中、allocate、configure、start。
Step 1:被MediaCodec选中。 GetCodecBase那段已经讲完——名字以c2.开头时返回CreateCCodec(),此后第三篇状态机里所有mCodec->xxx()都作用在该实例上。
Step 2:CCodec::allocate。
void CCodec::allocate(const sp<MediaCodecInfo> &codecInfo) { |
Codec2 路径上有四个名字会在后面反复出现,这里先简单介绍:
- C2 HAL:Android 8.0 以后编解码器实现搬出 mediaserver,住进独立 vendor 进程
android.hardware.media.c2。这套跨进程接口就是 Codec2 HAL。 Codec2Client:mediaserver 这一侧的 HAL 客户端代理,负责跨进程握手、查询 vendor 那边可用的 codec 列表。Codec2Client::Component:通过Codec2Client拿到的”远端句柄”,对应 vendor 进程里某个具体 codec。CCodec后续所有start / queue / flush都通过它转发过去。C2Component:上面那个句柄对应的、住在 vendor 进程里的真正实例,是干活的对象。
知道这四个名字,Step 2 这一步做的事就清楚了:通过Codec2Client跨进程连上 vendor 的 C2 HAL 服务 → 让 vendor 按名字(如c2.android.avc.decoder)创建C2Component → mediaserver 这边拿回Codec2Client::Component句柄交给mChannel持有 → 回调onComponentAllocated,MediaCodec状态机推到INITIALIZED。
Step 3:CCodec::configure。 大头工作集中在这里,一次调用要做 10 件事:
- 把 Java
MediaFormat翻成C2Param(几十个字段); - 写入 width / height / color format 等基础参数;
- 解析并设置色彩元数据(
C2StreamColorAspectsInfo); - 解析 HDR 静态/动态元数据;
- 处理 SPS/PPS csd-0 / csd-1;
- 设置输出 Surface(
mChannel->setSurface); - 反向查询组件实际配置(vendor 可能改写);
- 创建输入/输出 BlockPool;
- 处理 low-latency / tunneling 等特殊模式;
- 回调
onComponentConfigured。
源码位置:CCodec.cpp::configure,约从 L1400 开始的 600 多行。这里只点一条最容易踩的:第 7 步会把组件回填后的格式重新写回outputFormat,所以 Java 侧getOutputFormat()拿到的不是用户原始MediaFormat,而是 vendor 协商后的版本。
Step 4:CCodec::start。
void CCodec::start() { |
到这一步,控制面(CCodec)的工作基本就交完了。后面每帧的 queue/dequeue/render 跟CCodec不再发生关系,MediaCodec直接打到mBufferChannel上。
CCodecBufferChannel 结构
CCodecBufferChannel内部状态分成Input和Output两块,各自带一把锁:
class CCodecBufferChannel : public BufferChannelBase { |
几个抽象先一一对上:
| 抽象 | 含义 |
|---|---|
InputBuffers |
当前有效的输入 buffer 集合(client 持有 + pending in HAL) |
OutputBuffers |
输出 buffer 集合 |
C2BlockPool |
底层 buffer 分配器(SURFACE / BUFFERQUEUE / BASIC 三类) |
mFrameIndex |
每帧分配的唯一 ID,HAL 回来时凭它认领 |
Input和Output各自被Mutexed<>包了一层,是因为queueInputBuffer来自 App 线程,onWorkDone来自 HAL 回调线程,renderOutputBuffer来自 App 线程,三条入口可能并发踩到同一份 slot 表。
输入路径:queueInputBuffer
App 调一帧queueInputBuffer(index, ...)后,进到CCodecBufferChannel:
status_t CCodecBufferChannel::queueInputBufferInternal( |
四步里有三个细节决定了后续路径:
mFrameIndex++给的就是配对凭证。HAL 回来时只带这个 frameIndex,BufferChannel 拿它和发出去时记下的映射表对一下,才能找回当时是哪个 input buffer、对应哪个 output slot。releaseBuffer不释放内存。它做的事是把MediaCodecBuffer切成C2Buffer视图——底层是同一块 ION/dmabuf,MediaCodecBuffer是给 Java/Native 客户端读写用的视图,C2Buffer是给 HAL 跨进程用的视图。第二个参数release=false表示这次只是”借”出去给 HAL,不归还 slot。comp->queue()是 fire-and-forget。Binder 调过去就返回,不等结果。HAL 处理完通过 Listener 回调onWorkDone,在另一条线程上。这条 fire-and-forget 是状态机能保持流畅的关键——queueInputBuffer不会被 vendor 慢解码拖住。
输出路径:onWorkDone
vendor 解完一帧,通过 Listener 回调到 BufferChannel:
void CCodecBufferChannel::onWorkDone(std::list<std::unique_ptr<C2Work>> workItems) { |
这里的mCallback->onOutputBufferAvailable容易和 Java 层的MediaCodec.Callback.onOutputBufferAvailable同名打架——两个不是同一个东西:
- 这个
mCallback类型是BufferCallback,是BufferChannel → MediaCodec的内部回调。无论同步还是异步模式都会触发。 - 它干的事只有一件:把
index / buffer包成kWhatDrainThisBuffer消息,丢到MediaCodec自己的 Looper。
同步与异步的分叉发生在MediaCodec处理kWhatDrainThisBuffer时:
| 模式 | 行为 |
|---|---|
异步(用户调过setCallback) |
调MediaCodec::onOutputBufferAvailable() → 投kWhatCallbackNotify → 派发到 Java 端用户Callback.onOutputBufferAvailable |
同步(用户在轮询dequeueOutputBuffer) |
当前正好有线程阻塞在dequeueOutputBuffer就直接 reply 唤醒它;没有就把 index 暂存进可用队列,下一次dequeueOutputBuffer取走 |
第三篇里讲过dequeueOutputBuffer怎么用PostAndAwaitResponse阻塞 + 条件变量唤醒——onWorkDone这条路径正好对接到那一头:HAL 回调线程把 buffer 写进可用队列后,给阻塞中的dequeueOutputBuffer线程发 reply,那边醒过来拿到 index 返回 Java。两层联动起来才是完整的同步语义。
渲染路径:renderOutputBuffer
Java 侧调releaseOutputBuffer(index, true)走的就是这一段:
status_t CCodecBufferChannel::renderOutputBuffer( |
这一步是 MediaCodec 系列与图形管线系列的衔接点:左手收 vendor 解出来的 YUV GraphicBuffer,右手以 Producer 身份投给 SurfaceFlinger。HDR/crop/transform/fence 这些 side band 看起来杂,背后逻辑只有一句——BufferQueue 那边的 Consumer 不读C2Buffer,所有 vendor 想传给显示链的元信息,都得在这一步翻译成QueueBufferInput字段。
总结
本篇沿着第三篇结尾的mCodec,往下钻了一层,讲清三件事:
- CCodec 是怎么被选中的——名字以
c2.开头就走CreateCCodec(),否则走ACodec;MediaCodecList的 owner 字段也能直接指定。 - CCodec 内部为什么再拆出 BufferChannel——变化频率、线程模型、可替换性三条理由都指向”控制面和数据面分开”。
MediaCodec同时持有mCodec和mBufferChannel,控制走mCodec,每帧数据直接走mBufferChannel。 - 一帧解码在 BufferChannel 上的三段路径——
queueInputBuffer把MediaCodecBuffer切成C2Buffer视图、打frameIndex后 fire-and-forget 投给 HAL;onWorkDone收到 HAL 回填、反向切回MediaCodecBuffer,再交给MediaCodecLooper 决定走同步还是异步路径;renderOutputBuffer把 HDR/crop/transform 翻成QueueBufferInput,以 Producer 身份接到 BufferQueue。
一句话收束:CCodec管开关,CCodecBufferChannel管搬运,BufferChannelBase是它们之间和 OMX 老路径之间唯一不变的契约——MediaCodec 在 native 这一侧的”控制 / 数据 / 抽象”三角,到这里就齐了。