一文看懂 FFmpeg 在 Android 和 iOS 上的硬件解码:MediaCodec、VideoToolbox 与 AVHWAccel
FFmpeg 的硬解不是一个"开关",而是一套把各平台系统解码器接进
AVPacket → AVFrame通用流水线的抽象层。
做移动端播放器、剪辑器或转码工具时,迟早会遇到一个问题:同样是硬件解码,为什么 iOS/macOS 常见的是 h264_videotoolbox 这类硬件加速后端,而 Android 常见的是 h264_mediacodec 这种独立 decoder?
本文只按公开的 GitHub FFmpeg 来讲,不绑定任何具体业务项目。核心结论先放前面:
| 问题 | 核心结论 |
|---|---|
| FFmpeg 如何引入硬件加速? | 通过硬件设备、硬件帧池、硬件像素格式、硬件后端/硬件 decoder wrapper,把平台差异收敛到 AVFrame。 |
| iOS/macOS 怎么接? | VideoToolbox 更像"通用 decoder + AVHWAccel 后端":FFmpeg 解析码流,VideoToolbox 负责硬件解码输出。 |
| Android 怎么接? | MediaCodec 更像"独立硬件 decoder wrapper":FFmpeg 把 Android 系统 decoder 包装成 h264_mediacodec 等 decoder。 |
| 硬解一定更快吗? | 不一定。如果解完还要拷回 CPU 内存做处理,拷贝成本可能抵消收益。 |
| 怎么自己加硬解? | 通常两条路:给现有 codec 增加 AVHWAccel 后端,或像 MediaCodec 一样新增独立硬件 decoder wrapper。 |
1. FFmpeg 的主线:仍然是 AVPacket 到 AVFrame
先看整体流水线:

在 FFmpeg 里,无论软解还是硬解,上层主流程都尽量保持一致:demuxer 读出压缩数据,decoder 消费 AVPacket,最后吐出 AVFrame。差异在于:软解输出的 AVFrame 通常是 CPU 可访问的 YUV/RGB;硬解输出的 AVFrame 可能只是一个平台硬件资源的引用。
| 对象 | 白话解释 | 软解/硬解差异 |
|---|---|---|
AVPacket |
压缩后的码流包,例如 H.264 NAL、HEVC NAL、VP9 frame。 | 软解和硬解都要消费它。 |
AVFrame |
解码后的帧。 | 软解多是系统内存帧;硬解可能是硬件帧句柄。 |
| 硬件帧 | Surface、CVPixelBuffer、GPU frame、平台私有 buffer 等。 |
不一定能被 CPU 直接读写。 |
这点很重要:**硬解不是把 FFmpeg 的上层接口推倒重来,而是让 AVFrame 能承载硬件侧的帧。**如果后续渲染、编码、滤镜也能在硬件侧完成,就可以少拷贝;如果后续算法必须读像素,就要把硬件帧下载/映射回系统内存。
2. FFmpeg 硬件加速的三层抽象
Android、iOS、Windows、Linux 的硬解 API 完全不同。FFmpeg 要做的是把这些差异收敛成一套统一模型。
| 抽象 | 代表结构/概念 | 解决什么问题 |
|---|---|---|
| 硬件设备类型 | enum AVHWDeviceType |
标识底层 API,例如 AV_HWDEVICE_TYPE_VIDEOTOOLBOX、AV_HWDEVICE_TYPE_MEDIACODEC。 |
| 硬件设备上下文 | AVHWDeviceContext |
保存某个硬件 API 的高层状态,例如系统硬解环境、GPU 设备等。 |
| 硬件帧上下文 | AVHWFramesContext |
描述一组硬件帧池,包括格式、尺寸、设备归属、内存形态。 |
| 硬件像素格式 | AV_PIX_FMT_* |
告诉上层这个帧不是普通 YUV,而是某类硬件帧。 |
| 硬件配置 | AVCodecHWConfig / hw_configs |
说明某个 decoder 支持哪些硬件输出。 |
公开源码 libavutil/hwcontext.h 里能看到这些核心类型。可以把它理解成 FFmpeg 的"硬件资源语言":不同平台实现各自的细节,但对上层尽量说同一种话。
从调用链看,硬解接入不是单点开关,而是一组对象一起完成协商:
| 层级 | 关键对象 / API | 谁提供 | 负责什么 |
|---|---|---|---|
| 能力声明层 | AVCodec / FFCodec 的 hw_configs、avcodec_get_hw_config() |
codec 注册结构 | 告诉上层"这个 decoder 支持哪些硬件像素格式、设备类型、初始化方式"。 |
| 用户选择层 | AVCodecContext.get_format |
应用层或 FFmpeg 默认逻辑 | 在 decoder 给出的候选像素格式里,选择 AV_PIX_FMT_VIDEOTOOLBOX / AV_PIX_FMT_MEDIACODEC 等硬件格式。 |
| 设备绑定层 | AVCodecContext.hw_device_ctx / hw_frames_ctx |
应用层创建,FFmpeg 校验 | 把具体硬件设备、硬件帧池交给 decoder / hwaccel 使用。 |
| 执行后端层 | AVHWAccel 或独立 decoder wrapper |
FFmpeg 平台适配代码 | 把 codec 语义、压缩数据、输出 buffer 翻译成平台硬解 API 调用。 |
| 统一输出层 | AVFrame.format / AVFrame.data[] / AVFrame.buf[] |
decoder 输出 | 对上层仍然表现为 AVFrame,但内容可能是平台硬件 buffer 引用。 |
3. 移动端硬解的两种接入模型
移动端最常见的是两种模型:一种是把系统硬解接成通用 decoder 的硬件后端,另一种是把系统 decoder 包装成独立 FFmpeg decoder。

