小程之前介绍过FFmpeg的帧的结构(AVPacket跟AVFrame),帧会在一些流程中使用到。

除了帧结构,FFmpeg还有其它一些结构会在流程中使用到。

FFmpeg还有哪些常见的结构呢?先来看一下这个截图:
AVFormatContext聚合结构

这张图中的主角,是AVFormatContext。AVFormatContext,是FFmpeg的基本结构之一,对应于封装格式(或容器格式)。

围绕FFmpeg的“格式场景”,本文介绍FFmpeg常见的数据结构

按照上图,小程依次介绍图中的几个结构体。

(一)AVCodec

AVCodec是FFmpeg设计上的一个结构体,用来保存编×××的信息,也就是说,AVCodec是编码器或×××。

小程还是以调试的办法,具体看一下AVCodec变量中的内容。

(1)演示代码

演示代码的目录结构是这样的:
代码目录结构

其中的FFmpeg静态库是事先编译好的(这里是macos版本,因为小程用了mac电脑),编译的办法可以参考之前的文章,读者可以关注“广州小程”微信公众号,并在相应的菜单项中找到文章。

moments.mp4 是试用的视频文件(mp4封装格式)。对于封装格式,在公众号的“音视频”之“基础概念与流程”中也可以找到,小程有专门介绍过媒体格式。

makefile是编译脚本,用来编译演示代码,当然也可以直接用gcc来编译。

show_avcodec.c就是演示代码了,内容如下:

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"

void show_avcodec(const char* filepath) {
    av_register_all();
    av_log_set_level(AV_LOG_DEBUG);
    AVFormatContext* formatContext = avformat_alloc_context();
    int status = 0;
    int success = 0;
    int videostreamidx = -1;
    AVCodecContext* codecContext = NULL;
    status = avformat_open_input(&formatContext, filepath, NULL, NULL);
    if (status == 0) {
        status = avformat_find_stream_info(formatContext, NULL);
        if (status >= 0) {
            for (int i = 0; i < formatContext->nb_streams; i ++) {
                if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                    videostreamidx = i;
                    break;
                }
            }
            if (videostreamidx > -1) {
                codecContext = formatContext->streams[videostreamidx]->codec;
                AVCodec* codec = avcodec_find_decoder(codecContext->codec_id);
                if (codec) {
                    status = avcodec_open2(codecContext, codec, NULL);
                    if (status == 0) {
                        success = 1;
                    }
                }
            }
        }
        else {
            av_log(NULL, AV_LOG_DEBUG, "avformat_find_stream_info error\n");
        }
        avformat_close_input(&formatContext);
    }
    avformat_free_context(formatContext);
}

int main(int argc, char *argv[])
{
    show_avcodec("moments.mp4");
    return 0;
}

(2)编译与调试

makefile的内容:

exe=showavcodec
srcs=show_avcodec.c 
$(exe):$(srcs)
    gcc -o $(exe) $(srcs) -Iffmpeg/include/ -Lffmpeg -lffmpeg -liconv -lz -g
clean:
    rm -f $(exe) *.o

直接执行make来编译,编译后会生成符号表文件即showavcodec.dSYM。

这里只是简单看一下AVCodec的内容,用gdb来调试即可:

gdb showavcodec
b 25
r

在断点的地方,看一下AVCodec变量中的值:
avcodec的内容

(3)AVCodec结构内容

AVCodec是编×××的结构体,在libavcodec/avcodec.h中定义。

在这个示例中,AVCodec是一个×××。

AVCodec结构中的一些变量,从它的名字或者FFmpeg详细的注释中,可以知道是什么含义。

比如name是编解码的名称,而long_name就是长的名称,等等。

在设计上,AVCodec是编×××的抽象,所以,编×××是有相应的具体实现的。

事实上,每一个编×××都有具体的实现。

比如h264的×××(libavcodec/h264.c):
h264×××

比如mp3lame的编码器(libavcodec/libmp3lame.c)
mp3lame编码器

FFmpeg会使用上这些具体的编×××的实现,以完成编解码等功能。

(二)AVCodecContext

AVCodecContext可以简单理解为AVCodec的使用场景,而实际上AVCodecContext包括的内容,除了关联AVCodec,还有其它信息。

跟调试AVCodec变量一样,直接使用上面的演示代码就可以调试AVCodecContext,部分代码如下 :

if (videostreamidx > -1) {
    codecContext = formatContext->streams[videostreamidx]->codec;
    AVCodec* codec = avcodec_find_decoder(codecContext->codec_id);
    if (codec) {
        status = avcodec_open2(codecContext, codec, NULL);
        if (status == 0) {
            success = 1;
        }
    }
}

同样用gdb来调试就可以了,在拿到codecContext后下断点,可以看到AVCodecContext的部分内容如下:
avcodeccontext内容

其中有一些变量应该引起注意,比如:

width/height 视频的宽与高
codec_id 编×××的id,根据它可以找到对应的编×××
extradata 对于h264编码的视频,保存了pps与sps的参数信息
profile 视频编码复杂等级
sample_rate 音频的采样率
channels 音频的声道数
sample_fmt 音频的采样格式

跟AVCodec一样,AVCodecContext结构体在libavcodec/avcodec.h中定义。

(三)AVStream

上面介绍了AVCodec、AVCodecContext,现在介绍AVStream。

这三者的大概关系是这样的:

