系列导读:第三篇把视角停在了MediaCodec.cpp的状态机,所有动作最后都落在一个sp<CodecBase> mCodec上。这一篇接着往下走,回答两个问题:

  • mCodec 到底是 CCodec 还是 ACodec?谁来选、什么时候选?
  • 进了 CCodec 以后,状态切换、buffer 进出、Surface 渲染——为什么要拆给两个类来做?

主角是CCodecCCodecBufferChannel,加上贯穿全文的BufferChannelBase这个抽象。

CCodec 和 ACodec

第三篇里反复出现的mCodec,类型是抽象基类sp<CodecBase>

// MediaCodec.h
class MediaCodec : public AHandler {
sp<CodecBase> mCodec; // 状态机的所有动作最终都作用在它身上
// ...
};

CodecBase在 AOSP 里有两个实现:旧路径的ACodec(OMX)和新路径的CCodec(Codec2)。具体走哪条由组件名决定:

// MediaCodec.cpp::GetCodecBase(约 L2358)
sp<CodecBase> MediaCodec::GetCodecBase(const AString &name, const char *owner) {
if (owner) {
if (strncmp(owner, "default", 8) == 0) {
return new ACodec; // 旧路径:OMX
} else if (strncmp(owner, "codec2", 6) == 0) {
return CreateCCodec(); // 新路径:Codec2
}
}
if (name.startsWithIgnoreCase("c2.")) return CreateCCodec();
if (name.startsWithIgnoreCase("omx.")) return new ACodec;
return nullptr;
}

规则只有三条:

  • 名字以c2.开头(例如c2.android.avc.decoder)走CCodec
  • 名字以omx.开头走ACodec
  • MediaCodecList里的 owner 字段也能直接指定。

Android 10 以后绝大多数 vendor 已经迁到 Codec2,本篇之后只看CCodec这条线。

CCodec 结构

打开mCodec之后,里面也不是一个职责无所不包的CCodec——MediaCodec真正持有的引用其实有两条:

// MediaCodec.h
class MediaCodec : public AHandler {
sp<CodecBase> mCodec; // CCodec / ACodec
std::shared_ptr<BufferChannelBase> mBufferChannel; // CCodecBufferChannel / ACodecBufferChannel
};

// MediaCodec.cpp(约 L2482,进入 INITIALIZED 时)
mBufferChannel = mCodec->getBufferChannel();

mBufferChannelCCodec::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 两条路径上是通用的——这条对偶后面会用到。

回到”为什么要拆”。CCodecCCodecBufferChannel的职责切得很干净:

职责
CCodec 生命周期管理(allocate / configure / start / stop / release),不直接管理数据流
CCodecBufferChannel 数据流管理(input/output buffer 进出、Surface 对接、HDR 侧带数据)

拆成两个类,背后有三条理由:

  1. 变化频率不同。生命周期事件少且粗(一次start、一次stop),buffer 流转高频且细(每帧两次:queue + dequeue)。两者放一起会让加锁、等待、回调逻辑互相挤占。
  2. 线程模型不同CCodec跑在CCodecLooper这条配置线程上;CCodecBufferChannelonWorkDone来自 HAL 回调线程;queueInputBuffer来自 App 线程。三条线索塞进同一个类里很难管。
  3. 可替换性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) {
AString componentName = codecInfo->getCodecName();

// 1. 获取 Codec2Client(单例,连接到 vendor 进程)
std::shared_ptr<Codec2Client> client;
std::shared_ptr<Codec2Client::Component> comp =
Codec2Client::CreateComponentByName(
componentName, mClientListener, &client);

// 2. 把组件句柄交给 BufferChannel
mChannel->setComponent(comp);

// 3. 回调给 MediaCodec:onComponentAllocated
mCallback->onComponentAllocated(componentName.c_str());
}

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持有 → 回调onComponentAllocatedMediaCodec状态机推到INITIALIZED

Step 3:CCodec::configure 大头工作集中在这里,一次调用要做 10 件事:

  1. 把 Java MediaFormat翻成C2Param(几十个字段);
  2. 写入 width / height / color format 等基础参数;
  3. 解析并设置色彩元数据(C2StreamColorAspectsInfo);
  4. 解析 HDR 静态/动态元数据;
  5. 处理 SPS/PPS csd-0 / csd-1;
  6. 设置输出 Surface(mChannel->setSurface);
  7. 反向查询组件实际配置(vendor 可能改写);
  8. 创建输入/输出 BlockPool;
  9. 处理 low-latency / tunneling 等特殊模式;
  10. 回调onComponentConfigured

