您的位置:首页 > 其它

ffmpeg视频播放过程

2013-04-05 06:20 267 查看
原文出自百度文库,但是要花费10个财富值,所以我干脆复制出来,并做简单编辑

出处:http://wenku.baidu.com/view/9ac5934be518964bcf847c2b.html?from_page=view&from_mod=download

另外,大家参考此处:http://my.oschina.net/u/555701/blog/56616

频播放过程

//2013-04-07 添加, 大概思路如下

1. 注册所有容器格式和CODEC:av_register_all()

2. 打开文件:av_open_input_file()

3. 从文件中提取流信息:av_find_stream_info()

4. 穷举所有的流,查找其中种类为CODEC_TYPE_VIDEO

5. 查找对应的解码器:avcodec_find_decoder()

6. 打开编解码器:avcodec_open()

7. 为解码帧分配内存:avcodec_alloc_frame()

8. 不停地从码流中提取出帧数据:av_read_frame()

9. 判断帧的类型,对于视频帧调用:avcodec_decode_video()

10. 解码完后,释放解码器:avcodec_close()

11. 关闭输入文件:av_close_input_file()

首先简单介绍以下视频文件的相关知识。我们平时看到的视频文件有许多格式,比如 avi, mkv, rmvb, mov, mp4等等,这些被称为容器(Container),不同的容器格式规定了其中音视频数据的组织方式(也包括其他数据,比如字幕等)。容器中一般会封装有视频和音频轨,也称为视频流(stream)和音频流,播放视频文件的第一步就是根据视频文件的格式,解析(demux)出其中封装的视频流、音频流以及字幕(如果有的话),解析的数据读到包(packet)中,每个包里保存的是视频帧(frame)或音频帧,然后分别对视频帧和音频帧调用相应的解码器(decoder)进行解码,比如使用H.264编码的视频和MP3编码的音频,会相应的调用H.264解码器和MP3解码器,解码之后得到的就是原始的图像(YUV
or RGB)和声音(PCM)数据,然后根据同步好的时间将图像显示到屏幕上,将声音输出到声卡,最终就是我们看到的视频。

FFmpeg的API就是根据这个过程设计的,因此使用FFmpeg来处理视频文件的方法非常直观简单。下面就一步一步介绍从视频文件中解码出图片的过程。

声明变量

首先定义整个过程中需要使用到的变量:

int main(int argc, const char *argv[])

{

AVFormatContext *pFormatCtx = NULL;

int i, videoStream;

AVCodecContext *pCodecCtx;

AVCodec *pCodec;

AVFrame *pFrame;

AVFrame *pFrameRGB;

AVPacket packet;

int frameFinished;

int numBytes;

uint8_t *buffer;

AVFormatContext:保存需要读入的文件的格式信息,比如流的个数以及流数据等

AVCodecCotext:保存了相应流的详细编码信息,比如视频的宽、高,编码类型等。

pCodec:真正的编解码器,其中有编解码需要调用的函数

AVFrame:用于保存数据帧的数据结构,这里的两个帧分别是保存颜色转换前后的两帧图像

AVPacket:解析文件时会将音/视频帧读入到packet中

打开文件

接下来我们打开一个视频文件。

av_register_all();

av_register_all 定义在 libavformat 里,调用它用以注册所有支持的文件格式以及编解码器,从其实现代码里可以看到它会调用 avcodec_register_all,因此之后就可以用所有ffmpeg支持的codec了。

if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0 )

return -1;

使用新的APIavformat_open_input来打开一个文件,第一个参数是一个AVFormatContext指针变量的地址,它会根据打开的文件信息填充AVFormatContext,需要注意的是,此处的pFormatContext必须为NULL或由avformat_alloc_context分配得到,这也是上一节中将其初始化为NULL的原因,否则此函数调用会出问题。第二个参数是打开的文件名,通过argv[1]指定,也就是命令行的第一个参数。后两个参数分别用于指定特定的输入格式(AVInputFormat)以及指定文件打开额外参数的AVDictionary结构,这里均留作NULL。

