Tutorial 1 Making Screencaps(tutorial01.c)
概述
电影文件有几个基本的组成部分。首先,文件本身被称为一个容器,容器的类型决定了在文件中的信息的范围。容器的例子有AVI和QuickTime。接下来,你有一堆的信息流,例如,你通常有一个音频流和视频流。 (一个“流”只是一个为“在时间上可用的连续的数据元素”而想象的词。)在一个流中的数据元素被称为帧(frame)。每个数据流进行编码由不同类型的解码器。该编解码器定义的实际数据是如何编码和解码 - 因而得名编解码器。例如DivX的编解码器和MP3编解码器。然后从流中读取数据包。数据包(packet)是包含数据位的数据,我们可以将数据位解码为最终可以为我们的应用程序操作的原始帧。就我们而言,每个数据包包含完整的帧,或者音频里的多个帧。
在非常基本的水平下,处理视频和音频流是很容易的:
10 OPEN video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20
用ffmpeg处理多媒体就像这个程序一样非常简单,尽管一些程序会有非常复杂的”DO SOMETHING ”步骤。所以在本教程中,我们会有打开文件、从视频帧中读取数据并写入文件和我们的“DO SOMETHING“步骤——将帧写入PPM文件。
打开文件
首先,我们先看一下怎样打开一个文件。对于ffmpeg,我们必须先初始化库(请注意,有些系统可能使用<ffmpeg/avcodec.h>和<ffmpeg/avformat.h>代替)。
#include <avcodec.h>
#include <avformat.h>
------
int main(int argc,charg *argv[])
{
av_register_all();
这个带库的注册对所有文件格式与编解码器都适用,使他们能够与时自动使用相应的格式文件/编解码器打开。请注意,你只需要在main函数中调用av_register_all()一次。如果你喜欢,它可能只是某些个人注册的文件格式和编解码器,但通常是没有理由你就必须这样做。
现在,我们可以真正打开一个文件:
AVFormatContext *pFormatCtx;
//Open video file
if(av_open_input_file(&pFormatCtx,argv[1],NULL,0,NULL) != 0)
return -1;//Couldn’t open file
我们从第一个参数获取文件名。这个函数读取文件头,并存储有关文件格式的信息到AVFormatContext数据结构。后面三个参数用来指定文件格式、缓冲区大小和格式选项,但是被设置为Null或0时,libavformat会自动检测。
这个函数只查看文件头,所有下一步我们需要检测文件中的流信息:
//Retrieve stream information
if(av_find_stream_info(pFormatCtx)<0)
return -1;//Couldn’t find stream information
次函数用恰当的信息填充pFormatCtx->streams,我们引入一个方便的调试函数来告诉我们里面发生了什么:
//Dump information about file onto standard error
dump_format(pFormatCtx,0,arg[1],0);
现在pFormatCtx->stream只是一个大小为pFormatCtx->nb_streams的数组指针,让我们跟踪它直到我们找到视频流:
int i;
AVCodeContext *pCodecCtx;
// Find the first video stream
videoStream = -1;
for(i=0;i<pFormatCtx->nb_streams;i++)
if(pFormatCtx->stream[i]->codec->codec_type == CODEC_TYPE_VIDEO) {
videoStream = i ;
break;
}
if(videoStream ==-1)
return -1;//Didn’t find a video stream
//Get a pointer to the codec context for the video stream
pCodecCtx = pFormatCtx->streams[videoStream]->codec;
关于编解码器的流信息在我们所谓的codec context中。这个容器中所有关于流正在应用的编解码器的信息,现在我们有个指针指向它。但是我们仍然要找到编解码器并打开它:
AVCodec *pCodec;
//Find the decoder for the video stream
pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec = NULL) {
fprintf(stderr,”Unsuoported codec!/n”);
return -1; //Codec not found
}
//Open codec
if(avcodec_open(pCodecCtx,pCodec)<0)
return -1;//Could not open codec
你们当中有些人可能还记得,旧教程,有两个其他部分的代码:加入CODEC_FLAG_TRUNCATED到pCodecCtx ->flags和增添纠正不正确帧速率的介绍。这两个补丁已不在ffplay.c了,所以我要当他们是没有必要了。还有一个差异指出,因为我们删除该代码:pCodecCtx -> time_base现在拥有帧速率信息。 time_base是一个结构具有分子和分母(AVRational)。我们代表的是分数的帧速率,因为许多编解码器已非整数的帧速率(比如NTSC制式的29.97fps)。
存储数据
现在,我们需要一个地方实际地存储帧:
AVFrame *pFrame;
// Allocate video frame
pFrame = avcodec_alloc_frame();
由于我们计划输出PPM文件,这是24位RGB存储的,我们将不得不将我们的帧从原始格式转换为RGB格式。 ffmpeg的会为我们做这些转换。对于大多数项目(包括我们)我们将要转换我们的初始帧,到特定的格式。现在,让我们分配一个帧转换:
//Allocate an AVFrame structure
pFrameRGB = avcodec_alloc_frarme();
if(pFrameRGB == NULL)
return -1;
即使我们为帧分配了空间,当我们转换时仍然需要一个空间存放原始数据。我们用avpicture_qet_size来获得我们需要的大小,并手动分配空间:
uint8_t *buffer;
int numBytes;
//Determine required buffer size and allocate buffer
numBytes = avpicture_qet_size(PIX_FMT_RGB24,pCodecCtx->width,pCodecCtx->height);
buffer = (uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_malloc是ffmpeg的malloc,它只是对malloc进行了简单的包装以确保内存地址对齐。而它并不能保证内存泄露、双字节释放(double freeing)或其他malloc问题。
现在我们用avpicture_fill将帧与我们刚分配的内存联系起来。关于AVPicture:该AVPicture结构是AVFrame结构的一个子集——在AVFrame结构开始的地方与AVPicture结构相同。
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame,but AVFrame is a superset of AVPicture
avpicture_fill((AVPicture*)pFrameRGB,buffer,PIX_FMT_RGB24,pCodeCtx->width,pCodecCtx->height);
现在我们已经为读入流数据做好了准备。
读数据
我们将要做的是从整个视频流中读取数据包、解码成我们的帧,并且一旦一帧完成我们就转换并存储它。
int frameFinished;
AVPacket packet;
i=0;
while(av_read_frame(pFormatCtx,&packet)>=0)
{
// Is this a packet from the video stream?
if(packet.stream_index == videoStream)
{
//Decode video frame
avcodec_decode_video(pCodecCtx,pFrame,&frameFinished,packet.data,packet.size
}
//did we get a video frame?
if(frameFinished)
{
//Convert the image from its native format to RGB
imq_convert((AVPicture *)pFrameRGB,PIX_FMT_RGB24,(AVPicture*)pFrame,
pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height);
//Save the frame to disk
if(++I <= 5)
SaveFrame(pFrameRGB,pCodecCtx->width,pCodecCtx->height,i);
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
这个过程,我们再来走一遍,非常简单:av_read_fraem()读取数据包并把它存在AVPacket结构中。需要指出的是,我们仅需分配数据包的结构——ffmpeg为我们分配了由packet.data指针指向的内部数据,后面将会被av_free_packet()释放。avcodec_decode_video()将会为我们将数据包转换为帧。然而,我们也许没有我们所需要的解码数据包成帧后的所有信息,所以avcodec_decode_video()在下一个帧到来时设置frameFinished。最后,我们利用ing_convert()将原始帧(pCodecCtx->pix_fmt)转化为RGB。记住,你可以投AVFrame指针一AVPicture指针。最后,我们传递帧的高度和宽度给SaveFrame函数。
现在我们需要做的是让SaveFrame函数以PPM格式写入文件中RGB信息。
void SaveFrame(AVFrame *pFrame,int width,int height,int iFrame)
{
FILE *pFile;
char szFilename[32];
int y;
// Open file
sprintf(szFilename,”frame%d.ppm”,iFrame);
pFile = fopen(szFilename,”wb”);
if(pFile == NULL)
return;
// Write header
fprintf(pFile,”P6/n%d %d/n255/n”,width,height);
//Write pixel data
for(y = 0;y < height;y++)
fwrite(pFrame->data[0] + y*pFrame->linesize[0],1,width*3,pFile);
//Close file
fclose(pFile);
}
我们做一个标准的文件打开,然后写入RGB数据。我们写一次读取一行。一个PPM的文件只是一个含有长字符串的RGB信息的文件。如果你了解HTML的颜色,它会像铺设出每个像素首尾相连地如#000000#000000 ....将是一个红色的屏幕。 (这是存储在二进制,没有分离)头信息表明图像的宽和高,以及RGB值最大尺寸。
现在,我们回到主函数。一旦我们从视频流中读取完成,我们要清扫一切:
// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);
// Free the YUV frame
av_free(pFrame);
// Close the codec
avcodec_close(pCodecCtx);
//Close the video file
av_close_input_file(pFormatCtx);
return 0;
注:关于数据包的说明:技术上说,一个数据包可以包含部分帧或者其他数据位,但是FFMPEG的解析器保证我们得到的数据包要么是完整的要么的具有多帧的。