ffmpeg关键数据结构及对应的协议层
FFMPEG中最关键的结构体之间的关系 - 雷霄骅(leixiaohua1020)的专栏 - 博客频道 - CSDN.NET
| 功能 | 结构体 |
|---|---|
| 整个音视频文件的抽象(包含下面所有结构体) | AVFormatContext |
| 协议层 | AVIOContext |
| 封装格式层 | AVInputFormat/AVOutputFormat |
| 解码之前的数据 | AVPacket |
| 编码层 | AVStream[0]->AVCodecContext->AVCodec |
| AVStream[1]->AVCodecContext->AVCodec | |
| 解码之后的数据 | AVFrame |
协议层(http,rtsp,rtmp,mms)
AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。
URLProtocol存储输入视音频使用的封装格式。
每种协议都对应一个URLProtocol结构。(注意:FFMPEG中文件也被当做一种协议“file”)
封装层(flv,avi,rmvb,mp4)
解封装
结果: 产生压缩的码流数据(解码前数据)——AVPacket
视频的话,每个结构一般是存一帧;音频可能有好几帧
封装
将h.264等裸流,封装为文件
主要结构体及初始化
AVFormatContext主要存储视音频封装格式中包含的信息(非常重要,包含封装层、编码层)
AVFormatContext结构体简介
FFMPEG结构体分析:AVFormatContext - 雷霄骅(leixiaohua1020)的专栏 - 博客频道 - CSDN.NET
AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体struct AVInputFormat * iformat:输入数据的封装格式 AVIOContext * pb:输入数据的缓存 unsigned int nb_streams:视音频流的个数 AVStream ** streams:视音频流 char filename[1024]:文件名 int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000) int bit_rate:比特率(单位bps,转换为kbps需要除以1000) AVDictionary * metadata:元数据创建 AVFormatContext的方法
方法一: 通过打开文件获取 AVFormatContext(解封装输入文件格式) (解封装,自顶向下初始化)
第一步: FFMpeg进行其他操作一样,首先需注册FFMpeg组件
av_register_all();
第二步: 打开待处理音视频文件
然而在此我们不使用打开文件的fopen函数,而是使用avformat_open_input函数。该函数不但会打开输入文件,而且可以根据输入文件读取相应的格式信息。该函数的声明如下:
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
该函数的各个参数的作用为:
ps:根据输入文件接收与格式相关的句柄信息;可以指向NULL,那么AVFormatContext类型的实例将由该函数进行分配。
url:视频url或者文件路径;
fmt:强制输入格式,可设置为NULL以自动检测;
options:保存文件格式无法识别的信息;
返回值:成功返回0,失败则返回负的错误码;该函数的调用方式为:
if (avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL) < 0)
{
fprintf(stderr, “Could not open source file %s\n”, files.src_filename);
return -1;
}第三步: 获取文件中的流信息
该函数的声明为:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
该函数的第一个参数即前面的文件句柄,第二个参数也是用于保存无法识别的信息的AVDictionary的结构,通常可设为NULL。调用方式如:
* retrieve stream information *
if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0)
{
fprintf(stderr, “Could not find stream information\n”);
return -1;
}
方法二: 手动创造 AVFormatContext(封装输出文件格式)(封装,自底向上初始化)
第一步: 根据输出文件的格式获取AVFormatContext结构
根据输出文件的格式获取AVFormatContext结构,获取AVFormatContext结构使用函数avformat_alloc_output_context2实现。该函数的声明为:
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);
其中:
ctx:输出到AVFormatContext结构的指针,如果函数失败则返回给该指针为NULL;
oformat:指定输出的AVOutputFormat类型,如果设为NULL则使用format_name和filename生成;
format_name:输出格式的名称,如果设为NULL则使用filename默认格式;
filename:目标文件名,如果不使用,可以设为NULL;第二步: 判断 AVFormatContext中是否存在音频流或者视频流
if (fmt->video_codec != AV_CODEC_ID_NONE)
{
add_stream(video_st, oc, &video_codec, fmt->video_codec);
video_st->st->codec->width = io.frame_width;
video_st->st->codec->height = io.frame_height;
ret |= HAVE_VIDEO;
ret |= ENCODE_VIDEO;
}
if (fmt->audio_codec != AV_CODEC_ID_NONE)
{
add_stream(audio_st, oc, &audio_codec, fmt->audio_codec);
ret |= HAVE_AUDIO;
ret |= ENCODE_AUDIO;
}第三步: 向AVFormatContext结构中所代表的媒体文件中添加数据流
添加流首先需要查找流所包含的媒体的编码器,这需要传入codec_id后使用avcodec_find_encoder函数实现,将查找到的编码器保存在AVCodec指针中。
调用avformat_new_stream函数向AVFormatContext结构中所代表的媒体文件中添加数据流。该函数的声明如下:
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
其中各个参数的含义:
s:AVFormatContext结构,表示要封装生成的视频文件;
c:上一步根据codec_id产生的编码器指针;
返回值:指向生成的stream对象的指针;如果失败则返回NULL指针。此时,一个新的AVStream便已经加入到输出文件中。
AVStream是视频文件中某一音频流或者视频流的抽象。
存储一个视频/音频流(一系列包的集合)的结构。
AVStream可以表示封装格式中的音频或者视频数据。通过AVStream可以得到音频流或者视频流对应的AVCodecContext,存储该视频/音频流使用解码方式的相关数据。
主要有下面两种获取AVStream的方法:
方法一: 通过打开文件的 AVFormatContext,获取音频和视频AVStream
第一步: 通过打开文件获取 AVFormatContext(解封装输入文件格式
第二步: 获取文件中的音频和视频AVStream
获取文件中的流使用av_find_best_stream函数,其声明如:
int av_find_best_stream(AVFormatContext *ic,
enum AVMediaType type,
int wanted_stream_nb,
int related_stream,
AVCodec **decoder_ret,
int flags);其中各个参数的意义:
ic:视频文件句柄;
type:表示数据的类型,常用的有AVMEDIA_TYPE_VIDEO表示视频,AVMEDIA_TYPE_AUDIO表示音频等;
wanted_stream_nb:我们期望获取到的数据流的数量,设置为-1使用自动获取;
related_stream:获取相关的音视频流,如果没有则设为-1;
decoder_ret:返回这一路数据流的解码器;
flags:未定义;
返回值:函数执行成功返回 type流的序号 ,失败则返回负的错误码; (解封装)
方法二: 手动构造 AVStream (封装)
其他结构体
AVInputFormat存储输入视音频使用的封装格式,每种视音频封装格式都对应一个AVInputFormat 结构。
编码或者解码
主要结构体及初始化
编解码器结构体 AVCodec
每个AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。
AVCodec保存一个编解码器的实例,实现时实际的编码功能。通常我们在程序中定义一个指向AVCodec结构的指针指向该实例。
根据编解码器ID,获取编解码器指针的方法有以下两种:
第一步: 所有涉及到编解码的的功能,都必须要注册音视频编解码器之后才能使用。
avcodec_register_all();
第二步: 通过编解码器ID获取编解码器指针
CODEC_ID通常指定了编解码器的格式,在这里我们使用当前应用最为广泛的H.264格式为例。查找codec调用的函数为avcodec_find_encoder,其声明格式为:
AVCodec *avcodec_find_encoder(enum AVCodecID id);
该函数的输入参数为一个AVCodecID的枚举类型,返回值为一个指向AVCodec结构的指针,用于接收找到的编解码器实例。如果没有找到,那么该函数会返回一个空指针。调用方法如下:
* find the mpeg1 video encoder *
ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根据CODEC_ID查找编解码器对象实例的指针
if (!ctx.codec)
{
fprintf(stderr, “Codec not found\n”);
return false;
}
编解码器参数配置结构体 AVCodecContext
AVCodecContext表示AVCodec所需要的上下文信息,保存AVCodec所需要的一些参数。实现编码功能时,通过AVCodecContext设置编码器参数。
方法一: 解封装解码过程,通过AVStream获取编解码器上下文指针
AVCodecContext *c;
c = AVFormatContext->AVStream->codec;方法二: 通过编解码器AVCodec指针,获取编解码器上下文
分配AVCodecContext实例需要我们前面查找到的AVCodec作为参数,调用的是avcodec_alloc_context3函数。其声明方式为:
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
其特点同avcodec_find_encoder类似,返回一个指向AVCodecContext实例的指针。如果分配失败,会返回一个空指针。调用方式为:
ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext实例
if (!ctx.c)
{
fprintf(stderr, “Could not allocate video codec context\n”);
return false;
}
编码
成功将原始的YUV像素值保存到了AVframe结构中之后,便可以调用avcodec_encode_video2函数进行实际的编码操作。该函数可谓是整个工程的核心所在,其声明方式为:
int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame, int *got_packet_ptr);
其参数和返回值的意义:
avctx: AVCodecContext结构,指定了编码的一些参数;
avpkt: AVPacket对象的指针,用于保存输出码流;
frame:AVframe结构,用于传入原始的像素数据;
got_packet_ptr:输出参数,用于标识AVPacket中是否已经有了完整的一帧;
返回值:编码是否成功。成功返回0,失败则返回负的错误码
通过输出参数*got_packet_ptr,我们可以判断是否应有一帧完整的码流数据包输出,如果是,那么可以将AVpacket中的码流数据输出出来,其地址为AVPacket::data,大小为AVPacket::size。具体调用方式如下:
* encode the image *
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //将AVFrame中的像素信息编码为AVPacket中的码流
if (ret < 0)
{
fprintf(stderr, “Error encoding frame\n”);
exit(1);
}
if (got_output)
{
//获得一个完整的编码帧
printf(“Write frame %3d (size=%5d)\n”, frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
解码
在最终解析出一个完整的包之后,我们就可以调用解码API进行实际的解码过程了。解码过程调用的函数为avcodec_decode_video2,该函数的声明为:
int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
int *got_picture_ptr,
const AVPacket *avpkt);
这个函数与前篇所遇到的编码函数avcodec_encode_video2有些类似,只是参数的顺序略有不同,解码函数的输入输出参数与编码函数相比交换了位置。该函数各个参数的意义:
AVCodecContext *avctx:编解码器上下文对象,在打开编解码器时生成;
AVFrame *picture: 保存解码完成后的像素数据;我们只需要分配对象的空间,像素的空间codec会为我们分配好;
int *got_picture_ptr: 标识位,如果为1,那么说明已经有一帧完整的像素帧可以输出了
const AVPacket *avpkt: 前面解析好的码流包;
实际调用的方法为:
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf(“Decode Error.\n”);
return ret;
}
if (got_picture)
{
//获得一帧完整的图像,写出到输出文件
write_out_yuv_frame(ctx, inputoutput);
printf(“Succeed to decode 1 frame!\n”);
}
结果: 产生解压缩后的数据(解码后数据)——AVFrame
编码和解码的数据输入和输出
编码的输入,解码的输出。AVFrame结构体
AVFrame结构体
即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM
FFMPEG结构体分析:AVFrame - 雷霄骅(leixiaohua1020)的专栏 - 博客频道 - CSDN.NET
uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
对于packed格式的数据(例如RGB24),会存到data[0]里面。
对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]…(YUV420P中data[0]存Y,data[1]存U,data[2]存V)int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。
int width, height:视频帧宽和高(1920x1080,1280x720…)
int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个
int format:解码后原始数据类型(YUV420,YUV422,RGB24…)
int key_frame:是否是关键帧
enum AVPictureType pict_type:帧类型(I,B,P…)
AVRational sample_aspect_ratio:宽高比(16:9,4:3…)
int64_t pts:显示时间戳
int coded_picture_number:编码帧序号
int display_picture_number:显示帧序号
int8_t *qscale_table:QP表
uint8_t *mbskip_table:跳过宏块表
int16_t (*motion_val[2])[2]:运动矢量表
uint32_t *mb_type:宏块类型表
short *dct_coeff:DCT系数,这个没有提取过
int8_t *ref_index[2]:运动估计参考帧列表(貌似H.264这种比较新的标准才会涉及到多参考帧)
int interlaced_frame:是否是隔行扫描
uint8_t motion_subsample_log2:一个宏块中的运动矢量采样个数,取log的
其他的变量不再一一列举,源代码中都有详细的说明。在这里重点分析一下几个需要一定的理解的变量:
方法一: 配置编码的输入数据
第一步: 分配AVFrame对象。
分配对象空间类似于new操作符一样,只是需要调用函数av_frame_alloc。如果失败,那么函数返回一个空指针。
ctx.frame = av_frame_alloc(); //分配AVFrame对象
if (!ctx.frame)
{
fprintf(stderr, “Could not allocate video frame\n”);
return false;
}第二步: AVFrame对象分配成功后,需要设置图像的分辨率和像素格式等
ctx.frame->format = ctx.c->pix_fmt;
ctx.frame->width = ctx.c->width;
ctx.frame->height = ctx.c->height;第三步: 分配实际的像素数据的存储空间。
分配像素的存储空间需要调用av_image_alloc函数,其声明方式为:
int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);
该函数的四个参数分别表示AVFrame结构中的缓存指针、各个颜色分量的宽度、图像分辨率(宽、高)、像素格式和内存对其的大小。该函数会返回分配的内存的大小,如果失败则返回一个负值。具体调用方式如:
ret = av_image_alloc(ctx.frame->data, ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt, 32);
if (ret < 0)
{
fprintf(stderr, “Could not allocate raw picture buffer\n”);
return false;
}需要注意的是,linesize中的值通常指的是stride而不是width,也就是说,像素保存区可能是带有一定宽度的无效边区的,在读取数据时需注意。
方法二: 为frame分配空间
static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
{
AVFrame *picture;
int ret;picture = av_frame_alloc();
if (!picture)
{
return NULL;
}picture->format = pix_fmt;
picture->width = width;
picture->height = height;* allocate the buffers for the frame data *
ret = av_frame_get_buffer(picture, 32);
if (ret < 0)
{
fprintf(stderr, “Could not allocate frame data.\n”);
exit(1);
}return picture;}
编码的输出,解码的输入。AVPacket结构体
各层结构体初始化步骤
解封装->解码(自顶向下初始化)
AVFormatContext->AVStream->AVCodecContext->AVCodec
封装(自底向上初始化)
AVCodec->AVStream->AVFormatContext
->AVCodecContext
本文详细介绍了FFmpeg中关键的数据结构及其对应的协议层,并深入探讨了协议层、封装层、编码层和解码层的主要结构体及初始化步骤。
2758

被折叠的 条评论
为什么被折叠?



