ffmpeg和SDL指南1---抓屏

概括

电影文件有几个基本组成部分:容器、流、包、帧。首先,文件本身被称为一个容器,容器的类型确定文件中的信息,容器有AVI、Quicktime等。其次,容器中通常有几个流,例如音频流和视频流(随着时间的推移生成的一连串数据元素形象的称为“流”)。流中的数据元素叫做帧。每个流有不同种类的编解码器编码。编解码器定义真实的数据如何被编码(COded)及被解码(DECoded)-因此编解码器简称为CODEC。编解码器有DivX、MP3等。接着帧进一步形成包(packet)。包是一组数据,该组数据可以解码成应用程序方便操作的原始帧数据。针对我们的讨论,每个包包含一个帧,或者包含多个帧(音频包)。
基本来说,处理音视频流非常简单:
10 从video.avi文件中打开video_stream
20 从video_stream中读取packet,然后解码成帧
30 如果帧不完整,跳到20
40 对该帧进行一些处理
50 跳到20
使用ffmpeg处理多媒体就像上面的几步一样简单,尽管一些程序可能在帧的处理上比较复杂。因此在该指南中,我们将追寻上面几步,打开一个文件,读取其中的视频流,将视频流中的帧保存到一个PPM文件。

打开文件

首先让我们看看如何打开一个文件。使用ffmpeg,你首先需要初始化ffmpeg库。(注意某些系统需要用<ffmpeg/avcodec.h>和<ffmpeg/avformat.h>替代)

#include <avcodec.h>
#include <avformat.h>
...
int main(int argc, char *argv[]) {
    av_register_all();


上面的代码将注册ffmpeg库中所有文件格式的mux、demux及编解码器,这样当一个文件被打开时,相应的format/codec被自动的使用。请注意,你只需要调用av_register_all()一次,所以我们在main()中调用。如果你喜欢,也可以只注册特定的formats和codecs,但通常没理由这么做。

现在我们实际打开一个文件:

AVFormatContext *pFormatCtx;

/*Open the file*/
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL) != 0)
    return -1;/*Couldn't open file*/


我们从第一个参数中获得文件名。该函数读文件头,然后在AVFormatContext结构中存储文件格式的相关信息。后三个参数用来指定文件格式、缓冲区大小、格式选项,但是若设置为NULL或0,libavformat将自动检测这些。

该函数(av_open_input_file)仅检测了文件的头部,因此接着我们需要检测文件的流信息:

/* Retrieve stream information */
if(av_find_stream_info(pFormatCtx) < 0)
    return -1;/* Couldn't find stream information */

该(av_find_stream_info())函数用合适的信息填充pFormatCtx->streams。我们使用一个手工调试函数来查看pFormatCtx->streams到底含有什么信息:

/* Dump information about file onto standard error */
dump_format(pFormatCtx, 0, argv[1], 0);


pFormatCtx->streams是一个大小为pFormatCtx->nb_streams的指针数组,我们遍历该指针数组直到找到一个视频流。

int i;
AVCodecContext *pCodecCtx;

/* Find the first video stream */
videoStream = -1;
for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[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(编解码器的上下文)”中。该编解码器上下文包含了流使用的编解码器的所有信息,现在我们有了一个指向它的指针。但是我们仍旧需要找到ffmpeg库中对应的编解码器并且打开它: 

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_open(pCodecCtx, pCodec) < 0)
    return -1;/* Could not open codec */


有一部分人可能记得旧的指南中还有两部分其它的代码:对pCodecCtx->flags增加CODEC_FLAG_TRUNCATED,增加对极不正确帧率的矫正。现在ffplay.c中不在出现这两部分,因此我也假设不再需要这两部分。删掉这两部分后还需要指出的一个不同点是:现在pCodecCtx->time_base保存的是帧率信息。time_base是一个含有一个分子成员和一个分母成员的结构(AVRational)。我们用一个分数描述帧率是因为许多编解码器的帧率都不是整数(例如NTSC为29.97fps)。

存储数据

现在我们需要一个实际存储帧的空间:

AVFrame *pFrame;

/* Allocate video frame */
pFrame = avcodec_alloc_frame();

因为我们计划输出保存24位RGB色的PPM文件,我们需要将帧从原来的格式转换为RGB。ffmpeg将为我们做这些转换。在大部分的项目中(包括我们的项目),我们都想把原始的帧转换为某种特定的格式。现在为了转换帧,我们申请另一个帧空间:

/* Allocate an AVFrame structure */
pFrameRGB = avcodec_alloc_frame();
if(pFrameRGB == NULL)
    return -1;


即使我们已经申请了帧空间,但当我们转换时我们仍然需要一个空间来存储原始的数据。我们使用avpicture_get_size来获得需要的空间大小,然后手工申请该内存空间:

uint8_t *buffer;
int numBytes;

/* Determine required buffer size and allocate buffer */
numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height);
buffer = (uint8_t *)av_alloc(numBytes * sizeof(uint8_t));


av_malloc是ffmpeg的malloc,他仅仅是malloc函数的简单封装,用来保证内存地址对齐等。该函数并不能阻止内存泄露、多次释放或者其它的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, pCodecCtx->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 fromat to RGB */
            img_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_frame()读取包数据并存储在AVPacket结构中。注意我们仅申请了包结构-ffmpeg为相应的内部数据申请内存并由packet.data指向它。这些内存由后面的av_free_packet()释放。avcodec_decode_video()将包转换为帧。但是,解码一个包后我们可能并没有得到一个帧的所有信息,因此,avcodec_decode_video通过设置frameFinished标志来指示是否得到一个完整的帧。最后,我们使用img_convert()将图片从原始的格式转换为RGB格式。请记住你可以将AVFrame指针转换为AVPicture指针。最后,我们将帧和宽高信息传递给SaveFrame函数。

现在我们需要做的是通过SaveFrame函数将RGB信息写入到一个PPM格式的文件中。我们使用一个粗略的PPM文件格式,但请相信我,它可以工作。
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 -1;

    /* Write header */
    fprintf(pFile, "P6\n%d %d\n255\n", width, height);

    /* Write pixel data */
    for(y=0; i<height; y++)
        fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);

    /* Close file */
    fclose(pFile);
}


我们做了一些标准的文件打开动作,等等,随后写RGB数据。我们一次向文件写入一行数据。RGB文件是一种包含一长串RGB信息的文件。如果你了解HTML颜色的表示方法,那么它就类似于把每个像素的颜色点到点的展开,像#ff0000#ff0000...将表示红色的屏幕。 (它被保存为二进制文件且没有分隔符,但是你自己知道什么地方应该分隔开)文件头指示图片的宽、高和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;


你可以看到我们使用av_free释放我们使用avcodec_alloc_frame和av_malloc申请的内存。

猜你喜欢

转载自blog.csdn.net/gutsyfarmer/article/details/6915026