[SimplePlayer] 1. 从视频文件中提取图像

在开始之前,我们需要了解视频文件的格式。视频文件的格式众多,无法三言两语就能详细分析其结构,尽管如此,ffmpeg却很好地提取了各类视频文件的共同特性,并对其进行了抽象描述。

image

视频文件格式,统称为container。它包含一个描述视频信息的头部,以及内含实际的音视频编码数据的packets。当然,这里的头部以及packet部分只是个抽象描述,实际的视频格式的描述信息可能不是存放在视频文件的起始位置,可能是由分散于视频文件的各个位置的多个部分组成;数据包有可能是由头部以及尾部进行分割的传统数据包形式,也有可能是一大块数据区域,由索引进行各个数据包的分割。

视频文件中的packets最主要的就是视频以及音频packets,demux的过程就是解析container的header来获取视频信息,所得到的视频信息能帮助我们区分packet是音频或者视频。同样属性的packets会被称为stream。

packet中存储的数据就是音视频编码后的数据,通过解码器进行decode后就能得到视频图像或者音频帧。其中需要注意的一点是,一个packet不一定对应一帧,packet的顺序也不一定是实际的播放顺序,而通过ffmpeg解码出来的frame的顺序就是实际的播放顺序。

 

Demux

首先需要一个用于存储视频文件信息的结构体。

pFormatCtx = avformat_alloc_context();

 

读取视频文件,并对该文件进行demux,所得到的视频信息存储于刚刚所构建的结构体当中

    if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0){
        fprintf(stderr, "open input failed\n");
        return -1;
    }

如果pFormatCtx=NULL,那么avformat_open_input也能自动为pFormatCtx分配存储空间。

 

对于有些视频格式,单单通过demux并不能获得所有的视频信息,为了获得这些信息,还需要读取并尝试解码该视频几个最前端packets(通常会解码每个stream第一个packet)。所读取的这几个packets会被缓存以供后续处理。

if(avformat_find_stream_info(pFormatCtx, NULL)<0){
        fprintf(stderr, "find stream info failed\n");
        return -1;
    }

 

从所获得的信息当中得到video stream序号,后续可以通过stream序号来对packet进行筛选。

videoStream = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);

 

 

Decode

创建一个用于存储以及维护解码信息结构体。

pCodecCtx = avcodec_alloc_context3(NULL);

 

把demux时所获得的视频相关信息传递到解码结构体中。

if(avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoStream]->codecpar)<0){
        fprintf(stderr, "copy param from format context to codec context failed\n");
        return -1;
    }

 

根据解码器id来寻找对应的解码器

pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if(pCodec==NULL){
        fprintf(stderr, "Unsupported codec,codec id %d\n", pCodecCtx->codec_id);
        return -1;
    }else{
        fprintf(stdout, "codec id is %d\n", pCodecCtx->codec_id);
    }

 

打开该解码器,主要目的是对解码器进行初始化

    if(avcodec_open2(pCodecCtx, pCodec, NULL)<0){
        fprintf(stderr, "open codec failed\n");
        return -1;
    }

 

创建一个用于维护所读取的packet的结构体,一个用于维护解码所得的frame的结构体

    pPacket = av_packet_alloc();
    pFrame = av_frame_alloc();
    if(pFrame == NULL||pPacket == NULL){
        fprintf(stderr, "cannot get buffer of frame or packet\n");
        return -1;
    }

 

从视频文件中读取packet,如果所读取的packet是video,则进行解码,解码所得的帧由pFrame进行维护。当然,并不是每次调用avcodec_decode_video2都会返回一帧,因为也可能会有需要多个packet才能解码出一帧的情况,因此只有当指示一帧是否解码完成的frameFinished为1才能对这一帧进行后续处理。

    while(av_read_frame(pFormatCtx, pPacket)>=0){
        //Only deal with the video stream of the type "videoStream"
        if(pPacket->stream_index==videoStream){
            //Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, pPacket);
            //fprintf(stdout, "Frame : %d ,pts=%lld, timebase=%lf\n", i, pFrame->pts, av_q2d(pFormatCtx->streams[videoStream]->time_base));
            if(frameFinished){
                if(i>=START_FRAME && i<=END_FRAME){
                    SaveFrame2YUV(pFrame, pCodecCtx->width, pCodecCtx->height, i);
                    i++;
                }else{
                    i++;
                    continue;
                }
            }
        }
        av_packet_unref(pPacket);
    }

当一个packet被解码后就可以调用av_packet_unref来释放该packet所占用的空间了。

 

 

Store

视频文件解码出来后通常都是YUV格式,Y、U、V三路分量分别存储在AVFrame的data[0]、data[1]、data[2]所指向的内存区域。linesize[0]、linesize[1]、linesize[2]分别指示了Y、U、V一行所占用的字节数。下面把解码所得的帧保存为YUV Planar格式。

void SaveFrame2YUV(AVFrame *pFrame, int width, int height, int iFrame){
    static FILE *pFile;
    char szFilename[32];
    int y;

    //Open file
    if(iFrame==START_FRAME){
         sprintf(szFilename, "Video.yuv");
        pFile = fopen(szFilename, "wb");
        if(pFile==NULL)
            return;
    }

    //Write YUV Data, Only support YUV420
    //Y
    for(y=0; y<height; y++){
        fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, pFrame->linesize[0], pFile);
    }
    //U
    for(y=0; y<(height+1)/2; y++){
        fwrite(pFrame->data[1]+y*pFrame->linesize[1], 1, pFrame->linesize[1], pFile);
    }
    //V
    for(y=0; y<(height+1)/2; y++){
        fwrite(pFrame->data[2]+y*pFrame->linesize[2], 1, pFrame->linesize[2], pFile);
    }

    //Close FIle
    if(iFrame==END_FRAME){
        fclose(pFile);
    }
}

 

 

最后就是释放内存,关闭decoder,关闭demuxer

    av_free(pPacket);
    av_free(pFrame);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);
posted @ 2018-09-07 12:20  TaigaComplex  阅读(2027)  评论(0编辑  收藏  举报