if(avformat_find_stream_info(pFormatCtx, NULL ) < 0 )

return -1;

av_dump_format(pFormatCtx, -1, argv[1], 0);

avformat_open_input函数只是读文件头,并不会填充流信息,因此我们需要接下来调用avformat_find_stream_info获取文件中的流信息,此函数会读取packet,并确定文件中所有的流信息,设置pFormatCtx->streams指向文件中的流,但此函数并不会改变文件指针,读取的packet会给后面的解码进行处理。

最后调用一个帮助函数av_dump_format,输出文件的信息,也就是我们在使用ffmpeg时能看到的文件详细信息。第二个参数指定输出哪条流的信息,-1表示给ffmpeg自己选择。最后一个参数用于指定dump的是不是输出文件,我们dump的是输入文件,因此一定要是0。

现在pFormatCtx->streams 中已经有所有流了,因此现在我们遍历它找到第一条视频流:

videoStream = -1;

for( i = 0; i < pFormatCtx->nb_streams; i++ )

if( pFormatCtx->streams[i]->codec->codec_type ==AVMEDIA_TYPE_VIDEO) {

videoStream = i;

break;

}

if(videoStream == -1 )

return -1;

codec_type 的宏定义已经由以前的 CODEC_TYPE_VIDEO 改为 AVMEDIA_TYPE_VIDEO 了。接下来我们通过这条 video stream 的编解码信息打开相应的解码器:

pCodecCtx = pFormatCtx->streams[videoStream]->codec;

pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

if(pCodec == NULL )

return -1;

if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0 )

return -1;

分配图像缓存

接下来我们准备给即将解码的图片分配内存空间。

pFrame = avcodec_alloc_frame();

if(pFrame == NULL )

return -1;

pFrameRGB = avcodec_alloc_frame();

if(pFrameRGB == NULL )

return -1;

调用avcodec_alloc_frame 分配帧,因为最后我们会将图像写成 24-bits RGB 的 PPM 文件,因此这里需要两个AVFrame,pFrame用于存储解码后的数据,pFrameRGB用于存储转换后的数据。

numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,

pCodecCtx->height);

这里调用 avpicture_get_size,根据 pCodecCtx 中原始图像的宽高计算 RGB24 格式的图像需要占用的空间大小,这是为了之后给 pFrameRGB 分配空间。

buffer = av_malloc(numBytes);

avpicture_fill( (AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,

pCodecCtx->width, pCodecCtx->height);

接着上面的,首先是用av_malloc 分配上面计算大小的内存空间,然后调用 avpicture_fill 将 pFrameRGB 跟 buffer 指向的内存关联起来。

获取图像

OK,一切准备好就可以开始从文件中读取视频帧并解码得到图像了。

i =0;

while( av_read_frame(pFormatCtx, &packet) >= 0 ) {

if( packet.stream_index == videoStream ) {

avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,&packet);

if( frameFinished ) {

structSwsContext *img_convert_ctx = NULL;

img_convert_ctx=

sws_getCachedContext(img_convert_ctx,pCodecCtx->width,

pCodecCtx->height,pCodecCtx->pix_fmt,

pCodecCtx->width,pCodecCtx->height,

PIX_FMT_RGB24, SWS_BICUBIC,

NULL, NULL, NULL);

if(!img_convert_ctx ) {

fprintf(stderr, "Cannot initialize swsconversion context\n");

exit(1);

}

sws_scale(img_convert_ctx,(const uint8_t* const*)pFrame->data,

pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,

pFrameRGB->linesize);

if(i++ < 50 )

SaveFrame(pFrameRGB, pCodecCtx->width,pCodecCtx->height, i);

}

}

av_free_packet(&packet);

}

