在开始之前,我们需要了解视频文件的格式。视频文件的格式众多,无法三言两语就能详细分析其结构,尽管如此,ffmpeg却很好地提取了各类视频文件的共同特性,并对其进行了抽象描述。
视频文件格式,统称为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 frameavcodec_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 fileif(iFrame==START_FRAME){sprintf(szFilename, "Video.yuv");pFile = fopen(szFilename, "wb");if(pFile==NULL)return;}//Write YUV Data, Only support YUV420//Yfor(y=0; y<height; y++){fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, pFrame->linesize[0], pFile);}//Ufor(y=0; y<(height+1)/2; y++){fwrite(pFrame->data[1]+y*pFrame->linesize[1], 1, pFrame->linesize[1], pFile);}//Vfor(y=0; y<(height+1)/2; y++){fwrite(pFrame->data[2]+y*pFrame->linesize[2], 1, pFrame->linesize[2], pFile);}//Close FIleif(iFrame==END_FRAME){fclose(pFile);}
}
最后就是释放内存,关闭decoder,关闭demuxer
av_free(pPacket);av_free(pFrame);avcodec_close(pCodecCtx);avformat_close_input(&pFormatCtx);