FFMPEG SDK 教程.docx
《FFMPEG SDK 教程.docx》由会员分享,可在线阅读,更多相关《FFMPEG SDK 教程.docx(71页珍藏版)》请在冰豆网上搜索。
FFMPEGSDK教程
FFPLAY的原理
概要
电影文件有很多基本的组成部分。
首先,文件本身被称为容器Container,容器的类型决定了信息被存放在文件中的位置。
AVI和Quicktime就是容器的例子。
接着,你有一组流,例如,你经常有的是一个音频流和一个视频流。
(一个流只是一种想像出来的词语,用来表示一连串的通过时间来串连的数据元素)。
在流中的数据元素被称为帧Frame。
每个流是由不同的编码器来编码生成的。
编解码器描述了实际的数据是如何被编码Coded和解码DECoded的,因此它的名字叫做CODEC。
Divx和MP3就是编解码器的例子。
接着从流中被读出来的叫做包Packets。
包是一段数据,它包含了一段可以被解码成方便我们最后在应用程序中操作的原始帧的数据。
根据我们的目的,每个包包含了完整的帧或者对于音频来说是许多格式的完整帧。
基本上来说,处理视频和音频流是很容易的:
10从video.avi文件中打开视频流video_stream
20从视频流中读取包到帧中
30如果这个帧还不完整,跳到20
40对这个帧进行一些操作
50跳回到20
在这个程序中使用ffmpeg来处理多种媒体是相当容易的,虽然很多程序可能在对帧进行操作的时候非常的复杂。
因此在这篇指导中,我们将打开一个文件,读取里面的视频流,而且我们对帧的操作将是把这个帧写到一个PPM文件中。
打开文件
首先,来看一下我们如何打开一个文件。
通过ffmpeg,你必需先初始化这个库。
(注意在某些系统中必需用和来替换)
#include
#include
...
intmain(intargc,charg*argv[]){
av_register_all();
这里注册了所有的文件格式和编解码器的库,所以它们将被自动的使用在被打开的合适格式的文件上。
注意你只需要调用av_register_all()一次,因此我们在主函数main()中来调用它。
如果你喜欢,也可以只注册特定的格式和编解码器,但是通常你没有必要这样做。
现在我们可以真正的打开文件:
AVFormatContext*pFormatCtx;
//Openvideofile
if(av_open_input_file(&pFormatCtx,argv[1],NULL,0,NULL)!
=0)
return-1;//Couldn'topenfile
我们通过第一个参数来获得文件名。
这个函数读取文件的头部并且把信息保存到我们给的AVFormatContext结构体中。
最后三个参数用来指定特殊的文件格式,缓冲大小和格式参数,但如果把它们设置为空NULL或者0,libavformat将自动检测这些参数。
这个函数只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
//Retrievestreaminformation
if(av_find_stream_info(pFormatCtx)<0)
return-1;//Couldn'tfindstreaminformation
这个函数为pFormatCtx->streams填充上正确的信息。
我们引进一个手工调试的函数来看一下里面有什么:
//Dumpinformationaboutfileontostandarderror
dump_format(pFormatCtx,0,argv[1],0);
现在pFormatCtx->streams仅仅是一组大小为pFormatCtx->nb_streams的指针,所以让我们先跳过它直到我们找到一个视频流。
inti;
AVCodecContext*pCodecCtx;
//Findthefirstvideostream
videoStream=-1;
for(i=0;inb_streams;i++)
if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO){
videoStream=i;
break;
}
if(videoStream==-1)
return-1;//Didn'tfindavideostream
//Getapointertothecodeccontextforthevideostream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
流中关于编解码器的信息就是被我们叫做"codeccontext"(编解码器上下文)的东西。
这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一个指向他的指针。
但是我们必需要找到真正的编解码器并且打开它:
AVCodec*pCodec;
//Findthedecoderforthevideostream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL){
fprintf(stderr,"Unsupportedcodec!
\n");
return-1;//Codecnotfound
}
//Opencodec
if(avcodec_open(pCodecCtx,pCodec)<0)
return-1;//Couldnotopencodec
有些人可能会从旧的指导中记得有两个关于这些代码其它部分:
添加CODEC_FLAG_TRUNCATED到pCodecCtx->flags和添加一个hack来粗糙的修正帧率。
这两个修正已经不在存在于ffplay.c中。
因此,我必需假设它们不再必要。
我们移除了那些代码后还有一个需要指出的不同点:
pCodecCtx->time_base现在已经保存了帧率的信息。
time_base是一个结构体,它里面有一个分子和分母(AVRational)。
我们使用分数的方式来表示帧率是因为很多编解码器使用非整数的帧率(例如NTSC使用29.97fps)。
保存数据
现在我们需要找到一个地方来保存帧:
AVFrame*pFrame;
//Allocatevideoframe
pFrame=avcodec_alloc_frame();
因为我们准备输出保存24位RGB色的PPM文件,我们必需把帧的格式从原来的转换为RGB。
FFMPEG将为我们做这些转换。
在大多数项目中(包括我们的这个)我们都想把原始的帧转换成一个特定的格式。
让我们先为转换来申请一帧的内存。
//AllocateanAVFramestructure
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
return-1;
即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始的数据。
我们使用avpicture_get_size来获得我们需要的大小,然后手工申请内存空间:
uint8_t*buffer;
intnumBytes;
//Determinerequiredbuffersizeandallocatebuffer
numBytes=avpicture_get_size(PIX_FMT_RGB24,pCodecCtx->width,
pCodecCtx->height);
buffer=(uint8_t*)av_malloc(numBytes*sizeof(uint8_t));
av_malloc是ffmpeg的malloc,用来实现一个简单的malloc的包装,这样来保证内存地址是对齐的(4字节对齐或者2字节对齐)。
它并不能保护你不被内存泄漏,重复释放或者其它malloc的问题所困扰。
现在我们使用avpicture_fill来把帧和我们新申请的内存来结合。
关于AVPicture的结成:
AVPicture结构体是AVFrame结构体的子集――AVFrame结构体的开始部分与AVPicture结构体是一样的。
//AssignappropriatepartsofbuffertoimageplanesinpFrameRGB
//NotethatpFrameRGBisanAVFrame,butAVFrameisasuperset
//ofAVPicture
avpicture_fill((AVPicture*)pFrameRGB,buffer,PIX_FMT_RGB24,
pCodecCtx->width,pCodecCtx->height);
最后,我们已经准备好来从流中读取数据了。
读取数据
我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最好后转换格式并且保存。
intframeFinished;
AVPacketpacket;
i=0;
while(av_read_frame(pFormatCtx,&packet)>=0){
//Isthisapacketfromthevideostream?
if(packet.stream_index==videoStream){
//Decodevideoframe
avcodec_decode_video(pCodecCtx,pFrame,&frameFinished,
packet.data,packet.size);
//Didwegetavideoframe?
if(frameFinished){
//ConverttheimagefromitsnativeformattoRGB
img_convert((AVPicture*)pFrameRGB,PIX_FMT_RGB24,
(AVPicture*)pFrame,pCodecCtx->pix_fmt,
pCodecCtx->width,pCodecCtx->height);
//Savetheframetodisk
if(++i<=5)
SaveFrame(pFrameRGB,pCodecCtx->width,
pCodecCtx->height,i);
}
}
//Freethepacketthatwasallocatedbyav_read_frame
av_free_packet(&packet);
}
这个循环过程是比较简单的:
av_read_frame()读取一个包并且把它保存到AVPacket结构体中。
注意我们仅仅申请了一个包的结构体――ffmpeg为我们申请了内部的数据的内存并通过packet.data指针来指向它。
这些数据可以在后面通过av_free_packet()来释放。
函数avcodec_decode_video()把包转换为帧。
然而当解码一个包的时候,我们可能没有得到我们需要的关于帧的信息。
因此,当我们得到下一帧的时候,avcodec_decode_video()为我们设置了帧结束标志frameFinished。
最后,我们使用img_convert()函数来把帧从原始格式(pCodecCtx->pix_fmt)转换成为RGB格式。
要记住,你可以把一个AVFrame结构体的指针转换为AVPicture结构体的指针。
最后,我们把帧和高度宽度信息传递给我们的SaveFrame函数。
关于包Packets的注释
从技术上讲一个包可以包含部分或者其它的数据,但是ffmpeg的解释器保证了我们得到的包Packets包含的要么是完整的要么是多种完整的帧。
现在我们需要做的是让SaveFrame函数能把RGB信息定稿到一个PPM格式的文件中。
我们将生成一个简单的PPM格式文件,请相信,它是可以工作的。
voidSaveFrame(AVFrame*pFrame,intwidth,intheight,intiFrame){
FILE*pFile;
charszFilename[32];
inty;
//Openfile
sprintf(szFilename,"frame%d.ppm",iFrame);
pFile=fopen(szFilename,"wb");
if(pFile==NULL)
return;
//Writeheader
fprintf(pFile,"P6\n%d%d\n255\n",width,height);
//Writepixeldata
for(y=0;yfwrite(pFrame->data[0]+y*pFrame->linesize[0],1,width*3,pFile);
//Closefile
fclose(pFile);
}
我们做了一些标准的文件打开动作,然后写入RGB数据。
我们一次向文件写入一行数据。
PPM格式文件的是一种包含一长串的RGB数据的文件。
如果你了解HTML色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,就像#ff0000#ff0000....就表示了了个红色的屏幕。
(它被保存成二进制方式并且没有分隔符,但是你自己是知道如何分隔的)。
文件的头部表示了图像的宽度和高度以及最大的RGB值的大小。
现在,回顾我们的main()函数。
一旦我们开始读取完视频流,我们必需清理一切:
//FreetheRGBimage
av_free(buffer);
av_free(pFrameRGB);
//FreetheYUVframe
av_free(pFrame);
//Closethecodec
avcodec_close(pCodecCtx);
//Closethevideofile
av_close_input_file(pFormatCtx);
return0;
你会注意到我们使用av_free来释放我们使用avcode_alloc_fram和av_malloc来分配的内存。
上面的就是代码!
下面,我们将使用Linux或者其它类似的平台,你将运行:
gcc-otutorial01tutorial01.c-lavutil-lavformat-lavcodec-lz-lavutil-lm
如果你使用的是老版本的ffmpeg,你可以去掉-lavutil参数:
gcc-otutorial01tutorial01.c-lavutil-lavformat-lavcodec-lz-lm
大多数的图像处理函数可以打开PPM文件。
可以使用一些电影文件来进行测试。
输出到屏幕
SDL和视频
为了在屏幕上显示,我们将使用SDL.SDL是SimpleDirectLayer的缩写。
它是一个出色的多媒体库,适用于多平台,并且被用在许多工程中。
你可以从它的官方网站的网址http:
//www.libsdl.org/上来得到这个库的源代码或者如果有可能的话你可以直接下载开发包到你的操作系统中。
按照这个指导,你将需要编译这个库。
(剩下的几个指导中也是一样)
SDL库中有许多种方式来在屏幕上绘制图形,而且它有一个特殊的方式来在屏幕上显示图像――这种方式叫做YUV覆盖。
YUV(从技术上来讲并不叫YUV而是叫做YCbCr)是一种类似于RGB方式的存储原始图像的格式。
粗略的讲,Y是亮度分量,U和V是色度分量。
(这种格式比RGB复杂的多,因为很多的颜色信息被丢弃了,而且你可以每2个Y有1个U和1个V)。
SDL的YUV覆盖使用一组原始的YUV数据并且在屏幕上显示出他们。
它可以允许4种不同的YUV格式,但是其中的YV12是最快的一种。
还有一个叫做YUV420P的YUV格式,它和YV12是一样的,除了U和V分量的位置被调换了以外。
420意味着它以4:
2:
0的比例进行了二次抽样,基本上就意味着1个颜色分量对应着4个亮度分量。
所以它的色度信息只有原来的1/4。
这是一种节省带宽的好方式,因为人眼感觉不到这种变化。
在名称中的P表示这种格式是平面的――简单的说就是Y,U和V分量分别在不同的数组中。
FFMPEG可以把图像格式转换为YUV420P,但是现在很多视频流的格式已经是YUV420P的了或者可以被很容易的转换成YUV420P格式。
于是,我们现在计划把指导1中的SaveFrame()函数替换掉,让它直接输出我们的帧到屏幕上去。
但一开始我们必需要先看一下如何使用SDL库。
首先我们必需先包含SDL库的头文件并且初始化它。
#include
#include
if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_TIMER)){
fprintf(stderr,"CouldnotinitializeSDL-%s\n",SDL_GetError());
exit
(1);
}
SDL_Init()函数告诉了SDL库,哪些特性我们将要用到。
当然SDL_GetError()是一个用来手工除错的函数。
创建一个显示
现在我们需要在屏幕上的一个地方放上一些东西。
在SDL中显示图像的基本区域叫做面surface。
SDL_Surface*screen;
screen=SDL_SetVideoMode(pCodecCtx->width,pCodecCtx->height,0,0);
if(!
screen){
fprintf(stderr,"SDL:
couldnotsetvideomode-exiting\n");
exit
(1);
}
这就创建了一个给定高度和宽度的屏幕。
下一个选项是屏幕的颜色深度――0表示使用和当前一样的深度。
(这个在OSX系统上不能正常工作,原因请看源代码)
现在我们在屏幕上来创建一个YUV覆盖以便于我们输入视频上去:
SDL_Overlay*bmp;
bmp=SDL_CreateYUVOverlay(pCodecCtx->width,pCodecCtx->height,
SDL_YV12_OVERLAY,screen);
正如前面我们所说的,我们使用YV12来显示图像。
显示图像
前面那些都是很简单的。
现在我们需要来显示图像。
让我们看一下是如何来处理完成后的帧的。
我们将原来对RGB处理的方式,并且替换SaveFrame()为显示到屏幕上的代码。
为了显示到屏幕上,我们将先建立一个AVPicture结构体并且设置其数据指针和行尺寸来为我们的YUV覆盖服务:
if(frameFinished){
SDL_LockYUVOverlay(bmp);
AVPicturepict;
pict.data[0]=bmp->pixels[0];
pict.data[1]=bmp->pixels[2];
pict.data[2]=bmp->pixels[1];
pict.linesize[0]=bmp->pitches[0];
pict.linesize[1]=bmp->pitches[2];
pict.linesize[2]=bmp->pitches[1];
//ConverttheimageintoYUVformatthatSDLuses
img_convert(&pict,PIX_FMT_YUV420P,
(AVPicture*)pFrame,pCodecCtx->pix_fmt,
pCodecCtx->width,pCodecCtx->height);
SDL_UnlockYUVOverlay(bmp);
}
首先,我们锁定这个覆盖,因为我们将要去改写它。
这是一个避免以后发生问题的好习惯。
正如前面所示的,这个AVPicture结构体有一个数据指针指向一个有4个元素的指针数据。
由于我们处理的是YUV420P,所以我们只需要3个通道即只要三组数据。
其它的格式可能需要第四个指针来表示alpha通道或者其它参数。
行尺寸正如它的名字表示的意义一样。
在YUV覆盖中相同功能的结构体是像素pixel和程度pitch。
(程度pitch是在SDL里用来表示指定行数据宽度的值)。
所以我们现在做的是让我们的覆盖中的pict.data中的三个指针有一个指向必要的空间的地址。
类似的,我们可以直接从覆盖中得到行尺寸信息。
像前面一样我们使用img_convert来把格式转换成PIX_FMT_YUV420P。
绘制图像
但我们仍然需要告诉SDL如何来实际显示我们给的数据。
我们也会传递一个表明电影位置、宽度、高度和缩放大小的矩形参数给SDL的函数。
这样,SDL为我们做缩放并且它可以通过显卡的帮忙来进行快速缩放。
SDL_Rectrect;
if(frameFinished){
//ConverttheimageintoYUVformatthatSDLuses
img_convert(&pict,PIX_FMT_YUV420P,
(AVPicture*)pFrame,pCodecCtx->pix_fmt,
pCodecCtx->width,pCodecCtx->height);
SDL_UnlockYUVOverlay(bmp);
rect.x=0;
rect.y=0;
rect.w=pCodecCtx->width;
rect.h=pCodecCtx->height;
SDL_DisplayYUVOverlay(bmp,&rect);
}
让我们再花一点时间来看一下SDL的特性:
它的事件驱动系统。
SDL被设置成当你在SDL中点击或者移动鼠标或者向它发送一个信号它都将产生一个事件的驱动方式。
如果你的程序想要处理用户输入的话,它就会检测这些事件。
你的程序也可以产生事件并且传递给SDL事件系统。
当使用SDL进行多线程编程的时候,这相当有用,这方面代码我们可以在指导4中看到。
在这个程序中,我们将在处理完包以后就立即轮询事件。
现在而言,我们将处理SDL_QUIT事件以便于我们退出:
SDL_Eventevent;
av_free_packet(&packet);
SDL_PollEvent(&event);
switch(event.type){
caseSDL_QUIT:
SDL_Quit();
exit(0);
break;
default:
break;
}
让我们去掉旧的冗余代码,开始编译。
如果你使用的是Linux或者其变体,使用SDL库进行编译的最好方式为:
gcc-otutorial02tutorial02.c-lavutil-lavformat-lavcodec-lz-lm\
`sdl-config--cflags--libs`
这里的sdl-config命令会打印出用于gcc编译的包含正确SDL库的适当参数。
为了进行编译,在你自己的平台你可能需要做的有点不同:
请查阅一下SDL文档中关于你的系统的那部分。
一旦可以编译,就马上运行它。
当运行这个程序的时候会