av_read_frame 从文件中读取一个packet,对于视频来说一个packet里面包含一帧图像数据,音频可能包含多个帧(当音频帧长度固定时),读到这一帧后,如果是视频帧,则使用 avcodec_decode_video2 对packet中的帧进行解码,有时候解码器并不能从一个packet中解码得到一帧图像数据(比如在需要其他参考帧的情况下),因此会设置 frameFinished,如果已经得到下一帧图像则设置 frameFinished 非零,否则为零。所以这里我们判断 frameFinished
是否为零来确定 pFrame 中是否已经得到解码的图像。注意在每次处理完后需要调用 av_free_packet 释放读取的packet。

解码得到图像后,很有可能不是我们想要的 RGB24 格式,因此需要使用 swscale 来做转换,调用 sws_getCachedContext 得到转换上下文,使用 sws_scale 将图形从解码后的格式转换为 RGB24,最后将前50帧写人 ppm 文件。最后释放图像以及关闭文件:

av_free(buffer);

av_free(pFrameRGB);

av_free(pFrame);

avcodec_close(pCodecCtx);

avformat_close_input(&pFormatCtx);

return 0;

}

static void SaveFrame(AVFrame *pFrame, intwidth, int height, int iFrame)

{

FILE *pFile;

char szFilename[32];

inty;

sprintf(szFilename, "frame%d.ppm", iFrame);

pFile = fopen(szFilename, "wb");

if(!pFile )

return;

fprintf(pFile, "P6\n%d %d\n255\n", width, height);

for( y = 0; y < height; y++ )

fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3,pFile);

fclose(pFile);

}

重点分析AVCodec/AVCodecContext/MsrleContext这几个数据结构,这几个数据结构定义了编解码

器的核心架构,相当于Directshow中的各种音视频解码器decoder。

typedefstructAVCodec

{

constchar*name; // 标示Codec的名字, 比如,"msrle""truespeech" 等。

enumCodecTypetype; // 标示Codec的类型,有Video,Audio,Data等类型

enumCodecIDid; // 标示Codec的ID,有CODEC_ID_MSRLE,CODEC_ID_TRUESPEECH等

intpriv_data_size; // 标示具体的Codec对应的Context的大小,在本例中是 MsrleContext // 或TSContext的大小。

int(*init)(AVCodecContext*);// 标示Codec对外提供的操作

int(*encode)(AVCodecContext*,uint8_t*buf,intbuf_size, void*data);

int(*close)(AVCodecContext*);

int(*decode)(AVCodecContext*,void*outdata,int*outdata_size,uint8_t*buf,intbuf_size);

intcapabilities; // 标示Codec的能力,在瘦身后的ffplay中没太大作用,可忽略

structAVCodec*next; // 用于把所有Codec串成一个链表,便于遍历

}AVCodec;

AVCodec 是类似COM接口的数据结构,表示音视频编解码器,着重于功能函数,一种媒体类型对应一个

AVCodec结构,在程序运行时有多个实例。next变量用于把所有支持的编解码器连接成链表,便于遍历查找;id

确定了唯一编解码器;priv_data_size表示具体的Codec 对应的 Context结构大小,比如MsrleContext 或

TSContext,这些具体的结够定义散落于各个.c文件中,为避免太多的ifelse类语句判断类型再计算大小,这里

就直接指明大小,因为这是一个编译时静态确定的字段,所以放在AVCodec而不是AVCodecContext中。

typedefstructAVCodecContext

{

intbit_rate;

intframe_number;

unsignedchar*extradata;//Codec的私有数据,对Audio是WAVEFORMATEX结构扩展字节。

intextradata_size; // 对Video是BITMAPINFOHEADER后的扩展字节

intwidth,height; // 此逻辑段仅针对视频

enumPixelFormatpix_fmt;

intsample_rate; // 此逻辑段仅针对音频

intchannels;

intbits_per_sample;

intblock_align;

structAVCodec*codec; // 指向当前AVCodec的指针,

void*priv_data; // 指向当前具体编解码器Codec的上下文Context。

enumCodecTypecodec_type;//seeCODEC_TYPE_xxx

enumCodecIDcodec_id; //seeCODEC_ID_xxx

int(*get_buffer)(structAVCodecContext*c,AVFrame*pic);

void(*release_buffer)(structAVCodecContext*c,AVFrame*pic);

int(*reget_buffer)(structAVCodecContext*c,AVFrame*pic);

intinternal_buffer_count;

void*internal_buffer;

structAVPaletteControl*palctrl;

}AVCodecContext;

