怎样以少于1000行代码写一个视频播放器-将电影文件中的视频帧存储为BMP图片
源代码:tutorial01.cpp
编译好的libav库:libav.zip
1 概述
电影文件都包含一些基本组件。首先,电影文件本身称之为一个容器(container),容器的类型决定了文件中信息的组织方式。例如:AVI和Quicktime就是容器类型。其次,电影文件由一个流(streams)集合组成,例如:电影文件通常都包含一个音频流和一个视频流(流是指一个数据元素序列,其数据元素随着时间推移而变得可用。)。流中的数据元素称之为帧(frames)。每个流都被一个不同种类的编解码器(codec)所编码。编解码器(codec)定义了实际的数据是如何被编码(COded)和解码(DECoded)的,因此得名为CODEC。例如DivX和MP3就是编解码器(codecs)。从流中读取得到包(packets)。包由数据片段组成,我们可以将之解码成应用程序可以直接操作的原始帧。出于教学的目的,本教程中的采用的电影文件,其视频流中的每个包都包含一个完整的帧(译者注:H264中的每个包就不包含一个完整的帧),音频流中的每个包则可包含多个帧。
从基本层面上来讲,处理视频流和音频流的步骤非常简单:
1 从video.avi中打开视频流(video_stream)
2 从视频流中将包(packet)读取到帧(frame)中
3 如果帧不完整,则转到第二步
4 对帧进行处理
5 转到第二步
尽管有一些应用程序会对帧进行复杂的处理,但是使用libav处理多媒体就如上面的流程那么简单。因此在本教程中,我们首先打开一个电影文件,然后从中读取视频流,最后再将每一帧输出成一个BMP文件。
2 打开电影文件
首先,让我们先看看如何打开一个电影文件。使用libav,你首先需要初始化libav库。(本教程假定你创建的是一个C++项目,由于libav是C库,因此需要添加extern "C")。
extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> } ... int main(int argc, charg *argv[]) { av_register_all();上面的代码将会注册libav库中所有可用的文件格式和编解码器(codecs),这样当你打开一个具有对应文件格式/编解码器的文件时,libav就会自动采用该文件格式和编解码器。注意你只需要调用一次av_register_all(),因此我们在main()中调用它。如果你喜欢,你也可以只注册特定的文件格式和编解码器,但是通常没有必要这样做。
现在我们可以打开文件了:
AVFormatContext *pFormatCtx = NULL; // Open video file if (avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0) return -1; // Couldn't open file我们从第一个参数中得到文件名。这个函数读取文件头并将该文件格式的信息存储在作为参数的AVFormatContext结构体中。后两个参数用于指定文件格式和格式选项,如果将之设置为NULL,libavformat就会自动检测这些信息。
这个函数仅仅解析文件头,因此接下来我们需要从文件中取出流信息:
// Retrieve stream information if (avformat_find_stream_info(pFormatCtx, NULL) < 0) return -1; // Couldn't find stream information该函数使用适当的信息填充 pFormatCtx->streams 。接下来我们使用一个方便的调试函数显示pFormatCtx中包含了一些什么信息。
// Dump information about file onto standard error av_dump_format(pFormatCtx, 0, argv[1], 0);现在pFormatCtx->streams只是一个大小为pFormatCtx->nb_streams的指针数组,因此我们遍历它直到找到一个视频流。
AVCodecContext *pCodecCtx; // Find the first video stream int videoStream = -1; for (int 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; // 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, "Unsupported codec!\n"); return -1; // Codec not found } // Open codec if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) return -1; // Could not open codec
3 存储数据
现在我们需要一个实际存放帧的地方:
AVFrame *pFrame; // Allocate video frame pFrame = avcodec_alloc_frame();因为我们需要将每一帧输出成BMP图片,其中BMP图片以BGR24格式存储,因此我们需要将帧从原始格式转换到BGR24格式。libav可以帮助我们完成这个转换。对于大多数项目(包括本教程的项目)我们需要将原始帧转换到一个特定的格式。现在让我们分配空间来存储转换后的帧。
AVFrame *pFrameRGB; // Allocate an AVFrame structure pFrameRGB = avcodec_alloc_frame(); if (pFrameRGB == NULL) return -1;尽管我们已经分配好帧,但是在进行转换时我们仍然需要一个空间用于存放原始数据。我们使用 avpicture_get_size ()获取空间的大小,然后手动分配它:
// Determine required buffer size and allocate buffer int numBytes = avpicture_get_size(PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height); uint8_t *buffer = (uint8_t*)av_malloc_array(numBytes, sizeof(uint8_t));av_malloc_array()是libav的内存分配函数,它仅仅对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_BGR24, pCodecCtx->width, pCodecCtx->height);现在我们已经做好读取流的准备了。
4 读取数据
接下来需要做的就是重复如下流程:将视频流读入包中,将包解码成帧,一旦我们得到一个完整的帧,则将其转换并保存。
// Read frames and save first five frames to disk SwsContext *swsContextPtr = NULL; 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_video2(pCodecCtx, pFrame, &frameFinished, &packet); // Did we get a video frame? if(frameFinished) { // Convert the image from its native format to RGB swsContextPtr = sws_getCachedContext(swsContextPtr, pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL); if (NULL == swsContextPtr) { printf("Could not initialize the conversion context.\n"); return -1; } sws_scale(swsContextPtr, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); // 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_frame()读入一个包并将之存储在AVPacket结构体中。注意我们仅仅分配了AVPacket结构体-libav会为我们分配内部数据,其中packet.data指向libav分配的内部数据,这个内部数据最后由av_free_packet()释放。avcodec_decode_video2()将包解码成帧。解码一个包后,有可能并不能得到一个帧的所有信息,因此当得到一个完整的帧后,avcodec_decode_video2()会将frameFinished设置为1。然后我们使用sws_scale()将帧从原始格式转换成BGR24格式。最后,我们将转换后的帧以及宽和高传给SaveFrame()函数。
接下来需要做的就是就是将BGR24信息写入一个BMP格式的文件。
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) { FILE *pFile; char szFilename[32]; // Open file sprintf(szFilename, "frame%d.bmp", iFrame); pFile = fopen(szFilename, "wb"); if(pFile == NULL) return; int wBitCount = 24; // 表示由3个字节定义一个像素 int bmByteCount = pFrame->linesize[0] * height; // 像素数据的大小 // 位图信息头结构,定义参考MSDN BITMAPINFOHEADER bi; bi.biSize = sizeof(BITMAPINFOHEADER); bi.biWidth = width; bi.biHeight = height; bi.biPlanes = 1; bi.biBitCount = wBitCount; bi.biCompression= BI_RGB; bi.biSizeImage=0; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrImportant = 0; bi.biClrUsed = 0; // 位图文件头结构,定义参考MSDN BITMAPFILEHEADER bf; bf.bfType = 0x4D42; // BM bf.bfSize = bmByteCount; bf.bfReserved1 = 0; bf.bfReserved2 = 0; bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); fwrite(&bf, sizeof(BITMAPFILEHEADER), 1, pFile); fwrite(&bi, sizeof(BITMAPINFOHEADER), 1, pFile); for (int i = 0; i < height; ++ i) fwrite(pFrame->data[0] + (height - i - 1)*pFrame->linesize[0], 1, pFrame->linesize[0], pFile); // Close file fclose(pFile); }现在回到main()函数,一旦完成了读取视频流的工作,接下来就得释放资源了。
// 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 avformat_close_input(&pFormatCtx);本教程的 tutorial01.cpp已经在VS2010下编译通过,并且使用一个AVI文件进行测试,结果证明,程序运行正确。