【FFmpeg之如何新增一个硬件解码器】

ops/2025/3/6 8:21:35/

FFmpeg之如何新增一个硬件解码器

  • 前言
  • 一、config配置
  • 二、解码器定义
    • 1.目录结构
    • 2.数据结构
  • 三、解码流程
    • 1、初始化mediacodec_decode_init
    • 2、帧接收mediacodec_receive_frame
      • 2.1 解码上下文MediaCodecH264DecContext
      • 2.2 发包AVPacket到解码器 -- ff_mediacodec_dec_send
      • 2.3 接收解码后数据AVFrame -- ff_mediacodec_dec_receive
    • 3、刷新缓冲区mediacodec_decode_flush
    • 4、关闭解码器mediacodec_decode_close
  • 四、回顾与总结

前言

  最近在鸿蒙上开发音视频相关功能,在适配好SDL2之后,接入FFmpeg软解即可播放音视频,然对于4k大码率的视频,播放时却非常卡顿。于是乎琢磨着在鸿蒙上加一个FFmpeg的硬解码,PS:鸿蒙官方目前只提供解码相关 SDK。不过应该如何入手呢?想到这个鸿蒙安卓还是有点类似,解码都是异步回调机制,于是先捋一遍安卓下硬解码流程吧。

一、config配置

  首先MediaCodec是Android平台提供的底层音视频编解码API,支持解码的格式有

  •   视频:H.264(AVC)、H.265(HEVC)、VP8、VP9、MPEG-4、AV1
  •   音频:AAC、MP3、Opus
    下面以H.264格式为例,在configure文件中有以下两行:
h264_mediacodec_decoder_deps="mediacodec"
h264_mediacodec_decoder_select="h264_mp4toannexb_bsf h264_parser"

  第一行"mediacodec"表示该解码器依赖 Android 的 MediaCodec API,在配置阶段,configure 脚本会通过配置的NDK路径,检测系统中是否存在这些依赖项;
  第二行声明该解码器的关联组件,当 h264_mediacodec_decoder 被启用时,configure 会自动启用以下组件:

  •   h264_mp4toannexb_bsf:将 H.264 码流从 MP4 封装格式(AVCC)转换为 Annex B 格式。【这个后面会用到,稍后再讲~】
  •   h264_parser:H.264 码流解析器,用于解析码流中的 NALU 单元。.

  在命令行指定一下相关参数即可开启硬件解码:

./configure \--target-os=android \--arch=arm64 \--enable-cross-compile \--sysroot=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot \--enable-mediacodec \--enable-decoder=h264_mediacodec

  生成的MakeFile文件中有一行

OBJS-$(CONFIG_H264_MEDIACODEC_DECODER) += mediacodecdec.o

二、解码器定义

1.目录结构

  在libavcodec目录下,mediacodec相关的文件有12个,如下:
在这里插入图片描述

2.数据结构

  首先我们可以看下mediacodecdec.c中DECLARE_MEDIACODEC_VDEC相关的宏定义,