源码位置:CCodec.cpp::configure,约从 L1400 开始的 600 多行。这里只点一条最容易踩的:第 7 步会把组件回填后的格式重新写回outputFormat,所以 Java 侧getOutputFormat()拿到的不是用户原始MediaFormat,而是 vendor 协商后的版本。

Step 4:CCodec::start

void CCodec::start() {
// 1. 启动组件(让 vendor 进程开始干活)
c2_status_t err = comp->start();

// 2. 启动 BufferChannel:分配 buffer pool、把 input/output 的 slot 备好
mChannel->start(inputFormat, outputFormat, buffersBoundToCodec);

// 3. 订阅事件:C2Component 通过 Listener 回调 onWorkDone / onError / onTripped
// 这三条回调走的是 BufferChannel 不是 CCodec

// 4. 回调 MediaCodec
mCallback->onStartCompleted();
}

到这一步,控制面(CCodec)的工作基本就交完了。后面每帧的 queue/dequeue/render 跟CCodec不再发生关系,MediaCodec直接打到mBufferChannel上。

CCodecBufferChannel 结构

CCodecBufferChannel内部状态分成InputOutput两块,各自带一把锁:

class CCodecBufferChannel : public BufferChannelBase {
std::shared_ptr<Codec2Client::Component> mComponent; // HAL 组件句柄

Mutexed<Input> mInput; // 输入管道
Mutexed<Output> mOutput; // 输出管道

std::shared_ptr<C2BlockPool> mInputAllocator;
std::shared_ptr<C2BlockPool> mOutputSurfacePool;

std::atomic_uint64_t mFrameIndex; // 单调递增帧号,HAL 回调用它来配对
};

struct Input {
std::unique_ptr<InputBuffers> buffers;
size_t numSlots;
std::shared_ptr<LocalBufferPool> bufferPool;
sp<MemoryDealer> memoryDealer;
};

struct Output {
std::unique_ptr<OutputBuffers> buffers;
size_t numSlots;
sp<Surface> surface;
sp<IGraphicBufferProducer> bufferProducer;
};

几个抽象先一一对上:

抽象 含义
InputBuffers 当前有效的输入 buffer 集合(client 持有 + pending in HAL)
OutputBuffers 输出 buffer 集合
C2BlockPool 底层 buffer 分配器(SURFACE / BUFFERQUEUE / BASIC 三类)
mFrameIndex 每帧分配的唯一 ID,HAL 回来时凭它认领

InputOutput各自被Mutexed<>包了一层,是因为queueInputBuffer来自 App 线程,onWorkDone来自 HAL 回调线程,renderOutputBuffer来自 App 线程,三条入口可能并发踩到同一份 slot 表。

输入路径:queueInputBuffer

App 调一帧queueInputBuffer(index, ...)后,进到CCodecBufferChannel

status_t CCodecBufferChannel::queueInputBufferInternal(
sp<MediaCodecBuffer> buffer,
std::shared_ptr<C2BlockPool> encryptedBlockPool,
sp<AMessage> outputFormat) {

int64_t timeUs;
buffer->meta()->findInt64("timeUs", &timeUs);

// 1. 构造 C2Work:Codec2 框架里"一次编解码任务"的描述对象,由 input(数据 + 时间戳 +
// 序号 + 配置更新)和 worklets(HAL 回填的输出占位)两部分组成。
std::unique_ptr<C2Work> work(new C2Work);
work->input.ordinal.timestamp = timeUs;
work->input.ordinal.frameIndex = mFrameIndex++; // 给 HAL 回调认领用
work->input.ordinal.customOrdinal = timeUs;

// 2. 把 MediaCodecBuffer 视图切到 C2Buffer 视图(同一块共享内存,零拷贝)
if (eos) work->input.flags = C2FrameData::FLAG_END_OF_STREAM;
if (csd) work->input.flags = C2FrameData::FLAG_CODEC_CONFIG;

std::shared_ptr<C2Buffer> c2buffer;
mInput.lock()->buffers->releaseBuffer(buffer, &c2buffer, /*release*/ false);
if (c2buffer) work->input.buffers.push_back(c2buffer);

// 3. 加一个空 worklet,HAL 处理完后会把输出回填进来
work->worklets.emplace_back(new C2Worklet);

// 4. 跨进程投递给 vendor 进程
std::list<std::unique_ptr<C2Work>> items;
items.push_back(std::move(work));
c2_status_t err = mComponent->queue(&items);

return err == C2_OK ? OK : UNKNOWN_ERROR;
}

