如何将编解码器添加到FFmpeg


FFmpeg是一个宏大的开源项目,是一种多媒体百科全书。 使用FFmpeg,您可以解决大量的计算机多媒体任务。 但是,有时仍需要扩展FFmpeg。 标准方法是更改​​项目代码,然后编译新版本。 本文详细介绍了如何添加新的编解码器。 还考虑了一些将外部功能连接到FFmpeg的功能。 如果不需要添加编解码器,那么这篇文章可能对更好地了解FFmpeg编解码器的体系结构及其设置很有用。 假定读者熟悉FFmpeg的体系结构,FFmpeg的编译过程,并且具有使用FFmpeg API的编程经验。 该说明适用于FFmpeg 4.2“ Ada”,2019年8月。



目录



引言


编解码器(编解码器,来自术语COder和DECoder的组合)是一个非常常见的术语,在这种情况下经常发生,其含义会根据上下文而有所不同。 主要含义是用于压缩/解压缩媒体数据的软件或硬件。 代替术语压缩/解压缩,经常使用术语编码/解码。 但是在某些情况下,通常将编解码器理解为仅表示压缩格式(也称为编解码器格式),而不考虑用于压缩/解压缩的方式。 让我们看看如何在FFmpeg中使用术语“编解码器”。



1.编解码器识别


FFmpeg编解码器在libavcodec库中进行编译。



1.1。 编解码器ID


enum AVCodecIDlibavcodec/avcodec.h enum AVCodecID定义。 该枚举的每个元素都标识压缩格式。 此枚举的元素必须采用AV_CODEC_ID_XXX的形式,其中XXX大写XXX唯一编解码器标识符名称。 以下是编解码器标识符的示例: AV_CODEC_ID_H264AV_CODEC_ID_AAC 。 有关编解码器标识符的更详细说明,请使用AVCodecDescriptor结构(在libavcodec/avcodec.h声明,以缩写形式给出):


 typedef struct AVCodecDescriptor { enum AVCodecID id; enum AVMediaType type; const char *name; const char *long_name; // ... } AVCodecDescriptor; 

此结构的关键成员是id ,其余成员提供有关编解码器标识符的其他信息。 每个编解码器标识符与媒体类型( type成员)唯一关联,并具有唯一的名称( name成员),以小写形式编写。 在文件libavcodec/codec_desc.c AVCodecDescriptor定义了AVCodecDescriptor类型的数组。 对于每个编解码器标识符,都有一个对应的数组元素。 该数组的元素应按id值排序,因为二进制搜索用于搜索元素。 要获取有关编解码器标识符的信息,可以使用以下功能:


 const AVCodecDescriptor* avcodec_descriptor_get(enum AVCodecID id); const AVCodecDescriptor* avcodec_descriptor_get_by_name(const char *name); enum AVMediaType avcodec_get_type(enum AVCodecID codec_id); const char* avcodec_get_name(enum AVCodecID id); 


1.2。 编解码器


编解码器本身-执行媒体数据编码/解码所必需的一组工具,结合了AVCodec结构(在libavcodec/avcodec.h声明)。 这是它的缩写版本,下面将讨论更完整的版本。


 typedef struct AVCodec { const char *name; const char *long_name; enum AVMediaType type; enum AVCodecID id; // ... } AVCodec; 

该结构最重要的成员是id ,即编解码器标识符,还有一个成员定义了媒体的类型( type ),但其值必须与AVCodecDescriptor相同成员的值匹配。 编解码器分为两类:对媒体进行压缩或编码的编码器和执行相反操作(解压缩或解码)的解码器。 (有时用俄语文本代替编码器,而不是术语,编码器使用英语的描图纸-编码器。) AVCodec没有定义编解码器类别的特殊成员(尽管可以使用函数av_codec_is_encoder()av_codec_is_decoder()间接确定类别,该类别是在注册期间确定的。具体操作如下:多个编解码器可以具有相同的编解码器标识符;如果它们具有相同的类别,则它们的名称必须不同(成员name );具有相同编解码器标识符的编码器和解码器可以有一个 相同的名称,也可能与编解码器标识符的名称重合(但这些匹配项是可选的)。这种情况可能会引起混淆,但是您无须做任何事情,您必须清楚地了解该名称属于哪个实体。编解码器必须唯一,要搜索注册的编解码器,请使用以下功能:


 AVCodec* avcodec_find_encoder_by_name(const char *name); AVCodec* avcodec_find_decoder_by_name(const char *name); AVCodec* avcodec_find_encoder(enum AVCodecID id); AVCodec* avcodec_find_decoder(enum AVCodecID id); 

由于多个编解码器可以具有相同的标识符,因此后两个函数将返回其中一个,可以将其视为给定编解码器标识符的默认编解码器。


可以使用以下命令请求所有已注册编解码器的列表


ffmpeg -codecs >codecs.txt


执行命令后, codecs.txt文件将包含此列表。 每个编解码器标识符将由单独的记录(行)表示。 在此,例如,编解码器标识符AV_CODEC_ID_H264的条目:


DEV.LS
h264
H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
(decoders: h264 h264_qsv h264_cuvid)
(encoders: libx264 libx264rgb h264_amf h264_nvenc h264_qsv nvenc nvenc_h264)


在录制开始时,有一些特殊字符确定此编解码器标识符的可用常用功能: D已注册的解码器, E已注册的编码器, V用于视频的视频, L可能存在有损压缩, S可能存在无损压缩。 接下来是编解码器标识符名称( h264 ),然后是长编解码器标识符名称( H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 ),然后是已注册的解码器和编码器的名称列表。



2.向FFmpeg添加新的编解码器


我们将使用音频编解码器示例(称为FROX考虑向FFmpeg添加新编解码器的过程。


步骤1.向enum AVCodecID添加一个新元素。


此清单位于libavcodec/avcodec.h 。 添加时,必须遵循以下规则:


  1. 元素的值不得与现有枚举元素的值一致;
  2. 不要更改现有枚举元素的值;
  3. 在一组类似的编解码器中发布新值。

根据模板,此元素的标识符应为AV_CODEC_ID_FROX 。 将其AV_CODEC_ID_PCM_S64LE之前,并提供值0x10700


步骤2.将项目添加到codec_descriptors数组(文件libavcodec/codec_desc.c )。


 static const AVCodecDescriptor codec_descriptors[] = { // ... { .id = AV_CODEC_ID_FROX, .type = AVMEDIA_TYPE_AUDIO, .name = "frox", .long_name = NULL_IF_CONFIG_SMALL("FROX audio"), .props = AV_CODEC_PROP_LOSSLESS, }, // ... }; 

您需要将元素添加到“正确的”位置, id值不违反数组元素的单调性。


步骤3.为编码器和解码器分别定义AVCodec实例。


为此,您首先需要确定编解码器上下文的结构以及执行实际编码/解码和其他一些必要操作的几个功能。 在本节中,将非常示意性地进行这些定义;稍后将进行更详细的描述。 我们将代码放置在文件libavcodec/frox.c


 #include "avcodec.h" // context typedef struct FroxContext { // ... } FroxContext; // decoder static int frox_decode_init(AVCodecContext *codec_ctx) { return -1; } static int frox_decode_close(AVCodecContext *codec_ctx) { return -1; } static int frox_decode(AVCodecContext *codec_ctx, void* outdata, int *outdata_size, AVPacket *pkt) { return -1; } AVCodec ff_frox_decoder = { .name = "frox_dec", .long_name = NULL_IF_CONFIG_SMALL("FROX audio decoder"), .type = AVMEDIA_TYPE_AUDIO, .id = AV_CODEC_ID_FROX, .priv_data_size = sizeof(FroxContext), .init = frox_decode_init, .close = frox_decode_close, .decode = frox_decode, .capabilities = AV_CODEC_CAP_LOSSLESS, .sample_fmts = (const enum AVSampleFormat[]) {AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_NONE}, .channel_layouts = (const int64_t[]) {AV_CH_LAYOUT_MONO, 0 }, }; // encoder static int frox_encode_init(AVCodecContext *codec_ctx) { return -1; } static int frox_encode_close(AVCodecContext *codec_ctx) { return -1; } static int frox_encode(AVCodecContext *codec_ctx, AVPacket *pkt, const AVFrame *frame, int *got_pkt_ptr) { return -1; } AVCodec ff_frox_encoder = { .name = "frox_enc", .long_name = NULL_IF_CONFIG_SMALL("FROX audio encoder"), .type = AVMEDIA_TYPE_AUDIO, .id = AV_CODEC_ID_FROX, .priv_data_size = sizeof(FroxContext), .init = frox_encode_init, .close = frox_encode_close, .encode2 = frox_encode, .sample_fmts = (const enum AVSampleFormat[]) {AV_SAMPLE_FMT_S16, AV_SAMPLE_FMT_NONE}, .channel_layouts = (const int64_t[]) {AV_CH_LAYOUT_MONO, 0 }, }; 

为简单起见,在此示例中,编码器和解码器具有相同的上下文FroxContext ,但最常见的是编码器和解码器具有不同的上下文。 还要注意, AVCodec实例AVCodec必须遵循特殊的模式。


步骤4.将AVCodec实例添加到注册列表。


转到文件libavcodec/allcodecs.c 。 该文件的开头是所有已注册编解码器的声明列表。 将我们的编解码器添加到此列表:


 extern AVCodec ff_frox_decoder; extern AVCodec ff_frox_encoder; 

在执行期间, configure脚本会找到所有此类声明,并生成libavcodec/codec_list.c ,该libavcodec/codec_list.c包含指向在libavcodec/allcodecs.c声明的编解码器的指针数组。 在文件libavcodec/codec_list.c执行脚本后,我们将看到:


 static const AVCodec * const codec_list[] = { // ... &ff_frox_encoder, // ... &ff_frox_decoder, // ... NULL }; 

同样,在执行configure脚本的过程中,会config.h文件,在其中找到声明


 #define CONFIG_FROX_DECODER 1 #define CONFIG_FROX_ENCODER 1 

步骤5.编辑libavcodec/Makefile


打开libavcodec/Makefile 。 我们找到# decoders/encoders ,并在其中添加


 OBJS-$(CONFIG_FROX_DECODER) += frox.o OBJS-$(CONFIG_FROX_ENCODER) += frox.o 

步骤6.编辑多路复用器和解复用器的代码。


多路复用器(muxer)和多路分解器(demuxer)必须“知道”新编解码器。 在记录时,有必要记录该编解码器的标识信息,同时在读取时根据标识信息确定编解码器的标识符。 这是您需要对matroska格式( *.mkv )进行的操作。


1.在文件libavformat/matroska.c ,将新编解码器的元素添加到libavformat/matroska.c数组中:


 const CodecTags ff_mkv_codec_tags[] = { // ... {"A_FROX", AV_CODEC_ID_FROX}, // ... }; 

字符串"A_FROX" ,将由多路复用器写入文件作为标识信息。 在此阵列中,它与编解码器标识符关联,因此,在读取时,多路分解器可以轻松确定它。 解复用器将编解码器标识符写入codec_id结构的codec_id成员。 指向此结构的指针是AVStream结构的成员。


2.在文件libavformat/matroskaenc.c ,将元素添加到libavformat/matroskaenc.c数组中:


 static const AVCodecTag additional_audio_tags[] = { // ... { AV_CODEC_ID_FROX, 0XFFFFFFFF }, // ... }; 

一切准备就绪。 首先,运行configure脚本。 之后,您需要确保对文件libavcodec/codec_list.cconfig.h中的上述内容进行了更改。 然后,您可以运行编译:


make clean
make


如果编译进行ffmpeg.exe ,则会显示ffmpeg可执行文件(如果目标操作系统为Windows,则为ffmpeg.exe )。 执行命令


./ffmpeg -codecs >codecs.txt


并确保FFmpeg“看到”了我们的新编解码器,我们在codecs.txt文件中找到了该条目


DEA..S frox FROX audio (decoders: frox_dec) (encoders: frox_enc)



3.上下文和所需功能的详细说明


在本节中,我们将更详细地描述编解码器上下文的结构和必要的功能。



3.1。 编解码器上下文


编解码器上下文可以支持选项的安装。 对于编码器,此支持经常使用,而对于解码器则较少使用。 支持安装选件的结构应首先指向AVClass结构,然后再指向选件本身。


 #include "libavutil/opt.h" typedef struct FroxContext { const AVClass *av_class; int frox_int; char *frox_str; uint8_t *frox_bin; int bin_size; } FroxContext; 

接下来,您需要定义一个AVOption类型的数组,其每个元素都描述一个特定的选项。


 static const AVOption frox_options[] = { { "frox_int", "This is a demo option of int type.", offsetof(FroxContext, frox_int), AV_OPT_TYPE_INT, { .i64 = -1 }, 1, SHRT_MAX }, { "frox_str", "This is a demo option of string type.", offsetof(FroxContext, frox_str), AV_OPT_TYPE_STRING }, { "frox_bin", "This is a demo option of binary type.", offsetof(FroxContext, frox_bin), AV_OPT_TYPE_BINARY }, { NULL }, }; 

对于每个选项,必须在结构,类型中定义名称,描述,偏移量。 您还可以定义一个默认值,对于整数选项,可以定义一个有效值范围。


接下来,您需要定义一个AVClass类型的实例。


 static const AVClass frox_class = { .class_name = "FroxContext", .item_name = av_default_item_name, .option = frox_options, .version = LIBAVUTIL_VERSION_INT, }; 

指向该实例的指针必须用于初始化相应的AVCodec成员。


 AVCodec ff_frox_decoder = { // ... .priv_data_size = sizeof(FroxContext), .priv_class = &frox_class, // ... }; AVCodec ff_frox_encoder = { // ... .priv_data_size = sizeof(FroxContext), .priv_class = &frox_class, // ... }; 

现在执行功能时


 AVCodecContext *avcodec_alloc_context3(const AVCodec *codec); 

AVCodecContext结构的实例,并初始化codec成员。 接下来,基于codec->priv_class使用codec->priv_class FroxContext实例分配必要的内存,将初始化该实例codec->priv_class第一个成员,然后将av_opt_set_defaults()函数,这将为选项设置默认值。 可以通过priv_data结构的priv_data成员获得指向FroxContext实例的指针。


使用FFmpeg API时,可以直接设置选项的值。


 const AVCodec *codec; // ... AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); // ... av_opt_set(codec_ctx->priv_data, "frox_str", "meow", 0); av_opt_set_int(codec_ctx->priv_data, "frox_int", 42, 0); 

另一种方法是使用选项字典,该字典将在调用avcodec_open2()时作为第三个参数传递(请参见下文)。


使用功能


 const AVOption* av_opt_next(const void* ctx, const AVOption* prev); 

您可以获取编解码器上下文支持的所有选项的列表。 这在检查编解码器时很有用。 但是在此之前,必须确保将codec_ctx->codec->priv_class设置为非零值,否则上下文不支持options,并且任何带有options的操作都会使程序崩溃。



3.2。 功能介绍


现在让我们更详细地研究编解码器的初始化和实际的编码/解码中使用的功能是如何安排的。 他们通常总是需要获取一个指向FroxContext的指针。


 AVCodecContext *codec_ctx; // ... FroxContext* frox_ctx = codec_ctx->priv_data; 

执行该函数时,将调用frox_decode_init()frox_encode_init()函数


 int avcodec_open2( AVCodecContext *codec_ctx, const AVCodec *codec, AVDictionary **options); 

他们需要分配必要的资源以使编解码器正常工作,并在必要时初始化AVCodecContext结构的某些成员,例如,用于音频frame_size


执行时将调用frox_decode_close()frox_encode_close()函数


 int avcodec_close(AVCodecContext *codec_ctx); 

他们需要释放分配的资源。


考虑实现解码的功能


 int frox_decode( AVCodecContext *codec_ctx, void *outdata, int *outdata_size, AVPacket *pkt); 

她应该执行以下操作:


  1. 实际解码;
  2. 为输出帧分配必要的缓冲区;
  3. 将解码的数据复制到帧缓冲区。

考虑如何为输出帧分配必要的缓冲区。 outdata参数实际上指向AVFrame ,因此您必须首先执行类型转换:


 AVFrame* frm = outdata; 

接下来,您需要分配用于存储帧数据的缓冲区。 为此,请初始化确定帧缓冲区大小的AVFrame成员。 对于音频,这是nb_sampleschannel_layoutformat (对于视频widthheightformat )。


之后,您需要调用该函数


 int av_frame_get_buffer(AVFrame* frm, int alignment); 

指向框架的指针是转换后的outdata参数,用作第一个参数;建议将零作为第二个参数传递。 使用帧之后(此情况已在编解码器外部发生),此函数分配的缓冲区由该函数释放


 void av_frame_unref(AVFrame* frm); 

frox_decode()函数应从pkt指向的包中返回用于解码的字节数。 如果完成了帧形成,则将outdata_size指向的变量分配一个非零值,否则该变量将获得值0


考虑实现编码的功能


 int frox_encode( AVCodecContext *codec_ctx, AVPacket *pkt, const AVFrame *frame, int *got_pkt_ptr); 

她应该执行以下操作:


  1. 实际编码;
  2. 为输出数据包分配必要的缓冲区;
  3. 将编码的数据复制到数据包缓冲区。

要选择所需的缓冲区,请使用函数


 int av_new_packet(AVPacket *pkt, int pack_size); 

参数pkt用作第一个参数,编码数据的大小为第二个参数。 使用该程序包后(这种情况已经在编解码器外部发生),该函数分配的缓冲区由该函数释放


 void av_packet_unref(AVPacket *pkt); 

如果包已完成,则将got_pkt_ptr指向的变量分配为非零值,否则该变量的值为0 。 如果没有错误,则函数返回零,否则返回错误代码。


在实施编解码器时,通常使用日志记录(对于错误,可以将其视为强制性要求)。 这是一个例子:


 static int frox_decode_close(AVCodecContext *codec_ctx) { av_log(codec_ctx, AV_LOG_INFO, "FROX decode close\n"); // ... } 

在这种情况下,当输出到日志时,编解码器名称将用作上下文名称。



3.3。 时标


要以FFmpeg设置时间,将使用时基,以AVRational类型表示的有理数以秒为单位指定。 (在C ++ 11中使用类似的方法。例如,1/1000设置毫秒。)帧和数据包的时间戳类型为int64_t ,它们的值包含相应时间单位的时间。 帧(即AVFrame结构)具有成员pts (表示时间戳),其值确定在帧中捕获的场景的相对时间。 程序包即AVPacket结构,具有成员pts (表示时间戳记)和dts (解压缩时间戳记)。 值dts确定了要解码的数据包传输的相对时间。 对于简单的编解码器,它与pts相同,但是对于复杂的编解码器,它可以有所不同(例如,对于使用B帧的h264 ),即,可以按照错误的顺序解码数据包,而应使用帧。


为流和编解码器定义了时间单位, AVStream结构具有相应的成员time_base ,同一成员具有AVCodecContext结构。


使用av_read_frame()从流中提取的数据包的时间戳将以该流的时间单位指定。 解码时,不使用编解码器的时间单位。 对于视频解码器,通常根本不会设置它,对于音频解码器,它具有标准值-采样频率的倒数。 解码器应根据数据包的时间戳为输出帧设置时间戳。 FFmpeg独立定义此类标签,并将其写入best_effort_timestamp结构的best_effort_timestamp成员。 所有这些时间戳将使用从中提取数据包的流的时间单位。


对于编码器,必须指定时间单位。 在组织解码的客户端代码中,必须在调用avcodec_open2()之前为time_base结构的time_base成员设置值。 通常采用用于编码帧的时间戳的时间单位。 如果不这样做,则视频编码器通常会出现错误,音频编码器会设置默认值-采样频率的倒数。 编解码器是否可以更改给定的时间单位尚不完全清楚。 以防万一,最好在调用avcodec_open2()之后始终检查time_base值,如果已更改,请重新计算编解码器每单位时间输入帧的时间戳。 在编码过程中,必须安装软件包的ptsdts 。 编码后,在将数据包写入输出流之前,必须重新计算从编解码器时间单位到数据流时间单位的数据包时间戳。 为此,请使用函数


 void av_packet_rescale_ts( AVPacket *pkt, AVRational tb_src, AVRational tb_dst); 

将数据包写入流时,必须确保dts值严格增加,否则多路复用器将引发错误。 (有关更多信息,请参见av_interleaved_write_frame()函数的文档。)



3.4。 编解码器使用的其他功能


初始化AVCodec实例时,可以再注册两个功能。 以下是AVCodec的相关成员:


 typedef struct AVCodec { // ... void (*init_static_data)(AVCodec *codec); void (*flush)(AVCodecContext *codec_ctx); // ... } AVCodec; 

注册编解码器后,将首先调用它们中的第一个。


第二个重置编解码器的内部状态,它将在函数执行期间被调用


 void avcodec_flush_buffers(AVCodecContext *codec_ctx); 

例如,在强行更改当前播放位置时,此调用是必需的。



4.编解码器的外部实现



4.1。 外部功能连接


考虑以下编解码器组织:在FFmpeg中注册的编解码器充当框架的角色,并将实际的编码/解码过程委派给在FFmpeg外部实现的外部功能(某种插件)。


. 以下是其中一些:


  1. , FFmpeg ;
  2. C, , C++;
  3. framework, FFmpeg.

, FFmpeg «», FFmpeg API. «» FFmpeg ( , ), . — . .


 typedef int(*dec_extern_t)(const void*, int, void*); static int frox_decode( AVCodecContext* codec_ctx, void* outdata, int *outdata_size, AVPacket* pkt) { int ret = -1; void* out_buff; //      out_buff FroxContext *fc = codec_ctx->priv_data; if (fc->bin_size > 0) { if (fc->bin_size == sizeof(dec_extern_t)) { dec_extern_t edec; memcpy(&edec, fc->frox_bin, fc->bin_size); ret = (*edec)(pkt->data, pkt->size, out_buff); if (ret >= 0) { //     out_buff   } } else { /*  */ } } else { /*    */ } // ... return ret; } 

FFmpeg API ( C++) .


 extern "C" { int DecodeFroxData(const void* buff, int size, void* outBuff); typedef int(*dec_extern_t)(const void*, int, void*); #include <libavcodec/avcodec.h> #include <libavutil/opt.h> } // ... AVCodecContext* ctx; // ... dec_extern_t dec = DecodeFroxData; void* pv = &dec; auto pb = static_cast<const uint8_t*>(pv); auto sz = sizeof(dec); av_opt_set_bin(ctx->priv_data, "frox_bin", pb, sz, 0); 


4.2.


— . , . , . , , FFmpeg , «» , . . , . FFmpeg API - , , . . , . PC (Windows) DirectShow AVI . PC - DirectShow. 32- FourCC. ( biCompression BITMAPINFOHEADER .) , DirectShow , PC -. FFmpeg , , , codec_tag AVCodecParameters FourCC, . FFmpeg API , . FFmpeg FFmpeg API.


, *.mkv FFmpeg ( ENCODER ).



结论


, , FFmpeg: , changelog, .. «» FFmpeg, , .



资源资源


FFmpeg


[1] FFmpeg —
[2] FFmpeg —
[3] FFmpeg —
[4] FFmpeg — Ubuntu



[5] FFmpeg Compilation Guide
[6] Compilation of FFmpeg 4.0 in Windows 10


FFmpeg API


[7] ffmpeg



[8] FFmpeg codec HOWTO
[9] FFmpeg video codec tutorial




Source: https://habr.com/ru/post/zh-CN480714/


All Articles