赞
踩
目录
本文对FFmpeg学习过程中的一些知识点,开发流程中用到的数据结构、函数进行梳理总结。
雷神博客:http://blog.csdn.net/column/details/ffmpeg-devel.html
音视频开发--从零到整: https://www.jianshu.com/p/c99ce47f4280
ffmpeg论坛:http://bbs.chinaffmpeg.com/forum.php
ffmpeg官方文档:http://ffmpeg.org/ffmpeg.html
一些基础概念:https://www.cnblogs.com/leisure_chn/p/10285829.html
移动端音视频入门:https://www.imooc.com/learn/959
FFmpeg既是一款音视频编解码工具,同时也是一组音视频编解码开发套件,作为编解码开发套件,它为开发者提供了丰富的音视频的调用接口。
FFMpeg提供了多种媒体格式的封装和解封装,包括多种音视频编码、多种协议的流媒体、多种色彩格式转换、多种采样率转换、多种码率转换等;FFmpeg框架提供了多种丰富的插件模块,包含封装与解封装的插件、编码与解码的插件等。
FFmpeg中的“FF”指的是“Fast Forward”,FFmpeg中的“mpeg”则是“Moving Picture Experts Group(动态图像专家组)”。
FFmpeg的基本组成
FFmpeg框架的基本组成包括AVFormat、AVFilter、AVDevice、AVUtil等模块库。
下面针对这些模块做一个大概的介绍。
AVFormat中实现了目前多媒体领域中的绝大多数媒体封装格式,包括封装和解封装,如MP4、FLV、KV、TS等文件封装格式,RTMP、RTSP、MMS、HLS等网络协议封装格式。
FFmpeg是否支持某种媒体封装格式,取决于编译时是否包含了该格式的封装库。根据实际需求,可进行媒体封装格式的拓展,增加自己定制的封装格式,即在AVFormat中增加自己的封装处理模块。
AVCodec中实现了目前多媒体领域绝大多数常用的编解码格式,既支持编码,也支持解码。AVCodec除了支持MPEG4、AAC、MJPEG等自带的媒体编解码格式之外,还支持第三方的编解码器,如H.264(AVC)编码,需要使用x264编码器;H.265(HEVC)编码,需要使用X265编码器;MP3(mp3lame)编码,需要使用libmp3lame编码器。如果希望增加自己的编码格式,或者硬件编解码,则需要在AVCodec中增加相应的编解码模块。
AVFilter库提供了一个通用的音频、视频、字幕等滤镜处理框架。在AVFilter中,滤镜框架可以有多个输入或多个输出。
swscale模块提供了高级别的图像转换API,例如它允许进行图像缩放和像素格式转换,常见于将图像从1080p转换成720p或者480p等的缩放,或者将图像数据从YUV420P转换成YUYV,或者YUV转RGB等图像格式转换。
swresample模块提供了高级别的音频重采样API。例如它允许操作音频采样、音频通道布局转换与布局调整。
可能包含有视频,音频,字幕等,以及总时长信息
封装格式
音频的编码格式,采样率,通道数,位宽等
视频的编码格式,分辨率,码率,帧率
其他metadata信息,比如所有者,日期等
媒体文件是把原始的声音,图片等信息,经过压缩编码,封装后的结果,相当于对原始数据加了一层外壳。 我们要对媒体文件进行操作(诸如播放,裁剪等),都需要拿到原始的声音,图片信息,也就意味着我们需要经过一系列反操作,拿到最原始的信息进行处理。
从工作流程角度来说,ffmpeg的主要工作流程相对比较简单,具体如下:
1)解封装
2)解码
3)编码
4)封装
其中需要经过6个步骤,具体如下:
1)读取输入源
2)进行音视频的解封装
3)解码每一帧音视频数据
4)编码每一帧音视频数据
5)进行音视频的重新封装
6)输出到目标
从开发角度来说,FFMPEG中结构体很多,最关键的结构体可以分成以下几类:
AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。URLProtocol存储输入视音频使用的封装格式。每种协议都对应一个URLProtocol结构。(注意:FFMPEG中文件也被当做一种协议“file”)
AVIOContext是FFMPEG管理输入输出数据的结构体
AVFormatContext主要存储视音频封装格式中包含的信息,AVFormatContext是包含码流参数较多的结构体,在使用FFMPEG进行开发的时候,AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体;AVInputFormat存储输入视音频使用的封装格式。每种视音频封装格式都对应一个AVInputFormat 结构。
- AVFormatContext关键字段解释:
-
- 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:元数据
元数据(metadata)信息可以通过AVDictionary获取。元数据存储在AVDictionaryEntry结构体中,如下所示
- struct AVDictionary {
- int count;
- AVDictionaryEntry *elems;
- };
- typedef struct AVDictionaryEntry {
- char *key;
- char *value;
- } AVDictionaryEntry;
每一条元数据分为key和value两个属性。
一个媒体中包含多个AVStream流, 每个AVStream存储一个视频/音频流的相关数据;每个AVStream对应一个AVCodecContext,每个AVCodecContext对应一个AVCodec(AVCodecContext是一个描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息,AVCodec包含该视频/音频对应的解码器,每个解码器都对应一个AVCodec结构,且在编译时确定),存储该视频/音频流使用解码方式的相关数据;
AVStream可从AVFormatContext中获取,在解封装操作完成后,我们便可拿到视频/音频流
- AVStream结构体关键字段说明:
-
- int index:标识该视频/音频流
- AVCodecContext *codec:指向该视频/音频流的AVCodecContext(它们是一一对应的关系
- AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。FFMPEG其他结构体中也有这个字段,但是根据我的经验,只有AVStream中的time_base是可用的。PTS*time_base=真正的时间
- int64_t duration:该视频/音频流长度
- AVDictionary *metadata:元数据信息
- AVRational avg_frame_rate:帧率(注:对视频来说,这个挺重要的)
- AVPacket attached_pic:附带的图片。比如说一些MP3,AAC音频文件附带的专辑封面。
- AVCodec结构体关键字段说明:
-
- const char *name:编解码器的名字,比较短
- const char *long_name:编解码器的名字,全称,比较长
- enum AVMediaType type:指明了类型,是视频,音频,还是字幕
- enum AVCodecID id:ID,不重复
- const AVRational *supported_framerates:支持的帧率(仅视频)
- const enum AVPixelFormat *pix_fmts:支持的像素格式(仅视频)
- const int *supported_samplerates:支持的采样率(仅音频)
- const enum AVSampleFormat *sample_fmts:支持的采样格式(仅音频)
- const uint64_t *channel_layouts:支持的声道数(仅音频)
- int priv_data_size:私有数据的大小
AVCodecContex作为编解码器上下文数据结构,是在解封装完毕后从AVStream中获取的(AVCodecContex的内容是动态获取的,而AVCodec是静态的(const),在编译时系统就已经确定了所有的编码器和解码器,这一点从AVCodecContext的AVCodec字段使用const修饰就能看出来)
- typedef struct AVCodecContext {
- /**
- * information on struct for av_log
- * - set by avcodec_alloc_context3
- */
- const AVClass *av_class;
- int log_level_offset;
-
- enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
- const struct AVCodec *codec;//采用const修饰
- enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */
- .
- .
- .
- }
-
- enum AVMediaType codec_type:编解码器的类型(视频,音频...)
- struct AVCodec *codec:采用的解码器AVCodec(H.264,MPEG2...)
- int bit_rate:平均比特率
- uint8_t *extradata; int extradata_size:针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)
- AVRational time_base:根据该参数,可以把PTS转化为实际的时间(单位为秒s)
- int width, height:如果是视频的话,代表宽和高
- int refs:运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了)
- int sample_rate:采样率(音频)
- int channels:声道数(音频)
- enum AVSampleFormat sample_fmt:采样格式
- int profile:型(H.264里面就有,其他编码标准应该也有)
- int level:级(和profile差不太多)
视频的话,每个结构一般是存一帧;音频可能有好几帧
解码前数据:AVPacket
AVPacket是存储压缩编码数据相关信息的结构体
- AVPacket结构体字段说明:
-
- uint8_t *data:压缩编码的数据。
- 例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
-
- 注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
-
- 因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
- int size:data的大小
- int64_t pts:显示时间戳
- int64_t dts:解码时间戳
- int stream_index:标识该AVPacket所属的视频/音频流。
解码后数据:AVFrame
AVFrame是包含码流参数较多的结构体
AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),此外还包含了一些相关的信息。比如说,解码的时候存储了宏块类型表,QP表,运动矢量表等数据。编码的时候也存储了相关的数据。因此在使用FFMPEG进行码流分析的时候,AVFrame是一个很重要的结构体。
- AVFrame结构体关键字段说明:
-
- uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
- 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的
参考: http://blog.csdn.net/leixiaohua1020/article/details/14214577
对于packed格式的数据(eg:RGB24),会存到data[0]里面。
对于planar格式的数据(例如YUV420P),则会分开成data[0],data[1],data[2]...(YUV420P中data[0]存Y,data[1]存U,data[2]存V)
本节通过代码片段的形式对FFmpeg工作流程中涉及到的核心函数、数据结构进行说明,主要是理清各个数据结构之间的关联, 要参考完整代码,可参考我的其他文章。
通常对于网络流媒体,我们需要先解协议,再进行解封装操作,而对于本地视频文件,则从解封装开始,就是 “打开码流”,然后再“ 解析码流信息”,在 ffmpeg 中,这两步任务主要通过 `avformat_open_input` 和 `avformat_find_stream_info` 函数来完成,前者负责服务器的连接和码流头部信息的拉取,后者则主要负责媒体信息的探测和分析工作,这两步的示例代码如下:
- AVFormatContext *ic = avformat_alloc_context();
- if (avformat_open_input(&ic, url, NULL, NULL) < 0) {//解封装
- LOGE("could not open source %s", url);
- return -1;
- }
- if (avformat_find_stream_info(ic, NULL) < 0) {
- LOGE("could not find stream information");
- return -1;
- }
avformat_find_stream_info函数主要用于给每个媒体流(音频/视频)的AVStream结构体赋值,它其实已经实现了解码器的查找,解码器的打开,视音频帧的读取,视音频帧的解码等工作。换句话说,该函数实际上已经“走通”的解码的整个流程。
封装格式、总时长和总码率可以拿到了。另外,由于 AVStream **streams 还详细记录了每一路流的媒体信息,我们可以从中找到视频流的位置:
-
- int i = 0,videoindex = 0;
- for(i=0;i<pInFmtContext->nb_streams;i++){
- if( pInFmtContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
- videoindex = i;
- break;
- }
- }
- printf("vodeoindex2=%d\n",videoindex);
- if( videoindex == -1){
- printf("couldn't find a video stream\n");
- return -1;
- }
拿到视频流AVStream之后,我们可以获取视频流编解码上下文结构AVCodecContext,进而可以获取视频流编解码结构AVCodec,代码如下:
- AVCodecContext *pInCodecCtx;
- AVCodec *pInCodec;
- pInCodecCtx = pInFmtContext->streams[videoindex]->codec;
-
- //根据编码器id,查找对应的解码器
- pInCodec = avcodec_find_decoder(pInCodecCtx->codec_id);
-
- if( avcodec_open2( pInCodecCtx, pInCodec,NULL) < 0){
- printf("avcodec_open2 failed\n");
- return -1;
- }
读取一包数据并解码:
- if( av_read_frame( pInFmtContext, in_packet) >= 0){//读取一包,存放在AVPacket中
- if( in_packet->stream_index == videoindex){
- ret = avcodec_decode_video2(pInCodecCtx, pInFrame, &got_picture, in_packet);//将AVPacket解码,将解码后的数据存放在AVFrame中
- if( ret < 0){
- printf("avcodec_decode_video2 failed:%d\n", ret);
- return -1;
- }
- }
到这里, pInFrame(属于AVFrame类型)就是最原始的视频数据,我们可以对原始数据进行处理(缩放,裁剪等)
AVFrame结构体存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),业务层更多的逻辑都集中在这一层进行处理。
我们需要对pInFrame进行操作,比如,缩放:
sws_scale( img_convert_ctx, (const uint8_t * const)pInFrame->data, pInFrame->linesize, 0, pOutCodecCtx->height, pOutFrame->data, pOutFrame->linesize);
(在调用sws_scale之前,需要调用sws_getContext进行初始化,这里不再详细介绍)
pOutFrame为处理后的结果,同样是原始数据形式。
对原始数据处理完毕后,还需要进行编码压缩,存储,代码片段如下:
-
- avformat_write_header(pOutFmtContext, NULL);//写入封装格式头
-
- while(1){
- ret = avcodec_encode_video2(pOutCodecCtx, &out_packet, pOutFrame, &got_picture);//对原始数据pOutFrame进行编码,结果保存在out_packet(AVPacket)中
-
- if( ret < 0){
- av_free_packet(in_packet);
- break;
- }
- if( got_picture == 1){
- av_packet_rescale_ts(&out_packet, pOutCodecCtx->time_base, out_stream->time_base);// pts设置
- ret = av_interleaved_write_frame(pOutFmtContext, &out_packet);//保存数据
- }
- }
-
- av_write_trailer(pOutFmtContext);//写入封装格式尾
这里需要注意的是pts的设置,可参考我的其他文章。
结构体,函数操作关系流程:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。