FFmpeg 框架简读—Bit Stream Filter

在 H.264 中有两种编码的封装格式,一种是 Annex B格式(在ISO-14496-Part 10 的附录B中),一种是 AVCC 格式,也叫 AVC1 格式或 MPEG-4 格式。

Annex B 格式是一种有 startcode 的格式,以 0x000001 或 0x0000001 分割 NALUnit(文本写NALU)。这种格式 SPS 和 PPS 是在 ES 流内,通常会周期性的在关键帧之前重复 SPS 和 PPS。

AVCC 格式是以长度信息分割 NALU 的格式,这种格式 SPS 和 PPS 以及其它信息被封装在 container 中。一般 mp4、mkv、flv 容器用该格式封装。因此 AVCC 格式的一个优点是在开始配置解码器的时候可以跳到流的中间播放,这种格式通常用于可以被随机访问的多媒体数据,如存储在硬盘的文件。

但很多解码器只支持 Annex B 这种格式,因此需要将解复用后的码流做转换,ffmpeg 提供了这样的码流过滤器,名为 AVBitStreamFilter,其中的 “h264_mp4toannexb” 就用于上述例子:将 AVCC 格式码流转为 Annex B 格式码流。

接下来看看 FFmpeg 在关于 BitStreamFilter 的部分是怎么做的:

AVBSF 与关于 FFmpeg 4.4 的改动 

AVBitStreamFilterContext 之前的接口耦合度较高。在 4.4 升级后,除了接口更名为AVBSFContext 外,更大的改动是将 BitStreamFilter 解耦。解耦后的模式类似于AVCodec 一样,将整个 BitStreamFilter 更改为 AVBSFContext + AVBitStreamFilter 的模式。由此整个 AVBitStreamFilter 流水线也变得更加独立和灵活,同时还可以一个AVBSFContext 绑定对应多个 Filter(其内部的 priv_data 字段将作为 AVBSFList 链表指向该上下文引用的所有 filter;而旧版的实现是 AVBitStreamFilterContext 中含有一个next 指针,即自身为一个链表节点)


//旧版,deprecated
typedef struct AVBitStreamFilterContext {
    other members...
    AVCodecParserContext *parser;
    struct AVBitStreamFilterContext *next; 
    other members...
} AVBitStreamFilterContext;
//新版
typedef struct AVBSFContext {
    other members...
    //使用av_bsf_alloc函数时,该Ctx仅有一个filter,使用该成员变量
    const struct AVBitStreamFilter *filter;
    //BSF内部数据,存储av_bsf_send_packet输入的AVPacket数据
    AVBSFInternal *internal;
    //使用av_bsf_list_parse_str函数,传入以逗号为分隔符的一系列filter名的字符串时
    //该变量会在内部强转为BSFListContext,即一系列filter的链表
    void *priv_data;
    other members...
} AVBSFContext;

初始化 – av_bsf_get_by_name / av_bsf_alloc / av_bsf_init 

AVBSFContext 相关的初始化函数较为简单,整个流水线为以下三个函数,使用起来也比较简单:


//1.通过filter.name传入名称寻找filter
const AVBitStreamFilter *av_bsf_get_by_name(const char *name);

//2.根据filter分配一个Context
int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx);

//2.5.(可选)拷贝AVCodecParameters参数给Filter
avcodec_parameters_copy( AVBSFContext->par_in, AVCodecParameters );

//3.init bsf ctx
int av_bsf_init(AVBSFContext *ctx);

在查看各个函数的内部实现之前,我们先了解一下关于 AVBitStreamFilter 类的定义,该类中有一个 const char* 描述名变量,作为 filter 具体的唯一标识;codec_ids 数组描述该 filter 支持的 codec 列表数组,约定要以 AV_CODEC_ID_NONE 为结尾;以及一些私有成员和函数指针。每一个具体的 AVBitStreamFilter 实例是作为全局常量存在的,这也决定了后面 av_bsf_get_by_name 的实现方式。

//定义
typedef struct AVBitStreamFilter {
    const char *name;
    const enum AVCodecID *codec_ids;

    const AVClass *priv_class;
    int priv_data_size;

    int (*init)(AVBSFContext *ctx);
    int (*filter)(AVBSFContext *ctx, AVPacket *pkt);
    void (*close)(AVBSFContext *ctx);
    void (*flush)(AVBSFContext *ctx);
} AVBitStreamFilter;

//具体实例(作为全局常量存在)
const AVBitStreamFilter ff_h264_mp4toannexb_bsf = {
    .name           = "h264_mp4toannexb",
    .priv_data_size = sizeof(H264BSFContext),
    .init           = h264_mp4toannexb_init,
    .filter         = h264_mp4toannexb_filter,
    .flush          = h264_mp4toannexb_flush,
    .codec_ids      = {AV_CODEC_ID_H264, AV_CODEC_ID_NONE,},
};

