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