四步里有三个细节决定了后续路径:

  • 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) {
for (auto &work : workItems) {
handleWork(std::move(work), outputFormat, initData);
}
feedInputBufferIfAvailable(); // 顺便看 input 还有没有空 slot 可以喂
}

void CCodecBufferChannel::handleWork(std::unique_ptr<C2Work> work, ...) {
const auto &worklet = work->worklets.front();
const auto &output = worklet->output;

if (output.buffers.size() > 0) {
// 1. 拿到解码后的 C2Buffer(YUV graphic / 压缩 NAL linear)
std::shared_ptr<C2Buffer> buffer = output.buffers[0];

// 2. 反向切视图:注册到 OutputBuffers,分配 index 和 MediaCodecBuffer
sp<MediaCodecBuffer> outBuffer;
size_t index;
mOutput.lock()->buffers->registerBuffer(buffer, &index, &outBuffer);

// 3. 回调上层
mCallback->onOutputBufferAvailable(index, outBuffer);
}
}

这里的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(
const sp<MediaCodecBuffer> &buffer, int64_t timestampNs) {

// 1. 反向切视图:从 MediaCodecBuffer 拿回 C2Buffer,并把这个 slot 还给 OutputBuffers
std::shared_ptr<C2Buffer> c2Buffer;
mOutput.lock()->buffers->releaseBuffer(buffer, &c2Buffer, /*release*/ true);

// 2. 提取 HDR 元数据(C2StreamHdrStaticInfo / C2StreamHdr10PlusInfo)
HdrStaticInfo hdrStatic;
HdrDynamicInfo hdrDynamic;
// ...

// 3. 从 C2 ColorAspects 读出 dataspace / crop / transform
android_dataspace dataSpace = /* ... */;
Rect cropRect = /* ... */;
uint32_t transform = /* ... */;

// 4. 装进 BufferQueue 的 QueueBufferInput
IGraphicBufferProducer::QueueBufferInput input(
timestampNs,
/*isAutoTimestamp=*/false,
dataSpace,
cropRect,
NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW,
transform,
Fence::NO_FENCE); // 也可能从 C2Fence 提取
input.setHdrMetadata(hdrMetadata);
input.setSurfaceDamage(Region());

// 5. 投递给 Surface(也就是 BufferQueue 的 Producer 端)
IGraphicBufferProducer::QueueBufferOutput output;
mOutputSurface->queueBuffer(slot, input, &output);
return OK;
}

这一步是 MediaCodec 系列与图形管线系列的衔接点:左手收 vendor 解出来的 YUV GraphicBuffer,右手以 Producer 身份投给 SurfaceFlinger。HDR/crop/transform/fence 这些 side band 看起来杂,背后逻辑只有一句——BufferQueue 那边的 Consumer 不读C2Buffer,所有 vendor 想传给显示链的元信息,都得在这一步翻译成QueueBufferInput字段。

总结

本篇沿着第三篇结尾的mCodec,往下钻了一层,讲清三件事:

  • CCodec 是怎么被选中的——名字以c2.开头就走CreateCCodec(),否则走ACodecMediaCodecList的 owner 字段也能直接指定。
  • CCodec 内部为什么再拆出 BufferChannel——变化频率、线程模型、可替换性三条理由都指向”控制面和数据面分开”。MediaCodec同时持有mCodecmBufferChannel,控制走mCodec,每帧数据直接走mBufferChannel
  • 一帧解码在 BufferChannel 上的三段路径——queueInputBufferMediaCodecBuffer切成C2Buffer视图、打frameIndex后 fire-and-forget 投给 HAL;onWorkDone收到 HAL 回填、反向切回MediaCodecBuffer,再交给MediaCodec Looper 决定走同步还是异步路径;renderOutputBuffer把 HDR/crop/transform 翻成QueueBufferInput,以 Producer 身份接到 BufferQueue。

一句话收束:CCodec管开关,CCodecBufferChannel管搬运,BufferChannelBase是它们之间和 OMX 老路径之间唯一不变的契约——MediaCodec 在 native 这一侧的”控制 / 数据 / 抽象”三角,到这里就齐了。