AVCodecContext结构表示程序运行的当前Codec使用的上下文,着重于所有Codec共有的属性(并且是在程

序运行时才能确定其值)和关联其他结构的字段。extradata和extradata_size两个字段表述了相应Codec使用的私

有数据,对Codec全局有效,通常是一些标志信息;codec字段关联相应的编解码器;priv_data字段关联各个具

体编解码器独有的属性上下文,和AVCodec结构中的priv_data_size 配对使用。

typedefstructMsrleContext

{

AVCodecContext*avctx;

AVFrameframe;

unsignedchar*buf;

intsize;

}MsrleContext;

MsrleContext结构着重于RLE行程长度压缩算法独有的属性值和关联AVCodecContext 的avctx字段。因为

RLE行程长度算法足够简单,属性值相对较少。

接着来重点分析AVInputFormat/AVFormatContext/AVIContext这几个数据结构,这几个数据结构定义了识别文件容器格式的核心架构,相当于Directshow中的各种解复用demuxer。

typedefstructAVInputFormat

{

constchar*name;

intpriv_data_size; // 标示具体的文件容器格式对应的Context的大小,在本例中是AVIContext

int(*read_probe)(AVProbeData*);

int(*read_header)(structAVFormatContext*,AVFormatParameters*ap);

int(*read_packet)(structAVFormatContext*,AVPacket*pkt);

int(*read_close)(structAVFormatContext*);

constchar*extensions;// 文件扩展名

structAVInputFormat*next;

}AVInputFormat;

AVInputFormat是类似COM接口的数据结构,表示输入文件容器格式,着重于功能函数,一种文件容器格

式对应一个AVInputFormat结构,在程序运行时有多个实例。next变量用于把所有支持的输入文件容器格式连接

成链表,便于遍历查找;priv_data_size标示具体的文件容器格式对应的Context的大小,在本例中是AVIContext,

这些具体的结够定义散落于各个.c文件中,为避免太多的ifelse 类语句判断类型再计算大小,这里就直接指明大小,因为这是一个编译时静态确定的字段,所以放在AVInputFormat而不是AVFormatContext中。

typedefstructAVFormatContext //formatI/Ocontext

{

structAVInputFormat*iformat;

void*priv_data; // 指向具体的文件容器格式的上下文Context,在本例中是AVIContext

ByteIOContextpb; // 广泛意义的输入文件

intnb_streams;

AVStream*streams[MAX_STREAMS];

}AVFormatContext;

AVFormatContext结构表示程序运行的当前文件容器格式使用的上下文,着重于所有文件容器共有的属性(并且是在程序运行时才能确定其值)和关联其他结构的字段。iformat字段关联相应的文件容器格式;pb关联广义的

输入文件;streams关联音视频流;priv_data字段关联各个具体文件容器独有的属性上下文,和priv_data_size配

对使用。

typedefstructAVIContext

{

int64_triff_end;

int64_tmovi_end;

offset_tmovi_list;

intnon_interleaved;

intstream_index_2;// 为了和AVPacket中的stream_index相区别,添加后缀标记。

}AVIContext;

AVIContext定义了AVI中流的一些属性,其中stream_index_2定义了当前应该读取流的索引。

接着我们来重点分析URLProtocol/URLContext(ByteIOContext)/FILE(Socket)这几个数据结构,这几个数据结

构定义了读取文件的核心架构,相当于Directshow中的文件源filesourcefilter。

typedefstructURLProtocol