AVStream聚合AVCodecContext

AVStream对应音频流、视频流、字幕等媒体流。

FFmepg以流的概念来封装不同的媒体。

调试AVStream的示例代码与编译,可以查看上面AVCodec调试的介绍。大概如下:
查看AVStream的代码

下断点,可以看到AVStream中的内容,比如:
AVStream中的变量值

AVStream中的一些变量:

index,流的索引
codec,流对应的avcodeccontext
time_base,时间基准(比例)
duration,流的时长
metadata,流的元信息
nb_frames,流中帧的数量

AVStream结构,在libavformat/avformat.h中定义。

(四)AVFormatContext

AVFormatContext是主角,表示为格式的场景,对应于封装格式(或容器格式)。

同样,使用之前的示例代码,在avformat_open_input函数后下断点:
avformatcontext示例代码

可以查看avformatcontext结构中的变量值:
avformatcontext结构中的变量值

AVFormatContext中的metadata记录了多媒体文件的一些信息(比如作者、专辑之类),可以这样取得里面的信息:

if (formatCtx->metadata) {
    AVDictionaryEntry *item = NULL;
    while((item = av_dict_get(formatCtx->metadata, "", item, AV_DICT_IGNORE_SUFFIX))){
        printf("key:%s value:%s \n", item->key, item->value);
    }

    // 或者这样:
    AVDictionaryEntry *tag = NULL;
    tag = av_dict_get(formatCtx->metadata, "artist", NULL, 0);
    if (tag) {
        std::string artist = (char*)tag->value;
    }
}

AVFormatContext的一些变量说明:

iformat/oformat,输入/输出格式,在解复用(解封装)或复用(封装)时使用。
pb,输入或输出场景,提供数据操作接口(比如读写、seek等)。
nb_streams,流的个数(以流的方式来复用)。
streams,流的数组。
filename,文件名。
start_time,流的起始时间,以AV_TIME_BASE为单位(除以AV_TIME_BASE转为秒)。
duration,流的时长,以AV_TIME_BASE为单位。
bit_rate,比特率。
probesize,在检测容器格式时,最大的探测大小,在avformat_open_input之前设置(或不设置使用默认值)。
max_analyze_duration,最大的分析数据的时长,在检测编码格式时使用,在avformat_find_stream_info前设置(或不设置),越大越耗时。
metadata,元信息。

AVFormatContext结构,在libavformat/avformat.h中定义。

(五)AVIOContext

AVIOContext是输入输出信息的结构体,它在FFmpeg结构体系中的位置是这样的:
aviocontext的位置

可以看到,AVIOContext是AVFormatContext的一个成员,叫作pb。

pb是提供数据的变量,既用于读(解码)也用于写(编码)。

(1)解码时

在解码时,pb提供解码的原始数据,一般在调用avio_alloc_context创建aviocontext时,指定read与seek函数(自定义的实现,提供读数据、跳转位置的功能),然后把创建的aviocontext(即pb)直接设置给AVFormatContext。比如这样:

pb = avio_alloc_context(readBuf, readBufLen, 0, this, myReadFunc, NULL, mySeekFunc);
mFormatCtx->pb = pb;

或者,在解码时,这样使用aviocontext:

mIOContext.read_packet = myReadFunc;
mIOContext.seek = mySeekFunc;
const int MAX_PRO_SIZE = 321024;
unsigned char
probuf = (unsigned char*)av_malloc(MAX_PRO_SIZE);
mIOContext.buffer = probuf;
mIOContext.buf_ptr = probuf;
mIOContext.buffer_size = MAX_PRO_SIZE;
mIOContext.buf_end = probuf + MAX_PRO_SIZE;
mIOContext.max_packet_size = MAX_PRO_SIZE;
formatContext->pb = &mIOContext;

其中,函数myReadFunc与mySeekFunc,按照结构体AVIOContext中的格式说明(参照头文件说明)来定义即可。

解码时,pb的设置,要在avformat_open_input调用前完成。

(2)编码时

在编码写文件时,pb提供写到文件的数据(编码后的数据,对应AVPacket),比如可以直接用avio_open2来打开pb:

if (!(mFormatContext->flags & AVFMT_NOFILE)) {
    err = avio_open2(&mFormatContext->pb, url, AVIO_FLAG_WRITE, &mFormatContext->interrupt_callback, NULL);
    // ...
}

写文件时,可以用av_write_frame来写入一个packet,也可以用avio_write往pb中写入数据。

编码写文件时,pb的设置,要在avformat_write_header调用前完成。

(3)AVIOContext的变量

这里只列表一部分:

buffer,AVIOContext缓存数据的buffer,起始地址。
buffer_size,buffer的大小。
buf_ptr,操作buffer的当前位置。
buf_end,数据的结束位置,有可能未到buffer的未端。
opaque,指向URLContext,提供读、写、seek等接口,可以让它为空,从而使用自定义的接口。
read_packet/write_packet/seek,读写与seek的接口,可以在avio_alloc_context时指定,从而自定义。

AVIOContext结构体在libavformat/avio.h中定义。

至此,FFmpeg常见的几个结构体就介绍完毕了。


总结一下,本文介绍了FFmpeg的常见结构体,包括AVFormatContext、AVIOContext、AVStream、AVCodecContext、AVCodec等,并且以调试的方式查看了结构体的一些变量值。