概要
电影文件有很多基本的组成部分。首先,文件本身被称为容器Container,容器的类型决定了信息被存放在文件中的位置。AVI和Quicktime就 是容器的例子。接着,你有一组流,例如,你经常有的是一个音频流和一个视频流。(一个流只是一种想像出来的词语,用来表示一连串的通过时间来串连的数据元 素)。在流中的数据元素被称为帧Frame。每个流是由不同的编码 器来编码生成的。编解码器 描 述了实际的数据是如何被编码Coded和解码DECoded的,因此它的名字叫做CODEC。Divx和 MP3就是编解码器的例子。接着从流中被读出来的叫做包Packets。包是一段数据,它包含了一段可以被解码成方便我们最后在应用程序中操作的原始帧的 数据。根据我们的目的,每个包包含了完整的帧或者对于音频来说是许多格式的完整帧。
基本上来说,处理视频和音频流是很容易的:
10 从video.avi文件中打开视频流video_stream
20 从视频流中读取包到帧中
30 如果这个帧还不完整,跳到20
40 对这个帧进行一些操作
50 跳回到20
在这个程序中使用ffmpeg来处理多种媒体是相当容易的,虽然很多程序可能在对帧进行操作的时候非常的复杂。因此在这篇指导中,我们将打开一个文件,读取里面的视频流,而且我们对帧的操作将是把这个帧写到一个PPM文件中。
打开文件
首先,来看一下我们如何打开一个文件。通过ffmpeg,你必需先初始化这个库。(注意在某些系统中必需用<ffmpeg/avcodec.h>和<ffmpeg/avformat.h>来替换)
1 AVFormatContext *pFormatCtx; 2 // Open video file 3 if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0) 4 return -1; // Couldn't open file
这里注册了所有的文件格式和编解码器的库,所以它们将被自动的使用在被打开的合适格式的文件上。注意你只需要调用 av_register_all()一次,因此我们在主函数main()中来调用它。如果你喜欢,也可以只注册特定的格式和编解码器,但是通常你没有必要 这样做。
现在我们可以真正的打开文件:
1 AVFormatContext *pFormatCtx; 2 // Open video file 3 if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0) 4 return -1; // Couldn't open file
我们通过第一个参数来获得文件名。这个函数读取文件的头部并且把信息保存到我们给的AVFormatContext结构体中。最后三个参数用来指定特殊的文件格式,缓冲大小和格式参数,但如果把它们设置为空NULL或者0,libavformat将自动检测这些参数。
这个函数只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
1 // Dump information about file onto standard error 2 dump_format(pFormatCtx, 0, argv[1], 0);
这个函数为pFormatCtx->streams填充上正确的信息。我们引进一个手工调试的函数来看一下里面有什么:
1 // Dump information about file onto standard error 2 dump_format(pFormatCtx, 0, argv[1], 0);
现在pFormatCtx->streams仅仅是一组大小为pFormatCtx->nb_streams的指针,所以让我们先跳过它直到我们找到一个视频流。
1 int i; 2 AVCodecContext *pCodecCtx; 3 // Find the first video stream 4 videoStream=-1; 5 for(i=0; i<pFormatCtx->nb_streams; i++) 6 if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) { 7 videoStream=i; 8 break; 9 } 10 if(videoStream==-1) 11 return -1; // Didn't find a video stream 12 // Get a pointer to the codec context for the video stream 13 pCodecCtx=pFormatCtx->streams[videoStream]->codec;
流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文)的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一个指向他的指针。但是我们必需要找到真正的编解码器并且打开它:
1 AVCodec *pCodec; 2 // Find the decoder for the video stream 3 pCodec=avcodec_find_decoder(pCodecCtx->codec_id); 4 if(pCodec==NULL) { 5 fprintf(stderr, "Unsupported codec! "); 6 return -1; // Codec not found 7 } 8 // Open codec 9 if(avcodec_open(pCodecCtx, pCodec)<0) 10 return -1; // Could not open codec
有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加CODEC_FLAG_TRUNCATED到 pCodecCtx->flags和添加一个hack来粗糙的修正帧率。这两个修正已经不在存在于ffplay.c中。因此,我必需假设它们不再必 要。我们移除了那些代码后还有一个需要指出的不同点:pCodecCtx->time_base现在已经保存了帧率的信息。time_base是一 个结构体,它里面有一个分子和分母 (AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数的帧率(例如NTSC使用29.97fps)。
保存数据
现在我们需要找到一个地方来保存帧:
1 // Allocate an AVFrame structure 2 pFrameRGB=avcodec_alloc_frame(); 3 if(pFrameRGB==NULL) 4 return -1;
因为我们准备输出保存24位RGB色的PPM文件,我们必需把帧的格式从原来的转换为RGB。FFMPEG将为我们做这些转换。在大多数项目中(包括我们的这个)我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内存。
1 // Allocate an AVFrame structure 2 pFrameRGB=avcodec_alloc_frame(); 3 if(pFrameRGB==NULL) 4 return -1;
即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始的数据。我们使用avpicture_get_size来获得我们需要的大小,然后手工申请内存空间:
1 uint8_t *buffer; 2 int numBytes; 3 // Determine required buffer size and allocate buffer 4 numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width, 5 pCodecCtx->height); 6 buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_malloc是ffmpeg的malloc,用来实现一个简单的malloc的包装,这样来保证内存地址是对齐的(4字节对齐或者2字节对齐)。它并不能保护你不被内存泄漏,重复释放或者其它malloc的问题所困扰。
现在我们使用avpicture_fill来把帧和我们新申请的内存来结合。关于AVPicture的结成:AVPicture结构体是AVFrame结构体的子集――AVFrame结构体的开始部分与AVPicture结构体是一样的。
1 // Assign appropriate parts of buffer to image planes in pFrameRGB 2 // Note that pFrameRGB is an AVFrame, but AVFrame is a superset 3 // of AVPicture 4 avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24, 5 pCodecCtx->width, pCodecCtx->height);
最后,我们已经准备好来从流中读取数据了。
读取数据
我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最好后转换格式并且保存。
1 int frameFinished; 2 AVPacket packet; 3 i=0; 4 while(av_read_frame(pFormatCtx, &packet)>=0) { 5 // Is this a packet from the video stream? 6 if(packet.stream_index==videoStream) { 7 // Decode video frame 8 avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, 9 packet.data, packet.size); 10 // Did we get a video frame? 11 if(frameFinished) { 12 // Convert the image from its native format to RGB 13 img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, 14 (AVPicture*)pFrame, pCodecCtx->pix_fmt, 15 pCodecCtx->width, pCodecCtx->height); 16 // Save the frame to disk 17 if(++i<=5) 18 SaveFrame(pFrameRGB, pCodecCtx->width, 19 pCodecCtx->height, i); 20 } 21 } 22 // Free the packet that was allocated by av_read_frame 23 av_free_packet(&packet); 24 }
这个循环过程是比较简单的: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函数。