怎样以少于1000行代码写一个视频播放器-将电影文件中的视频帧存储为BMP图片

时间:2023-01-07 12:02:29

怎样以少于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文件进行测试,结果证明,程序运行正确。