当前位置: 首页 > article >正文

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

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.kler.cn/a/574207.html

相关文章:

  • 华为OD机试-发现新词的数量(Java 2024 E卷 100分)
  • JAVA实现有趣的迷宫小游戏(附源码)
  • 【算法day2】无重复字符的最长子串 两数之和
  • YOLOv8改进SPFF-LSKA大核可分离核注意力机制
  • linux上配置免密登录
  • react中的fiber和初次渲染
  • 爬虫逆向:脱壳工具Youpk的使用详解
  • rust笔记12:rust的泛型
  • 计网学习———网络安全
  • Uniapp使用wxml-to-canvas进行动态页面转图片
  • Better-SQLite3 参数绑定详解
  • 多模态模型在做选择题时,如何设置Prompt,如何精准定位我们需要的选项
  • 装饰器模式:灵活扩展对象功能的利器
  • 如何高效使用 Mybatis-Plus 的批量操作
  • CDH下配置Flume进行配置传输日志文件
  • 深入探究C++并发编程:信号 异步 原子
  • muduo库源码分析:TcpConnection 类
  • better-sqlite3之exec方法
  • 【深度学习】Adam(Adaptive Moment Estimation)优化算法
  • dify + ollama + deepseek-r1+ stable-diffusion 构建绘画智能体