av_bsf_get_by_name

上文通过实例 我们已经看到了 name 字段可以作为 Filter 的标识,那么 av_bsf_get_by_name 是如何通过我们传入的字符串名找到的Filter呢?

参考其内部实现可以发现,av_bsf_get_by_name 做的事情非常简单:内部通过文如其名的 av_bsf_iterate 方法,遍历整个的 bitstream_filters 数组。如果 filter 名与用户传入的目标名对应,则直接返回,否则继续遍历,直至末尾返回 NULL。那么这个代表可使用 filter 的 bitstream_filters 数组又在哪可以看到呢?

继续追溯 bitstream_filters 数组的来源,发现在 ide 中并不能直接索引到其来源所在,扩大搜索范围,我们可以在 “libavcodec/bsf_list.c” 文件中找到类似于这样的数组:


static const AVBitStreamFilter * const bitstream_filters[] = {
    &ff_aac_adtstoasc_bsf,
    &ff_h264_mp4toannexb_bsf,
    中间内容略...
    &ff_vp9_superframe_split_bsf,
    NULL };

此文件并非在 git 记载的 FFmpeg 初始项目的目录里,因为这是在执行 ./configure 时根据我们输入的参数 –enable-XXX 动态生成的。之前讲到,在 configure 中我们可以配置允许的 filter、muxer、codec,每种都可能会增加一定的包大小或编译成本,所以可以在这里可以通过个人需要,精简选择所需配置。

执行完配置脚本后,libavcodec文件夹下会生成bsf_list.c、codec_list.c与parser_list.c,分别代表可选择的 filter、decoder、demuxer 列表。bitstream_filters.c文件首部通过 extern const AVBitStreamFilter XXX 的方式,将所有 ffmpeg 支持(实现过的)的 filter 引入;在实际运行时,av_bsf_iterate 的遍历会参考通过配置生成的 bsf_list.c 内部代表可以使用的 bitstream_filters 数组,以此达到动态配置的功能。

av_bsf_alloc

av_bsf_alloc 作为分配 AVBSFContext 的函数,内部几乎也只是内存申请相关的工作:

● 申请AVBSFContext与AVBSFInternal
● 申请AVBSFContext的记录输入输出参数配置的AVCodecParameters
● 申请AVBSFInternal中的AVPacket缓存,作为BSF的输入缓存
● 按需申请某些Filter的私有内容

与该函数对应的还有 av_bsf_free,纯粹与内存释放相关,内部实现暂时先不再贴出

av_bsf_init

在分配好后,需要主动调用 av_bsf_init 初始化。其内部主要做了三件事:


● 确认输入流的codec是否在filter支持的序列内,如果不支持则初始化失败直接返回
● 出入参、出入time_base拷贝
● 调用filter的init函数

输入输出部分

旧版本的接口中,filter 过滤的形式也是和解码类似的,用户同步调用 av_bitstream_filter_filter 接口,指定入参和出参为 AVPacket.data、AVPacket.size(可以指定同一个 AVPacket,以使用户在接口调用完成后可以直接使用该Packet),交由 AVBitStreamFilter 进行过滤和输出。

在 FFmpeg 4.4 中,AVBitStreamFilter 的调用方式也一同改成了异步,并废弃旧的同步接口,变动如下: 

//旧版同步接口
attribute_deprecated
int av_bitstream_filter_filter(AVBitStreamFilterContext *bsfc,
                               AVCodecContext *avctx, const char *args,
                               uint8_t **poutbuf, int *poutbuf_size,
                               const uint8_t *buf, int buf_size, int keyframe);
//新版异步接口
int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);


int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);

av_bsf_send_packet

在用户直接接触的异步接口中的 av_bsf_send_packet 中,只是做了一件事:将输入的 AVPacket,通过引用计数,转移到其内部的 AVBSFInternal->buffer_pkt 上,等待真正被加工(调用 AVBitStreamFilter 的 filter 函数指针),所以 BSF 的内部实现,虽说用户层上适配成了异步接口,但在 FFmpeg 内部还是近乎同步的,所以在 BitStreamFilter 层真正的异步需要看 filter 的内部实现是否存在一个缓存,支持过滤队列与异步返回的逻辑。

观察 FFmpeg 的历史可以发现,在 release/3.4 左右,av_bsf_send_packet 的函数就已经存在于 bsf.c 中了可以被用户直接使用。而 av_bitstream_filter_filter 的内部做法基本也是调用了一次 av_bsf_send_packet + av_bsf_receive_packet,保留了该接口的同步方式。

av_bsf_receive_frame

