大家好!我是大聪明-PLUS!

FFmpeg 是一个庞大的开源项目,堪称多媒体百科全书。它能够解决大量的计算机多媒体问题。然而,有时我们需要扩展 FFmpeg 的功能。通常的做法是修改项目代码,然后编译新版本。本文将详细介绍如何添加新的编解码器,并介绍一些将外部函数连接到 FFmpeg 的方法。即使您不需要添加编解码器,本文也有助于您更好地理解 FFmpeg 编解码器的架构及其配置。本文假设读者熟悉 FFmpeg 架构、FFmpeg 编译过程以及使用 FFmpeg API 的编程经验。本说明适用于 2019 年 8 月发布的 FFmpeg 4.2 "Ada" 版本。
目录
目录
介绍
编解码器(Codec,由"编码器"(COder)和"解码器"(DECoder)组合而成)是一个非常常见的术语,而且通常情况下,它的含义会根据上下文略有不同。其基本定义是用于压缩/解压缩媒体数据的软件或硬件工具。人们常常使用"编码/解码"来代替"压缩/解压缩"。然而,在某些情况下,编解码器仅仅被理解为一种压缩格式(也称为编解码器格式),而与所使用的压缩/解压缩方法无关。让我们来看看 FFmpeg 中是如何使用"编解码器"这个术语的。
1. 编解码器识别
FFmpeg 编解码器收集在libavcodec库中。
1.1. 编解码器 ID
该文件libavcodec/avcodec.h定义了一个枚举enum AVCodecID。此枚举的每个元素标识一种压缩格式。此枚举的元素必须采用 `<codec_identifier>` 的形式AV_CODEC_ID_XXX,其中 `<codec_identifier>` 是XXX唯一的大写编解码器标识符名称。编解码器标识符的示例包括:` AV_CODEC_ID_H264<codec_identifier>`、` AV_CODEC_ID_AAC<codec_identifier>` 和 `<codec_identifier>`。要更详细地描述编解码器标识符,请使用以下结构AVCodecDescriptor(在 `<codec_identifier>` 中声明libavcodec/avcodec.h,以下为缩写形式):
typedef` `struct` `AVCodecDescriptor` {
`enum` `AVCodecID` `id`;
`enum` `AVMediaType` `type`;
`const` `char` `*name`;
`const` `char` `*long_name`;
`// ...`
} `AVCodecDescriptor`;`
该结构的关键成员是 ` id<codec_identifier>`,其余成员提供有关编解码器标识符的附加信息。每个编解码器标识符都唯一地关联到一个媒体类型(成员 `<media_type> type`),并具有一个唯一的名称(成员 `<name> name`),该名称以小写字母书写。该文件libavcodec/codec_desc.c定义了一个 `<codec_identifier>` 类型的数组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。编解码器分为两类:编码器,用于执行媒体数据的压缩或编码;解码器,用于执行相反的操作------解压缩或解码。 (在俄语文本中,有时会使用英文术语"encoder"代替"coder"。)[ AVCodeccodec/codec] 中没有专门定义编解码器类别的成员(尽管可以使用 [codec/codec] 和 [codec/codec] 函数间接确定类别av_codec_is_encoder();av_codec_is_decoder()此类别在注册期间确定。具体方法将在下文说明)。多个编解码器可以具有相同的编解码器标识符。如果它们属于同一类别,则必须通过名称(成员 [codec/codec 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`);`
由于多个编解码器可以具有相同的 ID,因此最后两个函数返回其中一个,该编解码器可以被视为给定编解码器 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 添加新的编解码器
让我们以音频编解码器为例,来看一下向 FFmpeg 添加新编解码器的步骤,我们将其称为FROX。
步骤 1. 向枚举中添加新元素enum AVCodecID。
此枚举位于文件中libavcodec/avcodec.h。添加时,请遵循以下规则:
- 该元素的值不得与枚举中现有元素的值相等;
- 不要更改现有枚举元素的值;
- 将新值放入一组相似的编解码器中。
根据模板,该元素的 ID 应该是AV_CODEC_ID_FROX。让我们把它放在前面AV_CODEC_ID_PCM_S64LE,并赋予它值0x10700。
codec_descriptors步骤 2. 向数组(文件)添加元素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。
AVCodec步骤 3.分别定义编码器和解码器的实例。
为此,我们首先需要定义编解码器上下文的结构以及几个用于执行实际编码/解码和其他必要操作的函数。本节中的定义将非常简略,更详细的描述将在后续章节中给出。代码将放在文件中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必须遵循特定模式。
步骤 4. 将实例添加AVCodec到注册列表。
我们打开这个文件libavcodec/allcodecs.c。文件开头列出了所有已注册编解码器的声明。让我们把我们的编解码器添加到这个列表中:
extern` `AVCodec` `ff_frox_decoder`;
`extern` `AVCodec` `ff_frox_encoder`;`
脚本执行期间,会查找所有此类声明,并生成一个包含指向已声明编解码器的指针数组的configure文件。脚本执行完毕后,我们将在该文件中看到以下内容: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. 编辑多路复用器和解复用器代码。
多路复用器和解复用器必须能够识别新的编解码器。写入时,它们必须记录该编解码器的标识信息;读取时,它们必须根据该标识信息确定编解码器的 ID。以下是针对该格式matroska(文件*.mkv)需要执行的操作。
1.在文件中的libavformat/matroska.c数组中ff_mkv_codec_tags添加新编解码器的元素:
const` `CodecTags` `ff_mkv_codec_tags`[] `=` {
`// ...`
{`"A_FROX"`, `AV_CODEC_ID_FROX`},
`// ...`
};`
多路复用器会将该字符串"A_FROX"作为标识信息写入文件。在此数组中,它与编解码器标识符关联,以便解复用器在读取时能够轻松识别。解复用器将编解码器标识符写入codec_id结构体的一个成员AVCodecParameters。指向该结构体的指针是结构体的一个成员AVStream。
2.libavformat/matroskaenc.c向文件中的数组additional_audio_tags添加一个元素:
static` `const` `AVCodecTag` `additional_audio_tags`[] `=` {
`// ...`
{ `AV_CODEC_ID_FROX`, `0XFFFFFFFF` },
`// ...`
};`
一切准备就绪。首先,我们运行脚本configure。之后,我们需要确保上述文件更改libavcodec/codec_list.c已config.h生效。然后,我们就可以运行编译程序了:
make clean
make
如果编译成功,将会生成一个可执行文件ffmpeg(ffmpeg.exe如果目标操作系统是 Windows 则为 .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_data_size该实例分配必要的内存FroxContext。然后,使用该值初始化该实例的第一个成员codec->priv_class,并调用相应的函数来设置选项的默认值。可以通过结构体成员访问指向av_opt_set_defaults()该实例的指针。FroxContext``priv_data``AVCodecContext
使用 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设置为非零值。否则,上下文将不支持任何选项,任何使用这些选项的操作都会导致程序崩溃。
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`);`
它必须执行以下操作:
- 实际解码;
- 为输出帧分配所需的缓冲区;
- 将解码后的数据复制到帧缓冲区。
我们来看看如何为输出帧分配必要的缓冲区。该参数outdata实际上指向AVFrame,因此我们首先需要进行类型转换:
AVFrame*` `frm` `=` `outdata`;`
接下来,我们需要分配一个缓冲区来存储帧数据。为此,我们需要初始化AVFrame决定帧缓冲区大小的成员。对于音频,这些成员是nb_samples, channel_layout, format(对于视频width,这些成员是height, format)。
之后你需要调用该函数。
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`);`
它必须执行以下操作:
- 实际编码;
- 为输出数据包分配所需的缓冲区;
- 将编码后的数据复制到数据包缓冲区。
使用该函数可以分配所需的缓冲区。
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 使用时间基准来指定时间,以秒为单位,使用类型为 `Timebase` 的有理数表示AVRational。(C++11 也采用了类似的方法。例如,1/1000 表示毫秒。)帧和数据包都具有类型为 `TimesTimes` 的时间戳;int64_t它们的值包含相应时间单位的时间。帧(即结构体)AVFrame有一个成员 ` pts(presentation timestamp)`,其值决定了帧中捕获场景的相对时间。数据包(即结构体)AVPacket有两个成员 ` pts(presentation timestamp)` 和 ` dts(decompression timestamp)`。`(decompression timestamp)` 的值dts决定了数据包解码传输的相对时间。对于简单的编解码器,此值与 `(presentation timestamp)` 相同pts,但对于复杂的编解码器,它可能不同(例如,h264使用 B 帧时),这意味着数据包的解码顺序可能与帧的使用顺序不同。
为流和编解码器定义了时间单位,结构体AVStream有一个对应的成员 - time_base,结构体具有相同的成员AVCodecContext。
从流中提取的数据包的时间戳av_read_frame()将以该流的时间单位指定。解码过程中不使用编解码器的时间单位。对于视频解码器,编解码器的时间单位通常不指定;对于音频解码器,其默认值为采样频率的倒数。解码器必须根据数据包的时间戳设置输出帧的时间戳。FFmpeg 会自动确定此类时间戳并将其写入best_effort_timestamp结构体成员AVFrame。所有这些时间戳都将使用提取数据包的流的时间单位。
编码器必须指定时间单位。在负责解码的客户端代码中,必须在调用 `getTimeout` 之前设置time_base结构体成员的值。通常情况下,会使用编码帧时间戳的时间单位。如果未设置,视频编码器通常会返回错误,而音频编码器则会设置默认值------采样频率的倒数。编解码器是否可以更改指定的时间单位尚不完全清楚。为了安全起见,最好在调用 `getTimeout` 之后检查该值,如果已更改,则将输入帧的时间戳重新计算为编解码器的时间单位。在编码过程中,需要设置`getTimeout` 和 `getTimeout`数据包。编码完成后,在将数据包写入输出流之前,需要将数据包的时间戳从编解码器的时间单位重新计算为流的时间单位。为此,可以使用 `getTimeout` 函数。AVCodecContext``avcodec_open2()``time_base``avcodec_open2()``pts``dts
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 之外实现的外部函数(一种插件)。
这种解决方案可能出于多种原因而受到青睐。以下列举一些原因:
- 该编解码器处于实验阶段,经常发生变化,编译 FFmpeg 是一个相当费力的过程;
- 该编解码器不是用 C 语言编写的,而是用其他语言编写的,例如 C++;
- 该编解码器使用的库或框架难以集成到 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`;
`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`) {
}
}
`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 客户端能够识别出这种编解码器并解码使用该编解码器编码的媒体数据,那么就可以播放此类媒体。作者也有类似的经历。我曾经使用过一款采用专有格式硬件压缩的 DVR。压缩数据被传输到一台 PC(Windows 系统),然后使用 DirectShow 录制成 AVI 文件。这台 PC 上有一个用于此格式的软件解码器,并且已经编写了一个使用该解码器的 DirectShow 解码器过滤器。该格式使用 32 位 FourCC(存储在biCompression结构体成员中BITMAPINFOHEADER)进行识别。因此,只要电脑上安装了此解码器过滤器,这些文件就可以在任何 DirectShow 播放器上播放。尝试使用 FFmpeg 播放器播放此类文件时,自然找不到解码器,但codec_tag结构体成员AVCodecParameters包含前面提到的 FourCC,从而解决了编解码器识别问题。基于现有的 FFmpeg API 客户端解码器,我们编写了一个额外的解码器,并将数据包传递给该解码器。这样,使用标准的 FFmpeg 版本和 FFmpeg API 就解决了播放此类文件的问题。
在某些情况下,可以通过流的元数据来识别未知的编解码器;例如,*.mkvFFmpeg 会在那里记录编解码器名称(属性ENCODER)。
结论
本文仅涵盖代码更改,不涵盖 FFmpeg 其他部分(例如文档、变更日志、版本控制等)所需的更改。但是,如果您计划为特定项目构建一个"自制"的 FFmpeg 版本,则无需执行此操作。