| 模型 | 代表 | 工作方式 | 适合怎么理解 |
|---|---|---|---|
| 通用 decoder + AVHWAccel 后端 | VideoToolbox、D3D11VA、VAAPI 等常见后端 | FFmpeg 通用 decoder 解析码流,再调用硬件后端输出硬件帧。 | "通用解码器 + 硬件执行器" |
| 独立硬件 decoder wrapper | Android MediaCodec | FFmpeg 把系统 decoder 封装成 h264_mediacodec、hevc_mediacodec 等 decoder。 |
"系统 decoder 的 FFmpeg 外壳" |
这就是很多人看源码时疑惑的来源:同样是 H.264 硬解,VideoToolbox 看起来像挂在 h264dec.c 后面的后端,MediaCodec 看起来却像一个独立 decoder。不是 FFmpeg 设计混乱,而是底层系统 API 的粒度不同。
4. 以 AVC / H.264 为例:h264dec.c 怎么使用硬件加速
如果只看一句话:h264dec.c 是 H.264 通用 decoder 主体,硬件加速不是在这里直接调用某个平台 API,而是通过 hw_configs → get_format → AVHWAccel 回调 挂进去。

4.1 h264dec.c 先把"能用什么硬件"声明出去
在公开 FFmpeg master 中,libavcodec/h264dec.c 里的 ff_h264_decoder 会声明自己的 hw_configs。这张表就是 H.264 通用 decoder 对外暴露的硬解能力入口。
h264dec.c 里的对象 |
作用 | 和硬解的关系 |
|---|---|---|
ff_h264_decoder |
H.264 / AVC 通用 decoder 的注册结构。 | 通过 .hw_configs 暴露 H.264 可用的硬件配置。 |
.hw_configs |
AVCodecHWConfigInternal 指针数组。 |
每一项描述一个硬件像素格式、设备类型、初始化方式,并可指向对应 AVHWAccel。 |
HWACCEL_VIDEOTOOLBOX(h264) |
把 H.264 VideoToolbox 后端挂进配置表。 | 选中 AV_PIX_FMT_VIDEOTOOLBOX 后,FFmpeg 会初始化 ff_h264_videotoolbox_hwaccel。 |
HWACCEL_VAAPI(h264) / HWACCEL_DXVA2(h264) / HWACCEL_NVDEC(h264) 等 |
Linux / Windows / NVIDIA 等平台的 H.264 硬件后端。 | 同一个 H.264 decoder 可以挂多个平台后端,编译开关决定最终有哪些项。 |
NULL 结尾 |
配置数组结束标记。 | 上层枚举 avcodec_get_hw_config(codec, index) 时靠它停止。 |
可以把 hw_configs 理解成一句声明:"我这个 H.264 decoder 可以输出普通软件帧,也可以在条件满足时输出某些硬件帧。"
4.2 上层通过 get_format() 真正选择硬解路径
hw_configs 只代表"编译进来了、理论支持";真正走不走硬解,要看 decoder 打开和格式协商时是否选中了硬件像素格式。
| 协商点 | 发生位置 | 关键判断 | 结果 |
|---|---|---|---|
| 创建硬件设备 | 应用层调用 av_hwdevice_ctx_create()。 |
设备类型是否和目标后端匹配,例如 AV_HWDEVICE_TYPE_VIDEOTOOLBOX。 |
得到 AVBufferRef *hw_device_ctx。 |
| 绑定到 decoder | codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx)。 |
decoder 打开时能看到硬件设备。 | FFmpeg 默认 get_format 或自定义 get_format 可选择硬件格式。 |
| 枚举候选格式 | decoder 内部调用 ff_get_format()。 |
候选列表里是否包含 AV_PIX_FMT_VIDEOTOOLBOX 等硬件格式。 |
上层回调拿到"软解格式 + 硬解格式"候选。 |
| 选择硬件格式 | 应用层 get_format() 或默认选择逻辑。 |
返回的格式必须在候选列表里,并且设备 / frames ctx 校验通过。 | 选中硬件格式后,FFmpeg 初始化对应 AVHWAccel。 |
| 回退软解 | get_format() 返回软件像素格式,或硬件初始化失败。 |
硬件不可用、格式不支持、设备不匹配、运行错误等。 | 输出普通 YUV 软件帧。 |
一个最小 API 心智模型如下:
c
static enum AVPixelFormat get_hw_format(AVCodecContext *ctx,
const enum AVPixelFormat *pix_fmts)
{
for (const enum AVPixelFormat *p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) {
if (*p == AV_PIX_FMT_VIDEOTOOLBOX)
return *p;
}
return AV_PIX_FMT_NONE;
}
av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
NULL, NULL, 0);
codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
codec_ctx->get_format = get_hw_format;
avcodec_open2(codec_ctx, codec, NULL);
| 代码点 | 作用 | 常见坑 |
|---|---|---|
av_hwdevice_ctx_create() |
创建硬件设备上下文。 | 设备类型要和后端匹配;Android / iOS / Linux / Windows 的可用类型不同。 |
codec_ctx->hw_device_ctx |
把硬件设备交给 decoder。 | 只创建不绑定,decoder 协商时仍然不知道硬件设备。 |
codec_ctx->get_format |
在候选格式里选择硬件像素格式。 | 不能返回候选列表之外的格式;找不到目标格式要显式失败或回退。 |
avcodec_open2() |
打开 decoder 并触发能力、格式、后端初始化链路。 | 很多硬解初始化错误会在打开或首帧阶段暴露。 |
av_hwframe_transfer_data() |
需要 CPU 读像素时,把硬件帧下载 / 映射成软件帧。 | 频繁读回 CPU 会让硬解收益大幅下降。 |
4.3 解码过程中,h264dec.c 在关键节点调用 AVHWAccel
选中硬件像素格式后,AVCodecContext 内部会挂上对应的 avctx->hwaccel。从这时开始,H.264 通用 decoder 的语法解析仍然继续,但真正重建像素的工作交给硬件后端。
| 阶段 | h264dec.c / H.264 通用层做什么 |
AVHWAccel 后端做什么 |
输出给下一层的东西 |
|---|---|---|---|
| 1. 参数集 NAL | 解析或转交 SPS / PPS,得到分辨率、profile、level、参考帧等语义。 | 可通过 decode_params() 接收参数集变化,更新平台 decoder 状态。 |
硬件 decoder 需要的 codec config。 |
| 2. 第一片 slice | 建立当前 picture、参考帧关系、POC / DPB 状态。 | start_frame() 准备一帧的硬件侧参数结构。 |
当前帧的 picture params。 |
| 3. slice 数据 | 解析 slice header,保留原始 NAL / slice 数据。 | decode_slice() 接收压缩 slice 数据,加入平台待解码输入。 |
待提交给硬件的 bitstream。 |
| 4. 一帧结束 | ff_h264_field_end() 处理参考图像标记、输出顺序等。 |
end_frame() 把整帧提交给硬件,或触发平台 API 解码。 |
硬件输出 buffer。 |
| 5. 分配输出帧 | 通过 FFmpeg 的 buffer / frame 机制拿到 AVFrame。 |
alloc_frame() / frame_params() 等负责硬件帧池、平台 buffer 绑定。 |
format = AV_PIX_FMT_* 的硬件帧。 |
| 6. 上层消费 | 对外仍然返回 AVFrame。 |
保持平台 buffer 引用,必要时支持 map / transfer。 | 渲染、硬件滤镜、硬编,或下载成软件帧。 |
这里最容易误解的一点是:**硬解并不等于 H.264 decoder 什么都不做。**在 AVHWAccel 模型下,FFmpeg 仍然要解析 H.264 语法、维护 DPB / 参考帧关系、决定什么时候 start / slice / end;硬件后端更像"拿着 FFmpeg 已解析好的参数去调用平台解码器"。
4.4 ff_h264_videotoolbox_hwaccel 是怎么接到 H.264 的
以 VideoToolbox 为例,公开源码 libavcodec/videotoolbox.c 中的 ff_h264_videotoolbox_hwaccel 会填写 AVHWAccel 的关键字段。
| 字段 / 回调 | 在 H.264 硬解里的含义 | 为什么必须有 |
|---|---|---|
.name = "h264_videotoolbox" |
后端名称。 | 日志、调试、配置表引用都需要明确名字。 |
.id = AV_CODEC_ID_H264 |
这个 hwaccel 只服务 H.264。 | 防止被错误 codec 选中。 |
.pix_fmt = AV_PIX_FMT_VIDEOTOOLBOX |
输出帧是 VideoToolbox 硬件帧。 | get_format() 正是通过这个像素格式选中硬解。 |
.start_frame |
一帧开始时准备 H.264 picture 参数。 | 硬件 API 需要在提交前拿到完整帧参数。 |
.decode_params |
SPS / PPS 变化时更新参数。 | H.264 分辨率、profile、level、参数集变化会影响硬件 session。 |
.decode_slice |
接收 slice bitstream。 | H.264 压缩数据仍要送进底层系统 decoder。 |
.end_frame |
一帧结束时提交硬解。 | 到这里整帧语义完整,硬件可以真正开始解码。 |
.frame_params |
填充硬件帧池参数。 | 让 AVHWFramesContext 知道输出帧格式、尺寸、池大小等。 |
.init / .uninit |
创建和释放后端私有资源。 | 例如创建 / 销毁平台 decoder session、私有上下文。 |
因此,h264dec.c 和 VideoToolbox 的关系可以概括成:
| 角色 | 不负责什么 | 主要负责什么 |
|---|---|---|
h264dec.c |
不直接调用 VTDecompressionSessionDecodeFrame()。 |
解析 H.264 码流语义,按帧 / slice 调度 avctx->hwaccel 回调。 |
videotoolbox.c |
不重新实现完整 H.264 通用 decoder。 | 把 FFmpeg 传来的参数和 bitstream 翻译成 VideoToolbox session 调用。 |
| 应用层 | 不直接参与每个 slice 的硬件提交。 | 创建硬件设备、选择硬件像素格式、消费硬件帧或下载软件帧。 |
5. iOS/macOS:VideoToolbox 像"硬件执行后端"
VideoToolbox 是 Apple 平台的视频编解码框架。以 H.264 为例,FFmpeg 常见做法是:H.264 的码流语义仍由通用 decoder 解析,VideoToolbox 负责最终硬件解码。

公开源码 libavcodec/videotoolbox.c 中可以看到 ff_h264_videotoolbox_hwaccel,它包含几类关键回调:
| 回调/字段 | 作用 |
|---|---|
.pix_fmt = AV_PIX_FMT_VIDEOTOOLBOX |
标识输出帧是 VideoToolbox 硬件帧。 |
.start_frame |
一帧开始,准备参数。 |
.decode_slice |
把 slice 数据送给硬件后端。 |
.decode_params |
处理 H.264 参数信息。 |
.end_frame |
一帧结束,触发解码或收尾。 |
.init / .uninit |
初始化和释放 VideoToolbox 资源。 |
VideoToolbox 后端会创建 VTDecompressionSession,再通过 VTDecompressionSessionDecodeFrame 把数据送进 Apple 系统解码器,最后得到 CVPixelBuffer 这类硬件帧。
所以 VideoToolbox 的心智模型是:
| 阶段 | 谁负责 |
|---|---|
| 解析 SPS/PPS、slice header、参考帧语义 | FFmpeg 通用 H.264 decoder |
| 创建系统解码 session | VideoToolbox 后端 |
| 执行硬件解码 | Apple 系统 VideoToolbox |
| 输出硬件帧 | AVFrame 携带 CVPixelBuffer 引用 |
6. Android:MediaCodec 像"完整系统 decoder"
Android 的 MediaCodec 本身就是一条完整 decoder 管线:创建 codec、配置格式、启动、喂输入 buffer、取输出 buffer、flush、stop、release。它不像一个只负责 slice 的底层执行器,更像系统提供的一整个解码器。
因此 FFmpeg 里常见的是独立 decoder wrapper:
| FFmpeg decoder | 对应格式 |
|---|---|
h264_mediacodec |
H.264 / AVC |
hevc_mediacodec |
H.265 / HEVC |
vp8_mediacodec |
VP8 |
vp9_mediacodec |
VP9 |
av1_mediacodec |
AV1,取决于 FFmpeg 版本与平台能力 |
公开源码可以重点看两个文件:
| 文件 | 关注点 |
|---|---|
libavcodec/mediacodecdec.c |
声明 *_mediacodec decoder,处理初始化、收帧、flush、关闭。 |
libavcodec/mediacodecdec_common.c |
封装 MediaCodec 的通用动作:create、configure、start、queue、dequeue。 |
它的流程大概是:
| 阶段 | 关键动作 |
|---|---|
| 创建 | 根据 MIME / codec name 找到 Android 系统 decoder。 |
| 配置 | 构造 MediaFormat,传入宽高、profile、extradata、Surface 等。 |
| 启动 | 调用 start,进入可收发 buffer 的状态。 |
| 送输入 | dequeue input buffer,把 AVPacket 数据写进去,再 queue input buffer。 |
| 取输出 | dequeue output buffer,得到 Surface buffer 或 ByteBuffer 输出。 |
| 包装 | 把输出结果包装成 FFmpeg 上层可处理的 AVFrame。 |
在 FFmpeg configure 中,MediaCodec 通常需要显式开启 JNI 与 MediaCodec:
bash
./configure \
--target-os=android \
--enable-jni \
--enable-mediacodec \
[其他 Android 交叉编译参数]
这里的交叉编译参数会因 NDK、ABI、API level、工具链不同而变化,但理解 Android 硬解时,--enable-jni --enable-mediacodec 是两个关键开关。
7. 为什么两者长得不一样?
一句话:VideoToolbox 和 MediaCodec 的系统 API 粒度不同。
| 对比项 | VideoToolbox | MediaCodec |
|---|---|---|
| 平台 | iOS / macOS / tvOS 等 | Android |
| FFmpeg 常见形态 | AVHWAccel 后端 |
独立 decoder wrapper |
| 码流解析 | FFmpeg 通用 decoder 参与更多 | 系统 MediaCodec decoder 接管更多 |
| 输出对象 | CVPixelBuffer / IOSurface 相关资源 |
Surface / ByteBuffer / 平台私有 buffer |
| 理解方式 | "通用 decoder 选择硬件执行器" | "系统 decoder 被包装成 FFmpeg decoder" |
这不是谁更高级的问题,而是平台 API 的自然结果。VideoToolbox 更容易作为通用 decoder 的硬件后端接入;MediaCodec 本身就是完整队列式 codec,所以包装成独立 decoder 更顺手。
8. 使用时怎么判断有没有硬解?
命令行里可以先看当前 FFmpeg build 编进了哪些硬件加速组件:
bash
ffmpeg -hwaccels
macOS 上验证 VideoToolbox:
bash
ffmpeg -hwaccel videotoolbox -i input.mp4 -f null -
Android 上如果编进了 MediaCodec decoder,常见验证方式是直接指定 decoder:
bash
ffmpeg -c:v h264_mediacodec -i input.mp4 -f null -
如果用 libavcodec API,典型步骤是:创建硬件设备、把 hw_device_ctx 绑定给 decoder、在 get_format 中选择硬件像素格式,然后正常 avcodec_send_packet() / avcodec_receive_frame()。如果拿到的是硬件帧,又需要 CPU 访问像素,就通过 av_hwframe_transfer_data() 转成普通内存帧。
也可以用一张排查表快速定位"为什么没走上硬解":
| 检查点 | 命令 / API | 看到什么才算正常 | 异常时优先看哪里 |
|---|---|---|---|
| 编译能力 | ffmpeg -hwaccels / ffmpeg -decoders |
能看到目标 hwaccel 或 h264_mediacodec 这类 decoder。 |
configure 是否启用对应开关,平台依赖是否满足。 |
| decoder 配置 | avcodec_get_hw_config(codec, i) |
H.264 decoder 能枚举到目标 pix_fmt / device_type。 |
hw_configs 是否接入,宏开关是否编译进来。 |
| 格式协商 | get_format() 日志 / 回调 |
候选列表包含硬件像素格式,并最终返回该格式。 | 回调是否写错,hw_device_ctx 是否缺失或类型不匹配。 |
| 输出帧格式 | frame->format |
值为 AV_PIX_FMT_VIDEOTOOLBOX、AV_PIX_FMT_MEDIACODEC 等。 |
如果是 yuv420p 等软件格式,说明已经回退软解或下载成软件帧。 |
| 运行性能 | profiler / systrace / Instruments | 解码负载转移到系统 codec / 硬件路径。 | 读回 CPU、格式转换、同步等待可能吞掉收益。 |
9. 如何给 FFmpeg 添加自己的硬件加速解码器
给 FFmpeg 增加自定义硬解时,先不要急着写代码,先判断你的平台 API 更像哪一种形态。
| 路线 | 适合场景 | 类比对象 | 核心思路 | 改动范围 |
|---|---|---|---|---|
路线 A:给现有 decoder 增加 AVHWAccel 后端 |
平台 API 更像"硬件执行器",需要 FFmpeg 继续解析 codec 语法。 | VideoToolbox、VAAPI、DXVA2、D3D11VA。 | 复用 h264dec.c,新增 myhw_h264 后端,并把它挂进 H.264 hw_configs。 |
hwcontext、AVHWAccel 文件、hw_configs、build system。 |
| 路线 B:新增独立硬件 decoder wrapper | 平台 API 本身就是完整 decoder 队列,负责创建、喂包、取帧、flush。 | Android MediaCodec、部分厂商私有 codec。 | 新增 h264_myhw decoder,把平台 decoder 包成 FFmpeg decoder。 |
decoder wrapper 文件、公共封装、注册表、bitstream 适配、build system。 |
9.1 路线 A:新增 AVHWAccel 后端
如果你的硬件 API 需要 FFmpeg 先解析 H.264,再把参数和 bitstream 交给硬件,优先走这条路线。
| 步骤 | 要做什么 | 关键文件 / 对象 | H.264 上特别要注意什么 |
|---|---|---|---|
| 1. 定义硬件设备和像素格式 | 如果已有 AV_HWDEVICE_TYPE_* / AV_PIX_FMT_* 可复用就别新增;没有才扩展。 |
libavutil/hwcontext.h、libavutil/pixfmt.h、libavutil/hwcontext_myhw.c。 |
硬件帧要能被 AVFrame.format 明确表达。 |
| 2. 实现硬件上下文 | 管理设备创建、帧池、map / transfer / unmap。 | AVHWDeviceContext、AVHWFramesContext、hwcontext_myhw.c。 |
决定是否支持 zero-copy、是否支持下载到 CPU。 |
| 3. 实现 H.264 hwaccel 文件 | 把 SPS / PPS / slice / DPB 信息翻译成平台 API 参数。 | libavcodec/myhw_h264.c、私有 context。 |
H.264 参数非常多,参考帧列表、field、POC、profile/level 都要谨慎。 |
4. 声明 AVHWAccel |
填 .name、.id、.pix_fmt、.start_frame、.decode_params、.decode_slice、.end_frame、.frame_params、.init、.uninit。 |
const AVHWAccel ff_h264_myhw_hwaccel。 |
回调边界要和 h264dec.c 的调用时机匹配。 |
5. 接入 H.264 hw_configs |
新增类似 HWACCEL_MYHW(h264) 的配置项。 |
libavcodec/hwconfig.h、libavcodec/h264dec.c。 |
不接入 hw_configs,上层就枚举不到你的硬件格式。 |
| 6. 接入编译系统 | 增加 configure 依赖、Makefile 对象、头文件声明。 | configure、libavcodec/Makefile、libavcodec/hwaccels.h。 |
编译开关要能控制是否生成 CONFIG_H264_MYHW_HWACCEL。 |
| 7. 验证 API 和命令行 | 用 avcodec_get_hw_config()、get_format()、frame->format 验证链路。 |
doc/examples/hw_decode.c 可作 API 参考。 |
先验证能解关键 profile/level,再测异常流、seek、flush。 |
AVHWAccel 的最小骨架大概长这样。这里是说明结构用的 C 片段,真实项目还要补齐错误处理、参数翻译、资源释放和 build system。
c
const AVHWAccel ff_h264_myhw_hwaccel = {
.name = "h264_myhw",
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_H264,
.pix_fmt = AV_PIX_FMT_MYHW,
.start_frame = myhw_h264_start_frame,
.decode_params = myhw_h264_decode_params,
.decode_slice = myhw_h264_decode_slice,
.end_frame = myhw_h264_end_frame,
.frame_params = myhw_h264_frame_params,
.init = myhw_h264_init,
.uninit = myhw_h264_uninit,
.priv_data_size = sizeof(MyHWContext),
};
9.2 路线 B:新增独立硬件 decoder wrapper
如果你的平台 API 已经是完整 decoder,像 MediaCodec 一样有自己的输入队列、输出队列、flush、surface / buffer 管理,那就更适合新增 decoder wrapper。
| 步骤 | 要做什么 | 类比 MediaCodec | H.264 上特别要注意什么 |
|---|---|---|---|
| 1. 新增 decoder 文件 | 创建 myhwdec.c 或 myhwdec_common.c,封装平台 decoder 生命周期。 |
mediacodecdec.c、mediacodecdec_common.c。 |
wrapper 自己就是 decoder,不一定复用 h264dec.c 的 slice 调度。 |
| 2. 注册 decoder 名称 | 暴露 h264_myhw、hevc_myhw 等 decoder。 |
h264_mediacodec。 |
名称要让用户能通过 -c:v h264_myhw 或 codec 查找选中。 |
| 3. 实现 init / decode / receive / flush / close | 创建平台 codec、送 AVPacket、取输出、处理 seek / flush、释放资源。 |
mediacodec_decode_init()、mediacodec_receive_frame()、mediacodec_decode_flush()。 |
平台队列状态和 FFmpeg send / receive 语义要对齐。 |
| 4. 处理 H.264 extradata | 把 MP4 里的 avcC、SPS / PPS、Annex B 起始码适配到底层 API 要求。 | h264_mp4toannexb bitstream filter、MediaFormat csd。 |
很多移动端硬解失败都卡在 Annex B / AVCC 和 extradata。 |
| 5. 包装输出帧 | 把 surface、GPU buffer、native handle、ByteBuffer 包成 AVFrame。 |
AV_PIX_FMT_MEDIACODEC。 |
明确 frame 生命周期,避免平台 buffer 过早释放或泄漏。 |
| 6. 接入编译和注册 | 增加 configure 依赖、Makefile 对象、codec 注册。 | CONFIG_H264_MEDIACODEC_DECODER。 |
依赖系统 SDK / NDK / 私有库时要做好条件编译。 |
| 7. 做能力探测和 fallback | profile / level / 分辨率 / 色彩格式不支持时回退。 | MediaCodec codec list / configure 失败处理。 | 移动端机型差异大,fallback 不是可选项。 |
9.3 H.264 自定义硬解的必查清单
无论走 AVHWAccel 还是 wrapper,H.264 都有一些绕不开的坑。建议实现前先把下面这张表逐项确认。
| 检查项 | 为什么重要 | 常见失败表现 |
|---|---|---|
| SPS / PPS | 硬件 decoder 需要分辨率、profile、level、参考帧数、熵编码等参数。 | 首帧黑屏、configure 失败、分辨率变化崩溃。 |
| Annex B vs AVCC | TS / elementary stream 常是 Annex B;MP4 常是 AVCC;平台 API 要求可能不同。 | 能解 TS 不能解 MP4,或反过来;SPS / PPS 丢失。 |
| extradata | MP4 的 SPS / PPS 常在 AVCodecParameters.extradata,不一定跟着每个 packet。 |
第一帧前硬件 decoder 没拿到 codec config。 |
| DPB / 参考帧关系 | B 帧、参考帧重排、长期参考帧都依赖正确 DPB。 | 花屏、马赛克、时间戳错乱、偶发绿屏。 |
| profile / level / bit depth | 移动芯片经常只支持部分 profile / level / 8-bit 格式。 | 某些视频能播,High 10、过高 level 或特殊 chroma 失败。 |
| flush / seek | seek 后平台队列、参考帧、时间戳缓存都要清干净。 | seek 后花屏、旧帧闪现、卡死不出帧。 |
| 分辨率变化 | H.264 流中可能出现参数集变化。 | 播到中途失败、输出尺寸不更新、buffer 越界。 |
| zero-copy / readback | 硬件帧留在 GPU / Surface 最划算,读回 CPU 成本高。 | 解码看似硬解,但整体耗时比软解更差。 |
| 错误回退 | 硬解初始化或运行失败要能回软解。 | 一条异常流导致播放直接失败。 |
10. 几个常见误区
| 误区 | 正确理解 |
|---|---|
| 硬解一定更快 | 不一定。低分辨率、小片段或需要频繁读回 CPU 的场景,硬解收益可能很小。 |
AVFrame 一定有 YUV 指针 |
硬解帧可能只是平台 buffer 引用,不能直接当普通数组读。 |
| 编译支持就代表运行可用 | 运行还依赖系统版本、芯片能力、驱动质量、视频 profile/level、Surface 绑定等。 |
| 所有平台硬解方式一样 | 不一样。FFmpeg 只是把差异尽量收敛到统一接口。 |
FFmpeg 官方命令行文档也提醒过:硬件解码未必总比现代 CPU 软解快,尤其当需要把 GPU/硬件侧帧拷回系统内存时,额外拷贝会带来性能损耗。
11. 总结
如果只记一张表,可以这样理解 FFmpeg 移动端硬解:
| 名词 | 快速理解 |
|---|---|
AVHWDeviceContext |
我有一个硬件设备或系统硬解环境。 |
AVHWFramesContext |
我有一池来自这个硬件设备的帧。 |
AV_PIX_FMT_VIDEOTOOLBOX |
这帧是 Apple VideoToolbox 侧的硬件帧。 |
AV_PIX_FMT_MEDIACODEC |
这帧和 Android MediaCodec 硬件输出相关。 |
AVHWAccel |
通用 decoder 调用硬件后端的桥。 |
h264dec.c |
H.264 通用 decoder 主体;硬解通过 hw_configs / get_format / AVHWAccel 接入。 |
hw_configs |
decoder 对外暴露"我支持哪些硬件像素格式和设备类型"的表。 |
h264_mediacodec |
Android MediaCodec 被包装成的 H.264 FFmpeg decoder。 |
自定义 AVHWAccel |
给现有 codec 增加一个新的硬件执行后端,复用通用 codec 解析。 |
| 自定义硬件 decoder wrapper | 把完整系统 decoder 包成一个独立 FFmpeg decoder,例如 h264_myhw。 |
所以,FFmpeg 支持 Android 和 iOS 硬解的关键,不是简单调用某个系统 API,而是把不同平台的硬件能力接进同一条媒体处理流水线:能硬解时走硬件,硬件帧能继续用就尽量留在硬件侧,必须回 CPU 时再做下载或映射,并始终准备好软解回退。
参考资料
| 资料 | 链接 |
|---|---|
| FFmpeg GitHub 仓库 | https://github.com/FFmpeg/FFmpeg |
硬件上下文抽象:libavutil/hwcontext.h |
https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/hwcontext.h |
H.264 decoder 主体:libavcodec/h264dec.c |
https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/h264dec.c |
硬件配置宏:libavcodec/hwconfig.h |
https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/hwconfig.h |
VideoToolbox 实现:libavcodec/videotoolbox.c |
https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/videotoolbox.c |
Android MediaCodec decoder:libavcodec/mediacodecdec.c |
https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/mediacodecdec.c |
MediaCodec 通用封装:libavcodec/mediacodecdec_common.c |
https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/mediacodecdec_common.c |
硬解 API 示例:doc/examples/hw_decode.c |
https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/hw_decode.c |
| FFmpeg 命令行文档 | https://ffmpeg.org/ffmpeg.html |