av_bsf_receive_frame 内部实现同样简洁明了,仅仅调用 AVBSFContext 对应 filter 的int (*filter)(AVBSFContext *ctx, AVPacket *pkt); 函数指针,进行数据流过滤。

在整个数据流水线中,某些情况下 demux 出的数据包需要经过一层处理才能输送给 decoder。为避免因用户忽略了这层处理而导致数据不兼容出错,FFmpeg 支持了在  FFmpeg 层 decoder 内附着一系列 filter 的能力,让某些需要特定数据输入格式的 decoder能够被喂以正确的数据。 

#define DECLARE_MEDIACODEC_VDEC(short_name, full_name, codec_id, bsf)                          \
DECLARE_MEDIACODEC_VCLASS(short_name)                                                          \
AVCodec ff_##short_name##_mediacodec_decoder = {                                               \
    .name           = #short_name "_mediacodec",                                               \
    略...
    .bsfs           = bsf,                                                                     \
    略...
};                                                                                             \

#if CONFIG_H264_MEDIACODEC_DECODER
DECLARE_MEDIACODEC_VDEC(h264, "H.264", AV_CODEC_ID_H264, "h264_mp4toannexb")
#endif

#if CONFIG_HEVC_MEDIACODEC_DECODER
DECLARE_MEDIACODEC_VDEC(hevc, "H.265", AV_CODEC_ID_HEVC, "hevc_mp4toannexb")
#endif

#if CONFIG_MPEG4_MEDIACODEC_DECODER
DECLARE_MEDIACODEC_VDEC(mpeg4, "MPEG-4", AV_CODEC_ID_MPEG4, NULL)
#endif

#if CONFIG_VP9_MEDIACODEC_DECODER
DECLARE_MEDIACODEC_VDEC(vp9, "VP9", AV_CODEC_ID_VP9, NULL)
#endif

故除了 demux 与 decode 之间的我们手动创建的 filter 之外,FFmpeg 也会给根据指定 bsfs 函数指针的 AVCodec 进行数据处理。例如上述例子中,根据宏动态定义的 h264/hevc decoder 中,分别对应两种格式的 filter,而传空的 MPEG4 和 vp9 则无需 filter。

根据实际应用,我们可以根据 filter 的数量,将一次数据流通的过程将分为三种类型:无需 filter、需要 1 个 filter、需要多个 filter。但因为 bsfs 声明在 AVCodec 内,考虑到所有 decoder 的兼容性和代码的通用性,即使一个 decoder 不含 filter,FFmpeg 也会在 decode 时进行 av_bsf_send_packet 操作。

依此,附着在 decoder 上的 filter,会在 init 部分 avcodec_open2 函数内,ff_(en/de) code_preinit 中的 decode_bsfs_init 已经对需要 BitStreamFilter 的 AVCodec 进行了初始化: 主要函数av_bsf_list_parse_str 函数做了两个工作:bsf_parse_single 进行每个解析逗号分隔符后的每个 filter 名,通过 find_by_name 找到对应 filter 添加到 list 上;av_bsf_list_finalize 进行判断最后的 filter 内部具体形式(是一个 filter 还是一个 filter 链表),并添加到 AVBSFContext 内作为出参返回。 

● 若场景只需要1个filter,在av_bsf_list_finalize的尾处理中,会直接将AVBSFList的头节点作为该AVBSFContext并返回,无需包装一个链表。
● 若无需filter或需要多个filter,他们的模式是一样的:返回一个“内部承载了一个FilterList”的AVBSFContext。当这种类型的上下文被下达“filter”指令进行数据过滤,其会利用内部的priv_data字段,强转成(BSFListContext*)类型的指针,而BSFListContext内部的AVBSFContext节点内记载着真正的filter。
  ○ 若无需filter,即AVCodec中的.bsfs字符串为NULL,内部会经过av_bsf_get_null_filter返回filter数为0的BSFListContext类型空链表。
  ○ 需要多个filter,则会根据字符串解析,通过int av_bsf_list_append(AVBSFList *lst, AVBSFContext *bsf);接口,将每个filter添加到AVBSFList中。在最后的尾处理函数以BSFListContext记录(承载)该链表。
//相关公开接口(用户可直接调用),位于libavcodec/bsf.h
int av_bsf_list_append(AVBSFList *lst, AVBSFContext *bsf);
int av_bsf_list_append2(AVBSFList *lst, const char * bsf_name, AVDictionary **options);
int av_bsf_list_finalize(AVBSFList **lst, AVBSFContext **bsf);//上面提到的"尾处理"
int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf);

作者:肖杨
公众号:流媒体技术
链接:https://mp.weixin.qq.com/s/Uq6VFrATKQ_N7ttsgqhCIg

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(1)

相关推荐

发表回复

登录后才能评论