用 AMediaCodec 解码到 Surface:看懂输入队列、输出队列和帧释放
上一章我们已经能从 MP4 中读出压缩视频帧。现在要把这些压缩帧交给硬件解码器,让它们变成真正能显示在屏幕上的图像。
AMediaCodec 是 Android NDK 暴露的媒体编解码接口。对播放器来说,我们主要用它做解码。
先用人话理解解码器
解码器像一台机器,它有入口和出口。
入口:压缩帧,例如 H.264 packet
出口:解码后的图像帧
但你不能直接把数据塞进去,也不能直接从里面拿图像。AMediaCodec 采用的是“缓冲区队列模型”。
你向 codec 借一个空输入缓冲区
你把压缩数据复制进去
你把这个输入缓冲区还给 codec
codec 在内部解码
你从 codec 拿一个输出缓冲区
你把输出缓冲区释放给 Surface 渲染
这就是 dequeueInputBuffer、queueInputBuffer、dequeueOutputBuffer、releaseOutputBuffer 的来历。
Surface 不是一张普通图片
Surface 是 Android 图形系统的一端。你可以把它理解成“显示管线的入口”。当 codec 配置了 Surface,解码后的图像通常不会经过你的 C++ 内存,而是由系统图形管线接管。
这个设计的好处是少拷贝。图像数据可以从解码器直接走到显示系统,避免 CPU 把大块 YUV 数据搬来搬去。
创建 decoder
上一章得到的 mime 可能是 video/avc 或 video/hevc。创建 decoder 时用它。
class CodecOwner {
public:
explicit CodecOwner(const std::string& mime) {
codec_ = AMediaCodec_createDecoderByType(mime.c_str());
}
~CodecOwner() {
if (codec_ != nullptr) {
AMediaCodec_stop(codec_);
AMediaCodec_delete(codec_);
}
}
AMediaCodec* get() const { return codec_; }
private:
AMediaCodec* codec_ = nullptr;
};
AMediaCodec_createDecoderByType 并不保证一定成功。设备可能不支持该 mime,或者厂商实现存在限制。生产代码必须处理 nullptr。
从 Kotlin Surface 到 ANativeWindow
Kotlin 层通常用 SurfaceView 或 TextureView。
override fun surfaceCreated(holder: SurfaceHolder) {
nativeAttachSurface(nativeHandle, holder.surface)
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
nativeDetachSurface(nativeHandle)
}
native 层拿到 Surface 后转换为 ANativeWindow。
ANativeWindow* attachWindow(JNIEnv* env, jobject surface) {
return ANativeWindow_fromSurface(env, surface);
}
void releaseWindow(ANativeWindow* window) {
if (window != nullptr) {
ANativeWindow_release(window);
}
}
这一步必须和生命周期绑定。ANativeWindow_fromSurface 得到的 window 使用完要 release。
配置 codec
AMediaCodec_configure 把 format 和 Surface 交给 codec。
bool configureDecoder(
AMediaCodec* codec,
AMediaFormat* format,
ANativeWindow* window
) {
media_status_t status = AMediaCodec_configure(
codec,
format,
window,
nullptr,
0
);
if (status != AMEDIA_OK) return false;
return AMediaCodec_start(codec) == AMEDIA_OK;
}
最后一个参数是 flags。解码器传 0,不要传 encoder 的配置 flag。
喂输入缓冲区
dequeueInputBuffer 是向 codec 借一个可写输入槽位。
bool feedInput(AMediaCodec* codec, const Packet& packet) {
const ssize_t index = AMediaCodec_dequeueInputBuffer(codec, 10000);
if (index < 0) {
return false;
}
size_t capacity = 0;
uint8_t* dst = AMediaCodec_getInputBuffer(codec, index, &capacity);
if (dst == nullptr || packet.data.size() > capacity) {
AMediaCodec_queueInputBuffer(
codec,
index,
0,
0,
packet.ptsUs,
AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM
);
return false;
}
memcpy(dst, packet.data.data(), packet.data.size());
media_status_t status = AMediaCodec_queueInputBuffer(
codec,
index,
0,
packet.data.size(),
packet.ptsUs,
packet.flags
);
return status == AMEDIA_OK;
}
这里的 ptsUs 会一路传到输出。后面的 AVSync 就靠它判断这帧何时显示。
取输出缓冲区
dequeueOutputBuffer 是从 codec 拿一帧处理完成的数据。
enum class DrainResult {
TryAgain,
FormatChanged,
Rendered,
Ended,
Error,
};
DrainResult drainOutput(AMediaCodec* codec) {
AMediaCodecBufferInfo info{};
const ssize_t index = AMediaCodec_dequeueOutputBuffer(codec, &info, 10000);
if (index == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
return DrainResult::TryAgain;
}
if (index == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
AMediaFormat* outputFormat = AMediaCodec_getOutputFormat(codec);
AMediaFormat_delete(outputFormat);
return DrainResult::FormatChanged;
}
if (index < 0) {
return DrainResult::Error;
}
const bool eos = (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0;
const bool render = info.size > 0;
AMediaCodec_releaseOutputBuffer(codec, index, render);
return eos ? DrainResult::Ended : DrainResult::Rendered;
}
最重要的一行是 AMediaCodec_releaseOutputBuffer。官方文档明确说明:当你处理完 buffer,要把它还给 codec。配置了 Surface 的视频 decoder 可以选择是否渲染。
如果输出 buffer 不释放,codec 的输出队列会被你占满,后面就不再产出新帧。
什么时候使用 releaseOutputBufferAtTime
AMediaCodec_releaseOutputBufferAtTime 可以把 Surface 时间戳一起交给系统。它适合更精细的渲染调度。
int64_t renderTimeNs = systemTimeNs + delayUs * 1000;
AMediaCodec_releaseOutputBufferAtTime(codec, index, renderTimeNs);
初学阶段可以先用 releaseOutputBuffer(codec, index, true) 跑通主链路。理解 AVSync 后,再引入 releaseOutputBufferAtTime。
解码线程主循环
void decodeLoop(AMediaCodec* codec, PacketQueue* queue) {
bool inputEnded = false;
bool outputEnded = false;
while (!outputEnded) {
if (!inputEnded) {
Packet packet;
if (queue->pop(&packet)) {
inputEnded = (packet.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0;
feedInput(codec, packet);
}
}
DrainResult result = drainOutput(codec);
if (result == DrainResult::Ended) {
outputEnded = true;
}
}
}
这只是最小模型。真实项目还要处理 pause、seek、surface detach 和错误恢复。
常见错误
第一类错误:拿 output 以后忘记 release。表现是播放几帧后卡住。
第二类错误:Surface 销毁后还在 render。表现是后台回来黑屏或 native crash。
第三类错误:seek 后只刷新 extractor,不 flush codec。表现是旧画面闪一下,或者花屏。
第四类错误:把 encoder API 用在 decoder 上。比如 createInputSurface 是编码输入 surface 的路径,不是视频播放解码到 Surface 的路径。
本章实验
准备一个 5 秒 MP4,只选择视频轨。
你需要看到这些日志。
decoder created: video/avc
configure ok
first packet ptsUs=0
output format changed
first frame rendered
eos received
如果第一帧不显示,按这个顺序查。
format 是否包含 csd-0/csd-1
Surface 是否有效
queueInputBuffer 是否成功
dequeueOutputBuffer 是否一直 TRY_AGAIN
releaseOutputBuffer 是否被调用
工程风险与观测
decode 阶段最重要的观测项是输入输出队列是否平衡。
input_dequeue_count
input_queue_count
output_dequeue_count
output_release_count
format_changed_count
try_again_later_count
codec_recreate_count
如果 output_dequeue_count 大于 output_release_count,说明 output buffer 可能泄漏。
如果 TRY_AGAIN_LATER 持续增长,说明输入不足、codec 卡住或线程调度异常。
如果 Surface 已销毁还在 render=true,这是典型并发资源释放风险。
错误恢复要分级。
短暂 dequeue 超时:观测并继续
连续 configure 失败:进入 fatal error
Surface 不可用:暂停渲染,等待 attach
seek:flush codec,清队列,重置时间线
本章结论
AMediaCodec 的核心不是“调用一个 decode 函数”,而是正确使用输入队列和输出队列。输入要喂得有时间戳,输出要及时释放,Surface 生命周期要和 Activity 生命周期对齐。