{

constchar*name; // 便于人性化的识别理解

int(*url_open)(URLContext*h,constchar*filename,intflags);

int(*url_read)(URLContext*h,unsignedchar*buf,intsize);

int(*url_write)(URLContext*h,unsignedchar*buf,intsize);

offset_t(*url_seek)(URLContext*h,offset_tpos,intwhence);

int(*url_close)(URLContext*h);

structURLProtocol*next;

}URLProtocol;

URLProtocol是类似COM接口的数据结构,表示广义的输入文件,着重于功能函数,一种广义的输入文件

对应一个URLProtocol结构,比如file,pipe,tcp等等,但瘦身后的ffplay只支持file一种输入文件。next变量

用于把所有支持的广义的输入文件连接成链表,便于遍历查找。

typedefstructURLContext

{

structURLProtocol*prot;

intflags;

intmax_packet_size;//ifnonzero,thestreamispacketizedwiththismaxpacketsize

void*priv_data; // 文件句柄fd,网络通信Scoket等

charfilename[1]; //specifiedfilename

}URLContext;

URLContext结构表示程序运行的当前广义输入文件使用的上下文,着重于所有广义输入文件共有的属性(并

且是在程序运行时才能确定其值)和关联其他结构的字段。prot字段关联相应的广义输入文件;priv_data字段关

联各个具体广义输入文件的句柄。

typedefstructByteIOContext

{

unsignedchar*buffer;

intbuffer_size;

unsignedchar*buf_ptr, *buf_end;

void*opaque; // 关联URLContext

int(*read_buf)(void*opaque,uint8_t*buf,intbuf_size);

int(*write_buf)(void*opaque,uint8_t*buf,intbuf_size);

offset_t(*seek)(void*opaque,offset_toffset,intwhence);

offset_tpos; //positioninthefileofthecurrentbuffer

intmust_flush; //trueifthenextseekshouldflush

inteof_reached; //trueifeofreached

intwrite_flag; //trueifopenforwriting

intmax_packet_size;

interror; //containstheerrorcodeor0ifnoerrorhappened

}ByteIOContext;

ByteIOContext结构扩展URLProtocol结构成内部有缓冲机制的广泛意义上的文件,改善广义输入文件的IO

性能。由其数据结构定义的字段可知,主要是缓冲区相关字段,标记字段,和一个关联字段opaque来完成广义

文件读写操作。opaque关联字段用于关联URLContext结构,间接关联并扩展URLProtocol结构。

接着我们来重点分析AVStream/AVIStream这几个数据结构,这几个数据结构定义了解析媒体流的核心属性,

主要用于读取媒体流数据,相当于Directshow中的解复用Demux内部的流解析逻辑。特别注意此结构关联

AVCodecContext 结构,并经此结构能跳转到其他结构。

typedefstructAVStream // 解析文件容器内部使用的逻辑

{

AVCodecContext*actx; //codeccontext,changefromAVCodecContext*codec;

void*priv_data; //AVIStream

AVRationaltime_base; // 由av_set_pts_info()函数初始化

AVIndexEntry*index_entries;//onlyusediftheformatdoesnotsupportseekingnatively

intnb_index_entries;

intindex_entries_allocated_size;

doubleframe_last_delay;

}AVStream;

AVStream结构表示当前媒体流的上下文,着重于所有媒体流共有的属性(并且是在程序运行时才能确定其值)

和关联其他结构的字段。actx字段关联当前音视频媒体使用的编解码器;priv_data字段关联解析各个具体媒体流

与文件容器有关的独有的属性;还有一些媒体帧索引和时钟信息。

typedefstructAVIStream

{

int64_tframe_offset;//currentframe(video)orbyte(audio)counter(usedtocomputethepts)

intremaining;

intpacket_size;

intscale;

intrate;

intsample_size; //sizeofonesample(orpacket)(intherate/scalesense)inbytes

int64_tcum_len; //temporarystorage(usedduringseek)

intprefix; //normally'd'<<8+'c'or'w'<<8+'b'

intprefix_count;

}AVIStream;

AVIStream 结构定义了AVI文件中媒体流的一些属性,用于解析AVI文件。