#define DECLARE_MEDIACODEC_VCLASS(short_name)                   \
static const AVClass ff_##short_name##_mediacodec_dec_class = { \.class_name = #short_name "_mediacodec",      /* 注册到 FFmpeg 的类名 */ \.item_name  = av_default_item_name,           /* 默认的对象名称生成器 */ \.option     = ff_mediacodec_vdec_options,     /* 解码器配置选项指针 */ \.version    = LIBAVUTIL_VERSION_INT,          /* 版本号对齐校验 */ \
};#define DECLARE_MEDIACODEC_VDEC(short_name, full_name, codec_id, bsf)         \
DECLARE_MEDIACODEC_VCLASS(short_name)  /* 先声明 AVClass */                     \
const FFCodec ff_ ## short_name ## _mediacodec_decoder = {                    \.p.name         = #short_name "_mediacodec",          /* 解码器名称 */       \CODEC_LONG_NAME(full_name " Android MediaCodec decoder"), /* 长描述 */     \.p.type         = AVMEDIA_TYPE_VIDEO,                /* 媒体类型:视频 */   \.p.id           = codec_id,                          /* FFmpeg 编解码ID */  \.p.priv_class   = &ff_##short_name##_mediacodec_dec_class, /* 私有类指针 */ \.priv_data_size = sizeof(MediaCodecH264DecContext),   /* 私有数据区大小 */    \.init           = mediacodec_decode_init,            /* 初始化回调函数 */    \FF_CODEC_RECEIVE_FRAME_CB(mediacodec_receive_frame), /* 帧接收回调 */      \.flush          = mediacodec_decode_flush,           /* 冲刷缓冲区回调 */    \.close          = mediacodec_decode_close,           /* 关闭解码器回调 */    \.p.capabilities = AV_CODEC_CAP_DELAY |               /* 支持延迟输出 */      \AV_CODEC_CAP_AVOID_PROBING |       /* 避免格式探测 */      \AV_CODEC_CAP_HARDWARE,             /* 硬件加速标志 */      \.caps_internal  = FF_CODEC_CAP_NOT_INIT_THREADSAFE, /* 非线程安全初始化 */   \.bsfs           = bsf,                              /* 关联的比特流过滤器 */ \.hw_configs     = mediacodec_hw_configs,            /* 硬件配置信息表 */      \.p.wrapper_name = "mediacodec",                     /* 封装器名称 */        \
};#if CONFIG_H264_MEDIACODEC_DECODER
/* 实例化 H.264 解码器结构体 */
DECLARE_MEDIACODEC_VDEC(h264,       /* 短名称 */"H.264",     /* 标准名称 */AV_CODEC_ID_H264, /* FFmpeg 编码ID */"h264_mp4toannexb") /* MP4 到 Annex-B 格式转换器 */
#endif

  其中先声明一个AVClass 结构体,这个是FFmpeg 的类系统核心,用于统一管理编解码器的元数据,注册FFCodec解码器;
  然后就是在解码器中注册四个回调函数:mediacodec_decode_init(初始化)、mediacodec_receive_frame(帧接收)、mediacodec_decode_flush(刷新缓冲区)、mediacodec_decode_close(关闭解码器)
  最后可以看到capabilities属性中的AV_CODEC_CAP_HARDWARE标志,这个就是开启硬件加速,比较关键,如果没有这个标志那么就是用的软解了。

三、解码流程

1、初始化mediacodec_decode_init

  mediacodec_decode_init里面主要做了两件事:
  一、设置FFAMediaFormat媒体格式的一些属性,如MIME类型、context的宽和高等:

	// h264对应的MIME为"video/avc"ff_AMediaFormat_setString(format, "mime", codec_mime);ff_AMediaFormat_setInt32(format, "width", avctx->width);ff_AMediaFormat_setInt32(format, "height", avctx->height);

  最后通过调用NDK里面的相关函数设置(没有NDK的话就通过jni去调java的? 没细究 0.o)
  二、ff_mediacodec_dec_init,其中首先通过

s->codec_name = ff_AMediaCodecList_getCodecNameByType(mime, profile, 0, avctx);
s->codec = ff_AMediaCodec_createCodecByName(s->codec_name, s->use_ndk_codec);

  获取解码器名称并创建解码器,然后再配置解码器并启动

status = ff_AMediaCodec_configure(s->codec, format, s->surface, NULL, 0);
status = ff_AMediaCodec_start(s->codec);

  上述函数均有NDK和JNI两套调用逻辑。

2、帧接收mediacodec_receive_frame

  mediacodec_receive_frame是从解码器中获取解码后的视频帧,里面的核心流程是异步处理输入和输出缓冲区(通过队列管理)。当ff_mediacodec_dec_send被调用时,AVPacket数据会被放入输入队列,等待解码器处理。解码后的数据则从输出队列中取出,即ff_mediacodec_dec_receive函数负责从输出队列获取解码后的帧AVFrame。

2.1 解码上下文MediaCodecH264DecContext

  不过在这之前首先来看下解码器上下文MediaCodecH264DecContext这个关键类的数据结构设计:

// H264 MediaCodec解码器上下文:
typedef struct MediaCodecH264DecContext {//AVClass 集成到FFmpeg的类系统中,用于日志记录、私有选项配置及参数解析AVClass *avclass;//MediaCodecDecContext指向通用的MediaCodec解码器上下文,h264只是其中的一个特化MediaCodecDecContext *ctx;//当输入数据包过大,无法一次性写入硬件缓冲区时,剩余数据暂存于此,等待后续处理AVPacket buffered_pkt;//延迟刷新解码器的标志,确保所有已提交数据被处理后再执行刷新int delay_flush;int amlogic_mpeg2_api23_workaround; // ?// NDK API更高效,减少Java层交互开销,适用于高性能需求场景int use_ndk_codec;
} MediaCodecH264DecContext;//通用的MediaCodec解码器上下文,管理硬件解码器状态:
typedef struct MediaCodecDecContext {AVCodecContext *avctx; //FFmpeg编解码上下文,用于日志和配置信息atomic_int refcount;atomic_int hw_buffer_count;char *codec_name;FFAMediaCodec *codec;FFAMediaFormat *format;void *surface; // FFANativeWindow * 渲染表面(用于零拷贝)int started;int draining;int flushing;int eos;int width;int height;int stride;int slice_height;int color_format;int crop_top;int crop_bottom;int crop_left;int crop_right;int display_width;int display_height;uint64_t output_buffer_count;ssize_t current_input_buffer;bool delay_flush;atomic_int serial;bool use_ndk_codec;
} MediaCodecDecContext;

  在提交缓冲区给解码器解码的过程中,一般来说可以通过Surface(上面的void *surface就是指向渲染表面)或DMA 缓冲区共享技术实现零拷贝,减少 CPU 与 GPU 间的数据传输避免解码过慢。

2.2 发包AVPacket到解码器 – ff_mediacodec_dec_send

// 尝试获取输入缓冲区索引
index = ff_AMediaCodec_dequeueInputBuffer(codec, input_dequeue_timeout_us);
// 获取输入缓冲区的内存地址data和容量size
data = ff_AMediaCodec_getInputBuffer(codec, index, &size);
// 提交输入缓冲区到解码器
status = ff_AMediaCodec_queueInputBuffer(codec, index, 0, size, pts, 0);

2.3 接收解码后数据AVFrame – ff_mediacodec_dec_receive

//: 从解码器输出队列中获取缓冲区索引
index = ff_AMediaCodec_dequeueOutputBuffer(codec, &info, output_dequeue_timeout_us);
...
if (info.size) {// Surface 模式:通过 ANativeWindow 直接渲染到 Surface,无需拷贝数据。if (s->surface) {if ((ret = mediacodec_wrap_hw_buffer(avctx, s, index, &info, frame)) < 0) {av_log(avctx, AV_LOG_ERROR, "Failed to wrap MediaCodec buffer\n");return ret;}// ByteBuffer 模式:将 MediaCodec 的 ByteBuffer 数据复制到 AVFrame->data} else {data = ff_AMediaCodec_getOutputBuffer(codec, index, &size);if (!data) {av_log(avctx, AV_LOG_ERROR, "Failed to get output buffer\n");return AVERROR_EXTERNAL;}if ((ret = mediacodec_wrap_sw_buffer(avctx, s, data, size, index, &info, frame)) < 0) {av_log(avctx, AV_LOG_ERROR, "Failed to wrap MediaCodec buffer\n");return ret;}}s->output_buffer_count++;return 0;
} else {status = ff_AMediaCodec_releaseOutputBuffer(codec, index, 0);if (status < 0) {av_log(avctx, AV_LOG_ERROR, "Failed to release output buffer\n");}
}
...
if (ff_AMediaCodec_infoOutputBuffersChanged(codec, index)) {ff_AMediaCodec_cleanOutputBuffers(codec); // 清理旧缓冲区
}

3、刷新缓冲区mediacodec_decode_flush

  mediacodec_decode_flush在视频播放中,当用户跳转进度时,需要清空之前的解码数据,这时候就会调用flush函数。

static void mediacodec_decode_flush(AVCodecContext *avctx)
{MediaCodecH264DecContext *s = avctx->priv_data;//av_packet_unref是FFmpeg中释放AVPacket资源的函数,//这里是释放MediaCodecH264DecContext 中缓存的未完全提交到硬件解码器的 AVPacket 数据包av_packet_unref(&s->buffered_pkt);//清空解码器的输入/输出缓冲区若,解码器正在处理数据(Executing 状态),flush会强制停止当前操作ff_mediacodec_dec_flush(avctx, s->ctx);
}

一般来说一下四种场景会调用flush:
  a、视频播放器跳转进度:
    用户拖动进度条时,需清空当前解码队列,避免旧数据与新位置的数据混合。
  b、处理解码错误
    当解码器因数据错误进入异常状态时,通过刷新重置其状态,恢复解码能力。
  c、格式动态切换
    切换分辨率或码率时,需先清空原有数据,再重新配置解码器。
  d、结束流或重新初始化
    在流结束或重新初始化解码器前,确保资源正确释放。

4、关闭解码器mediacodec_decode_close

  通过引用计数管理声明周期,计数为0时依次删除MediaCodec、MediaFormat和Surface等对象,最后删除MediaCodecDecContext。

static void ff_mediacodec_dec_unref(MediaCodecDecContext *s)
{...// 原子操作:引用计数减1,若原值为1(减后为0),则释放资源if (atomic_fetch_sub(&s->refcount, 1) == 1) {ff_AMediaCodec_delete(s->codec); ff_AMediaFormat_delete(s->format);ff_mediacodec_surface_unref(s->surface, NULL);}
}

四、回顾与总结

  综上所述,FFmpeg添加一个硬件解码器的关键步骤如下:

步骤关键操作
1. 配置编译修改 configure 和 Makefile,添加新解码器选项
2. 定义结构体注册 AVCodec,实现编解码器上下文
3. 初始化与配置创建 MediaCodec 实例,设置格式参数
4. 数据传递实现 send_packet 和 receive_frame,适配硬件缓冲区
5. 资源管理处理刷新、关闭和引用计数,确保无内存泄漏

  整体来说,硬件解码核心流程并不复杂,主要是要对NDK中的接口调用以及处理,相比于软件解码来说主要是在数据在CPU和GPU之间传输的不同,虽然硬件解码后支持GPU直接渲染,无需数据回传,不过若需要CPU处理(如滤镜),还需将数据从显存拷贝到系统内存。因此现代播放器常结合两者,优先尝试硬件解码,失败时回退到软件解码。鸿蒙平台和安卓很类似,无非是NDK不同罢了,不过NDK里面怎么写的那就不得而知了。(-_->


http://www.ppmy.cn/ops/163546.html

相关文章

Java面试时,该如何准备亮点?

我说个观点&#xff0c;对于在校生&#xff0c;也对于想通过社招跳槽的朋友&#xff0c;准备java项目的亮点不能光靠做业务&#xff0c;一定得通过事先定制、植入项目业务、准备说辞和准备相关问题等方式准备亮点。 先说下可能被大多数求职者写入简历并在面试时当亮点准备&…

深入理解三色标记、CMS、G1垃圾回收器

三色标记算法 简介 三色标记算法是一种常见的垃圾收集的标记算法&#xff0c;属于根可达算法的一个分支&#xff0c;垃圾收集器CMS&#xff0c;G1在标记垃圾过程中就使用该算法 三色标记法&#xff08;Tri-color Marking&#xff09;是垃圾回收中用于并发标记存活对象的核心算…

【计算机网络03】网络层协议IP(详细)

网络层协议IP 网络层的作用 在复杂的网络环境中通过IP确定目标主机的合适路径 IP协议 主机 &#xff1a;配有IP地址&#xff0c;但是不进行路由控制。路由器 &#xff1a;配有IP地址&#xff0c;能够进行路由。节点&#xff1a;主机和路由器的统称。 IP协议的报头格式 4位版本…

部署Windows Server自带“工作文件夹”实现企业网盘功能完整步骤

前文已经讲解过Windows Server自带的“工作文件夹”功能&#xff0c;现以Windows Server 2025为例介绍部署工作文件夹的完整步骤&#xff1a; 为了确保您能够顺利部署和充分利用工作文件夹的功能&#xff0c;我将按照以下步骤进行讲解。 请注意&#xff0c;在域环境中部署工作…

23种设计模式之《模板方法模式(Template Method)》在c#中的应用及理解

程序设计中的主要设计模式通常分为三大类&#xff0c;共23种&#xff1a; 1. 创建型模式&#xff08;Creational Patterns&#xff09; 单例模式&#xff08;Singleton&#xff09;&#xff1a;确保一个类只有一个实例&#xff0c;并提供全局访问点。 工厂方法模式&#xff0…

Java项目中ES作为时序库

一、ES作为时序库的核心优势 ​高写入性能​ 通过Bulk API支持批量插入/更新&#xff0c;优化吞吐量&#xff0c;适合流式数据&#xff08;如监控指标、IoT设备数据&#xff09;的高频写入。 使用Logstash作为数据管道时&#xff0c;可通过调整pipeline.workers和batch.size进…

【MySQL数据库】SQL语法基础--DQL(入门级)

在学习数据库的数据操作之前&#xff0c;我们应该先学习查询操作&#xff0c;只有学会了查询&#xff0c;后面操作我们才能看到操作后的反馈。 基础查询 select fieldlist from tablename; 解释&#xff1a;从[from]表tablename中查询&#xff0c;将字段列表fieldlist挑选[se…

FastGPT 引申:混合检索完整实例

文章目录 FastGPT 引申&#xff1a;混合检索完整实例1. 各检索方式的初始结果2. RRF合并过程3. 合并后的结果4. Rerank重排序后5. 最终RRF合并6. 内容总结 FastGPT 引申&#xff1a;混合检索完整实例 下边通过一个简单的例子说明不同检索方式的分值变化过程&#xff0c;假设我…