前言
上上篇文章我们实现了一个小demo:通过ffmpeg读取视频,然后截取一帧或几帧视频帧,转换色彩空间YUV为RGB,然后保存为图片。展示了ffmpeg开发的基本方法,当然也只简单展示了解封装,解码,色彩转换这些基础能力,这只是ffmpeg功能的一部分。
接着在上一篇文章中,我们简单分析了MP4文件的文件结构,以便于能够帮助我们更好的理解ffmpeg的一些操作逻辑。
现在我们需要针对ffmpeg中的一些比较常见的API,分析它的基本实现原理,以便于更好的理解ffmpeg开发过程中的API调用逻辑。
ffmpeg中申请与释放内存的API
在ffmpeg中,对于申请内存空间和释放内存空间的操作进行了封装,而不是让开发者直接使用c/c++的原生接口。之所以这么做是为了在不同的操作系统中都能实现内存对齐(至于内存对齐的好处就是比较基础的知识了)。
这里针对初学者或者对c/c++不算太熟悉的同学做一些解释,在c/cpp这种需要手动管理内存的语言来说,申请内存是需要通过一个函数调用来完成的。但是申请完内存之后,这块内存空间还是一片荒芜(或者可能赋了初始值0),总之不是有效的内容,在完成了内存空间的申请之后,再对这块空间中的各种成员进行赋值完成初始化才算完成。
比如ffmpeg中的各种结构体,我们首先通过av_malloc对该结构体进行内存空间的申请,然后再对结构体内的成员一一赋值,之后然后才算得到一个有效的可用的结构体对象。
这一点上和Java这样的高级语言差别很大,Java中内存申请是被隐藏起来的,我们只需要关注对象的创建。
这些API具体如下:
// 申请一块大小为size的内存
void *av_malloc(size_t size)
// 申请一块大小为size的内存,并把所有字节设置为0
void *av_mallocz(size_t size)
//申请一块nmemb*size大小的内存空间(数组空间)
void *av_calloc(size_t nmemb, size_t size)
//重新调整ptr指向的内存区域,调整为size大小
// 根据ptr是否为空,size是否大于0可能是释放,申请,扩张,缩减内存空间
void *av_realloc(void *ptr, size_t size)
// 释放ptr指向的空间,
void av_free(void *ptr);
// 释放ptr指向的空间并把ptr指向为NUL (recommend)
void av_freep(void *ptr);
我们以其中基础的av_malloc为例看一下它的实现
#define ALIGN (HAVE_AVX512 ? 64 : (HAVE_AVX ? 32 : 16))
// 申请一个字节数为size的内存空间,并返回指向该内存空间的指针给调用者
void *av_malloc(size_t size)
{
void *ptr = NULL;
// 申请的size不能大于最大值max_alloc_size,(原子操作)
if (size > atomic_load_explicit(&max_alloc_size, memory_order_relaxed))
return NULL;
#if HAVE_POSIX_MEMALIGN
// ALIGN = 16 表示按照16字节 进行内存对齐
if (size) //OS X on SDK 10.6 has a broken posix_memalign implementation
if (posix_memalign(&ptr, ALIGN, size)) // 按照16字节对齐来申请size大小的空间,假如size=24,最终申请到的内存大小应该是32
ptr = NULL;
...
...
#endif
if(!ptr && !size) { // 假如ptr为空并且size=0,做一下兜底逻辑
size = 1;
ptr= av_malloc(1);
}
...
...
return ptr;
}
然后我们再看看内存释放av_free和av_freep
void av_free(void *ptr)
{
#if HAVE_ALIGNED_MALLOC // windows下条件为真,linux下为假
_aligned_free(ptr);
#else
free(ptr); // 直接调用cpp的释放内存的接口
#endif
}
//@param arg Pointer to the pointer to the memory block which should be freed
void av_freep(void *arg) // 根据API文档,arg应该是一个二级指针,指向一个准备释放内存块的指针
{
void *val;
memcpy(&val, arg, sizeof(val));// 把等待释放的内存的地址保存到val中
memcpy(arg, &(void *){ NULL }, sizeof(val)); // 把arg指向的指针设置为nullptr,
av_free(val); // 释放val指针
}
av_freep的实现理解起来确实有点费脑子,大家可以就简单理解为av_freep的功能就是释放指针指向的内存,并且把指针置空,不过传参需要按照二级指针来传。
了解了这些基础的内容之后,我们再看ffmpeg中主要的几个结构体的内存申请和释放。
AVFormatContext
avformat_alloc_context (申请内存)
AVFormatContext *avformat_alloc_context(void)
{
// FFFormatContext是算是ForamtContext内部实现类
// 申请FFFormatContext结构体的内存空间
// 它内部有一个成员AVFormatContext pub;也会同样开辟内存空间
FFFormatContext *const si = av_mallocz(sizeof(*si));
AVFormatContext *s;
if (!si)
return NULL;
s = &si->pub; // 直接把FFFormatContext->pub这个AVFormatContext结构体对象返回
s->av_class = &av_format_context_class;
s->io_open = io_open_default;
s->io_close2= io_close2_default;
av_opt_set_defaults(s); // 设置一些默认值
si->pkt = av_packet_alloc();
si->parse_pkt = av_packet_alloc();
if (!si->pkt || !si->parse_pkt) {
avformat_free_context(s);
return NULL;
}
...
return s;
}
主要使用了av_mallocz函数手动申请AVFormatContext结构体的内存空间,此时它内部的成员变量都会获得空间的分配(指针类型只会获得指针所占用的空间:4byte或者8byte)。
至于AVFormatContext中的重要成员的真正被创建和可用,则是在avformat_open_input函数中。
avformat_open_input
avformat_open_input函数的主要功能:完成AVFormatContext的初始化(关键成员的创建和初始化),对文件解复用(解封装),读取有效信息。
static av_always_inline FFFormatContext *ffformatcontext(AVFormatContext *s)
{
return (FFFormatContext*)s;
}
int avformat_open_input(AVFormatContext **ps, const char *filename,
const AVInputFormat *fmt, AVDictionary **options)
{
AVFormatContext *s = *ps;
FFFormatContext *si;
int ret = 0;
// 如果AVFormatContext结构体是空,则创建它
if (!s && !(s = avformat_alloc_context()))
return AVERROR(ENOMEM);
// 直接把AVFormatContext转换成FFFormatContext 是有点夸张,
// 应该是因为AVFormatContext是FFFormatContext结构体的第一个成员,
// FFFormatContext又和我们持有同一个AVFormatContext,
// 所以此时AVFormatContext的地址就是FFFormatContext的地址
si = ffformatcontext(s);
...
// 重点: 初始化input,创建并初始化AVIOContext AVInputFormat等结构体
if ((ret = init_input(s, filename, &tmp)) < 0)
goto fail;
...
...
// 读取文件头数据
if (s->iformat->read_header)
if ((ret = s->iformat->read_header(s)) < 0) {
if (s->iformat->flags_internal & FF_FMT_INIT_CLEANUP)
goto close;
goto fail;
}
}
init_input函数不仅创建了一些关键的结构体比如AVInputFormat,还通过AVInputFormat读取了文件头的数据。
AVInputFormat的创建
这其中相对最重要的是AVInputFormat结构体,它是对不同的媒体文件进行解复用的关键结构体,我们跟进看看AVInputFormat是如何被初始化的。
// init_input ==> av_probe_input_buffer2 ==> av_probe_input_format3
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
{
int ret;
AVProbeData pd = { filename, NULL, 0 };
int score = AVPROBE_SCORE_RETRY;
...
return av_probe_input_buffer2(s->pb, &s->iformat, filename,
s, 0, s->format_probesize);
}
int av_probe_input_buffer2(AVIOContext *pb, const AVInputFormat **fmt,
const char *filename, void *logctx,
unsigned int offset, unsigned int max_probe_size)
{
AVProbeData pd = { filename ? filename : "" };
uint8_t *buf = NULL;
int ret = 0, probe_size, buf_offset = 0;
int score = 0;
int ret2;
int eof = 0;
for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt && !eof;
probe_size = FFMIN(probe_size << 1,
FFMAX(max_probe_size, probe_size + 1))) {
// 读取一些探针数据,用来确定文件的真实格式
if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)
goto fail;
if ((ret = avio_read(pb, buf + buf_offset,
probe_size - buf_offset)) < 0) {
...
}
...
// 获取AVInputFormat
*fmt = av_probe_input_format2(&pd, 1, &score);
av_freep(&pd.mime_type);
return ret < 0 ? ret : score;
}
const AVInputFormat *av_probe_input_format2(const AVProbeData *pd,
int is_opened, int *score_max)
{
int score_ret;
const AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);
...
}
const AVInputFormat *av_probe_input_format3(const AVProbeData *pd,
int is_opened, int *score_ret)
{
AVProbeData lpd = *pd;
const AVInputFormat *fmt1 = NULL;
const AVInputFormat *fmt = NULL;
int score, score_max = 0;
void *i = 0;
const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE];
enum nodat {
NO_ID3,
ID3_ALMOST_GREATER_PROBE,
ID3_GREATER_PROBE,
ID3_GREATER_MAX_PROBE,
} nodat = NO_ID3;
...
// 1,迭代循环获取demuxer
while ((fmt1 = av_demuxer_iterate(&i))) {
...
score = 0;
// read_probe是文件读取探针函数,作用是读取文件部分数据进行测试匹配程度
if (fmt1->read_probe) {
// 2,对文件进行部分读取,返回一个匹配的分数值
score = fmt1->read_probe(&lpd);
...
}
...
// 保留匹配最大的得分的AVInputFormat
if (score > score_max) {
score_max = score;
fmt = fmt1;
} else if (score == score_max)
fmt = NULL;
}
...
return fmt;
}
我们看一下av_demuxer_iterate是一个怎样的遍历函数:
// allformats.c
const AVInputFormat *av_demuxer_iterate(void **opaque)
{
// demuxer_list就是一列demuxer的列表
static const uintptr_t size = sizeof(demuxer_list)/sizeof(demuxer_list[0]) - 1;
uintptr_t i = (uintptr_t)*opaque;
const AVInputFormat *f = NULL;
uintptr_t tmp;
if (i < size) { // 判断下标没有越界
f = demuxer_list[i]; // 获取一个demuxer
}
...
return f;
}
// muxer_list.c
static const AVInputFormat * const demuxer_list[] = {
&ff_aa_demuxer,
&ff_aac_demuxer,
&ff_aax_demuxer,
&ff_ac3_demuxer,
...
...
&ff_mov_demuxer,
&ff_mp3_demuxer,
&ff_mpc_demuxer,
...
...
}
我们先来看AVInputFormat的结构体的定义:
typedef struct AVInputFormat {
const char *name;
const char *long_name;
...
int (*read_header)(struct AVFormatContext *);
int (*read_packet)(struct AVFormatContext *, AVPacket *pkt);
int (*read_close)(struct AVFormatContext *);
...
} AVInputFormat;
它内部除了一些成员属性,还定义了一些关键函数,用来读取媒体文件的函数接口。我们知道不同的封装格式的文件,它的文件头,文件体的数据格式都是不同的,那么在解复用(解封装)的时候就需要根据不同的封装格式来适配不同的读取方法,所以demuxer_list中就是定义了不同文件类型的对应的demuxer(AVInputFormat的实现对象)。
我们的测试MP4视频文件最终找到的是ff_mov_demuxer这个结构体,我们就以ff_mov_demuxer为例看看它的实现。
// mov.c
// 创建了一个AVInputFormat结构体的对象
const AVInputFormat ff_mov_demuxer = {
.name = "mov,mp4,m4a,3gp,3g2,mj2",
.long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),
.priv_class = &mov_class,
.priv_data_size = sizeof(MOVContext),
.extensions = "mov,mp4,m4a,3gp,3g2,mj2,psp,m4b,ism,ismv,isma,f4v,avif",
.flags_internal = FF_FMT_INIT_CLEANUP,
.read_probe = mov_probe, // 探针函数
.read_header = mov_read_header, // 读取文件头
.read_packet = mov_read_packet, // 读取数据
.read_close = mov_read_close, // 关闭
.read_seek = mov_read_seek, // seek
.flags = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS | AVFMT_SHOW_IDS,
};
我们看到ff_mov_demuxer是根据QuickTime协议定义的一个解复用器,我们在ffmpeg开发——深入理解MP4文件格式文章中简略的提到过QuickTime,它可以正常的解码MP4文件的,某种程度上是MP4协议格式借鉴了QuickTime协议的文件构造,因此QT可以兼容MP4。
读取探针数据
在获取匹配的demuxer过程中,都需要读取文件的部分数据来做一个对比,接下来我们看ff_mov_demuxer是如何读取quicktime协议定义的文件头数据。
static int mov_probe(const AVProbeData *p)
{
int64_t offset;
uint32_t tag;
int score = 0;
int moov_offset = -1;
/* check file header */
offset = 0;
for (;;) {
int64_t size;
int minsize = 8;
...
// 读取一个tag?
tag = AV_RL32(p->buf + offset + 4);
switch(tag) { 判断tag是否是MP4文件结构中的box类型
/* check for obvious tags */
case MKTAG('m','o','o','v'): //一般moov box不会直接放在开头,至少跟在ftyp box之后
moov_offset = offset + 4;
case MKTAG('m','d','a','t'):
case MKTAG('p','n','o','t'): /* detect movs with preview pics like ew.mov and april.mov */
case MKTAG('u','d','t','a'): /* Packet Video PVAuthor adds this and a lot of more junk */
case MKTAG('f','t','y','p'): // ftyp box属于MP4的文件头的box
if (tag == MKTAG('f','t','y','p') &&
( AV_RL32(p->buf + offset + 8) == MKTAG('j','p','2',' ')
|| AV_RL32(p->buf + offset + 8) == MKTAG('j','p','x',' ')
|| AV_RL32(p->buf + offset + 8) == MKTAG('j','x','l',' ')
)) { // 某些特殊类型的图片类型,最多5分
score = FFMAX(score, 5);
} else { // 否则可以得到最高分 100分
score = AVPROBE_SCORE_MAX;
}
break;
...
}
if (size > INT64_MAX - offset)
break;
offset += size;
}
...
...
return score;
}
根据我输入的MP4测试视频,这个ff_mov_demuxer能够理解MP4文件结构,正确的解析出MP4文件的开头部分ftyp box的数据,从而得到一个高分。(关于MP4文件结构可以看ffmpeg开发——深入理解MP4文件格式)。
读取文件头
获取到AVInputFormat之后,会调用iformat->read_header(s)函数,读取文件头数据,获取一些有效信息,比如duration,time_scale,width,height,bit_rate等。
那么ff_mov_demuxer是如何读取MP4文件的文件头的呢?当然我们看看ff_mov_demuxer中的read_header的实现:
static int mov_read_header(AVFormatContext *s)
{
MOVContext *mov = s->priv_data;
AVIOContext *pb = s->pb;
int j, err;
// 定义一个Atom,在MP4协议中叫box
MOVAtom atom = { AV_RL32("root") };
int i;
...
mov->fc = s; // 持有AVFormatContext
if (pb->seekable & AVIO_SEEKABLE_NORMAL)
atom.size = avio_size(pb); //64kb大小
else
atom.size = INT64_MAX;
do {
if (mov->moov_retry)
avio_seek(pb, 0, SEEK_SET);
if ((err = mov_read_default(mov, pb, atom)) < 0) {
av_log(s, AV_LOG_ERROR, "error reading header\n");
return err;
}
} while ((pb->seekable & AVIO_SEEKABLE_NORMAL) && !mov->found_moov && !mov->moov_retry++);
// 到这里AVFormatContext中的对应的结构(AVStream)差不多有了有效的信息
...
...
return 0;
}
读取文件头的逻辑主要在mov_read_default函数中,接着看mov_read_default函数的实现
static int mov_read_default(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
int64_t total_size = 0;
MOVAtom a;
int i;
...
...
if (atom.size < 0)
atom.size = INT64_MAX;
while (total_size <= atom.size - 8) { // 大概要读取64kb的数据(为什么-8?)
int (*parse)(MOVContext*, AVIOContext*, MOVAtom) = NULL;
//根据之前的关于MP4文件结构分析,box结构的开始是box header ,头两个字段是size和type,各占4个字节
a.size = avio_rb32(pb); // 读取4个byte 获取该box size
a.type = avio_rl32(pb); // 读取4个byte 获取该box type
...
...
total_size += 8;
// 也是之前文章提到的,假如box header中size=1,表示要用拓展字段largesize,占8个字节
if (a.size == 1 && total_size + 8 <= atom.size) { /* 64 bit extended size */
a.size = avio_rb64(pb) - 8;
total_size += 8;
}
if (a.size == 0) {
a.size = atom.size - total_size + 8;
}
if (a.size < 0)
break;
a.size -= 8; // 应该是减去box header的两个字段(或者加上拓展字段)占用的空间?剩余只有box data
if (a.size < 0)
break;
a.size = FFMIN(a.size, atom.size - total_size);
// 遍历quicktime的MP4文件解析器列表
for (i = 0; mov_default_parse_table[i].type; i++)
// 找到type匹配的解析器
if (mov_default_parse_table[i].type == a.type) {
parse = mov_default_parse_table[i].parse;
break;
}
// prase为空,且type是udta box(用户信息的box) 或者 ilst box的话,
// 可以指定mov_read_udta_string解析方法
if (!parse && (atom.type == MKTAG('u','d','t','a') ||
atom.type == MKTAG('i','l','s','t')))
parse = mov_read_udta_string;
if (!parse) { /* skip leaf atoms data */
avio_skip(pb, a.size);
} else {
int64_t start_pos = avio_tell(pb);
int64_t left;
int err = parse(c, pb, a); // 调用对应type的解析函数
...
...
}
total_size += a.size;
}
...
c->atom_depth --;
return 0;
}
// quicktime解析MP4文件的解析器列表{type,prase}
static const MOVParseTableEntry mov_default_parse_table[] = {
...
{ MKTAG('m','d','h','d'), mov_read_mdhd }, // mdhd box
...
{ MKTAG('s','t','b','l'), mov_read_default },
{ MKTAG('s','t','c','o'), mov_read_stco },
{ MKTAG('s','t','p','s'), mov_read_stps },
{ MKTAG('s','t','r','f'), mov_read_strf },
{ MKTAG('s','t','s','c'), mov_read_stsc },
{ MKTAG('s','t','s','d'), mov_read_stsd }, /* sample description */
{ MKTAG('s','t','s','s'), mov_read_stss }, /* sync sample */
{ MKTAG('s','t','s','z'), mov_read_stsz }, /* sample size */
{ MKTAG('s','t','t','s'), mov_read_stts },
{ MKTAG('s','t','z','2'), mov_read_stsz }, /* compact sample size */
{ MKTAG('s','d','t','p'), mov_read_sdtp }, /* independent and disposable samples */
{ MKTAG('t','k','h','d'), mov_read_tkhd }, /* track header */
{ MKTAG('t','f','d','t'), mov_read_tfdt },
{ MKTAG('t','f','h','d'), mov_read_tfhd }, /* track fragment header */
{ MKTAG('t','r','a','k'), mov_read_trak },
...
}
接下来,我们选择一个box对应的解析函数来看看解析逻辑是怎样的,就选择mdhd box为例,在上一篇文章ffmpeg开发——深入理解MP4文件格式中,我们对mdhd box有一定的了解,它的结构如下
image.png这里还要说一下,FullBox这个拓展类型的Box,在Box Header中还拓展了两个字段version,flags,分别占用1个字节和3个字节(具体也可从上一篇文章全面了解)。
image.png
static int mov_read_mdhd(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
AVStream *st;
MOVStreamContext *sc;
int version;
char language[4] = {0};
unsigned lang;
..
// AVFormatContext->streams
st = c->fc->streams[c->fc->nb_streams-1];
sc = st->priv_data;
...
// 读取version字段
version = avio_r8(pb);
if (version > 1) {
avpriv_request_sample(c->fc, "Version %d", version);
return AVERROR_PATCHWELCOME;
}
// flags,应该暂时无用
avio_rb24(pb); /* flags */
// 读取create_time和modification time
mov_metadata_creation_time(c, pb, &st->metadata, version);
// 读取time_scale字段,不管version的取值,timescale都只占32个位
sc->time_scale = avio_rb32(pb);
if (sc->time_scale <= 0) {
av_log(c->fc, AV_LOG_ERROR, "Invalid mdhd time scale %d, defaulting to 1\n", sc->time_scale);
sc->time_scale = 1;
}
// 根据version的取值,决定读取64位还是32位来获取该轨道的数据的时长
st->duration = (version == 1) ? avio_rb64(pb) : avio_rb32(pb); /* duration */
if ((version == 1 && st->duration == UINT64_MAX) ||
(version != 1 && st->duration == UINT32_MAX)) {
st->duration = 0;
}
// 其他字段
lang = avio_rb16(pb); /* language */
if (ff_mov_lang_to_iso639(lang, language))
av_dict_set(&st->metadata, "language", language, 0);
avio_rb16(pb); /* quality */
return 0;
}
基本上是按照MP4标准文档定义的结构来读取数据,不仅mdhd box是这样,对其他类型的box的读取方式也是一样的。
avformat_close_input
void avformat_close_input(AVFormatContext **ps)
{
AVFormatContext *s;
AVIOContext *pb;
...
s = *ps;
pb = s->pb;
if ((s->iformat && strcmp(s->iformat->name, "image2") && s->iformat->flags & AVFMT_NOFILE) ||
(s->flags & AVFMT_FLAG_CUSTOM_IO))
pb = NULL;
if (s->iformat)
if (s->iformat->read_close)
s->iformat->read_close(s); // 关闭输入流
avformat_free_context(s); // 释放AVFormatContext结构体
*ps = NULL; // AVFormatContext指针设置为NULL
avio_close(pb); // 关闭输入缓冲等
}
关闭输入操作主要是释放输入相关的资源,然后释放AVFormatContext结构体。
avformat_free_context
avformat_free_context 是释放AVFormatContext结构体的函数,avformat_close_input函数内也会调用这个函数。
void avformat_free_context(AVFormatContext *s)
{
FFFormatContext *si;
if (!s)
return;
si = ffformatcontext(s);
if (s->oformat && ffofmt(s->oformat)->deinit && si->initialized)
ffofmt(s->oformat)->deinit(s);
av_opt_free(s);
if (s->iformat && s->iformat->priv_class && s->priv_data)
av_opt_free(s->priv_data);
if (s->oformat && s->oformat->priv_class && s->priv_data)
av_opt_free(s->priv_data);
for (unsigned i = 0; i < s->nb_streams; i++)
ff_free_stream(&s->streams[i]);
s->nb_streams = 0;
for (unsigned i = 0; i < s->nb_programs; i++) {
av_dict_free(&s->programs[i]->metadata);
av_freep(&s->programs[i]->stream_index);
av_freep(&s->programs[i]);
}
s->nb_programs = 0;
av_freep(&s->programs);
av_freep(&s->priv_data);
while (s->nb_chapters--) {
av_dict_free(&s->chapters[s->nb_chapters]->metadata);
av_freep(&s->chapters[s->nb_chapters]);
}
av_freep(&s->chapters);
av_dict_free(&s->metadata);
av_dict_free(&si->id3v2_meta);
av_packet_free(&si->pkt);
av_packet_free(&si->parse_pkt);
av_freep(&s->streams);
ff_flush_packet_queue(s);
av_freep(&s->url);
av_free(s);
}
就是逐个释放AVForamtContext中各种成员所占用的内存空间,然后释放AVForamtContext自身。
网友评论