接着我们来分析AVPacket/AVPacketList/PacketQueue这几个数据结构,这几个数据结构定义解复用demux

模块输出的音视频压缩数据流队列,相当于Directshow中Demux的OutputPin,传递数据到解码器。

typedefstructAVPacket

{

int64_tpts;//presentationtimestampintime_baseunits

int64_tdts;//decompressiontimestampintime_baseunits

int64_tpos;//bytepositioninstream,-1ifunknown

uint8_t*data;

intsize;

intstream_index;

intflags;

void(*destruct)(structAVPacket*);

}AVPacket;

AVPacket 代表音视频数据帧,固有的属性是一些标记,时钟信息,和压缩数据首地址,大小等信息。

typedefstructAVPacketList

{

AVPacketpkt;

structAVPacketList*next;

}AVPacketList;

AVPacketList 把音视频AVPacket 组成一个小链表。

typedefstructPacketQueue

{

AVPacketList*first_pkt, *last_pkt;

intsize;

intabort_request;

SDL_mutex*mutex;

SDL_cond*cond;

}PacketQueue;

PacketQueue 通过小链表AVPacketList把音视频帧AVPacket 组成一个顺序队列,是数据交换中转站,当然

同步互斥控制逻辑是必不可少的。

最后我们来重点分析VideoState这个数据结构,这个数据结构把主要的数据结构整合在一起,声明成全局变

量,起一个中转的作用,便于在各个子结构之间跳转,相当于一个大背景,大平台的作用。

typedefstructVideoState

{

SDL_Thread*parse_tid;

SDL_Thread*video_tid;

intabort_request;

AVInputFormat*iformat;

AVFormatContext*ic; // 关联的主要数据结构是ByteIOContext 和AVStream

AVStream*audio_st; // 关联的主要数据结构是AVCodecContext 和AVIStream

AVStream*video_st;

intaudio_stream;// 音频流索引,实际表示AVFormatContext结构中AVStream*streams[]数组中的索引

intvideo_stream;// 视频流索引,实际表示AVFormatContext结构中AVStream*streams[]数组中的索引

PacketQueueaudioq; // 音频数据包队列,注意一包音频数据可能包含几个音频帧

PacketQueuevideoq; // 视频数据包队列,注意瘦身后的ffplay一包视频数据是完整的一帧

VideoPicturepictq[VIDEO_PICTURE_QUEUE_SIZE];// 输出视频队列,瘦身后的ffplay只有一项

doubleframe_last_delay;

uint8_taudio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE*3)/2];// 输出的音频缓存

unsignedintaudio_buf_size;

intaudio_buf_index;

AVPacketaudio_pkt; // 音频包属性,只一个指针指向原始音频包数据,非直接包含音频数据包数据

uint8_t*audio_pkt_data;

intaudio_pkt_size;

SDL_mutex*video_decoder_mutex;// 视频线程同步互斥变量

SDL_mutex*audio_decoder_mutex;// 音频线程同步互斥变量

charfilename[240]; // 媒体文件名

}VideoState;

音视频数据流简单流程,由ByteIOContext(URLContext/URLProtocol )表示的广义输入文件,在

AVStream(AVIStreamt)提供的特定文件容器流信息的指引下,用AVInputFormat(AVFormatContext/AVInputFormat

)接口的read_packet()函数读取完整的一帧数据,分别放到音频或视频PacketQueue(AVPacketList/AVPacket)队列

中,这部分功能由decode_thread线程完成。对于视频数据,video_thread线程不停的从视频PacketQueue队列中

取出视频帧,调用AVCodec(AVCodecContext/MsrleContext)接口的decode()函数解码视频帧,在适当延时后做颜

色空间转化并调用 SDL 库显示出来。对于音频数据,SDL播放库播放完缓冲区的 PCM 数据后,调用

sdl_audio_callback()函数解码音频数据,并把解码后的PCM数据填充到SDL音频缓存播放。当下次播放完后,

再调用sdl_audio_callback()函数解码填充,如此循环不已。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: