在開始之前,我們需要了解視頻文件的格式。視頻文件的格式眾多,無法三言兩語就能詳細分析其結構,盡管如此,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 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);