1. 程式人生 > >使用ffmpeg介面將h.264解碼為YUV

使用ffmpeg介面將h.264解碼為YUV

引數傳遞和解析

同編碼器類似,解碼器也需要傳遞引數。不過相比編碼器,解碼器在執行時所需要的大部分資訊都包含在輸入碼流中,因此輸入引數一般只需要指定一個待解碼的視訊碼流檔案即可

按照要求初始化需要的FFMpeg結構

第一步: 獲取解碼器指標。根據編解碼器ID,獲取編解碼器指標

第二步: 獲取解碼器上下文。AVCodecContext例項。通過編解碼器AVCodec指標,獲取編解碼器上下文

第三步: 設定 AVCodecContext可以截斷方式讀取資料

if (ctx.pCodec->capabilities & AV_CODEC_CAP_TRUNCATED)
ctx.pCodecContext->flags |= AV_CODEC_FLAG_TRUNCATED; // we do not send complete frames

第四步: 開啟解碼器,不需要像編碼一樣設定 AVCodecContext引數。

//開啟AVCodec物件
if (avcodec_open2(ctx.pCodecContext, ctx.pCodec, NULL) < 0)
{
fprintf(stderr, “Could not open codec\n”);
return false;
}

第五步: 獲取 AVCodecParserContext結構,解析h.264 NAL,生成可供解碼器解碼的AVPacket

我們應該記得,在FFMpeg視訊編碼的實現中,AVCodecContext物件分配完成後,下一步實在該物件中設定編碼的引數。而在解碼器的實現中,基本不需要額外設定引數資訊,因此這個物件更多地作為輸出引數接收資料。因此物件分配完成後,不需要進一步的初始化操作。

解碼器與編碼器實現中不同的一點在於,解碼器的實現中需要額外的一個AVCodecParserContext結構,用於從碼流中擷取一個完整的NAL單元。因此我們需要分配一個AVCodecParserContext型別的物件,使用函式av_parser_init,宣告為:

AVCodecParserContext *av_parser_init(int codec_id);

呼叫方式為:

ctx.pCodecParserCtx = av_parser_init(AV_CODEC_ID_H264);
if (!ctx.pCodecParserCtx)
{
printf(“Could not allocate video parser context\n”);
return false;
}

第六步: 分配 AVFrame物件

//分配AVFrame物件
ctx.frame = av_frame_alloc();
if (!ctx.frame)
{
fprintf(stderr, “Could not allocate video frame\n”);
return false;
}

解碼迴圈體

第一步: 根據碼流記憶體資料,解析出一個完整的H.264包(AVPacket)

完成必須的codec元件的建立和初始化之後,開始進入正式的解碼迴圈過程。解碼迴圈通常按照以下幾個步驟實現:

首先按照某個指定的長度讀取一段碼流儲存到快取區中。

由於H.264中一個包的長度是不定的,我們讀取一段固定長度的碼流通常不可能剛好讀出一個包的長度。所以我們就需要使用AVCodecParserContext結構對我們讀出的碼流資訊進行解析,直到取出一個完整的H.264包。對碼流解析的函式為av_parser_parse2,宣告方式如:

int av_parser_parse2(AVCodecParserContext *s,
AVCodecContext *avctx,
uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size,
int64_t pts, int64_t dts,
int64_t pos);

這個函式的各個引數的意義:

AVCodecParserContext *s:初始化過的AVCodecParserContext物件,決定了碼流該以怎樣的標準進行解析;
AVCodecContext *avctx:預先定義好的AVCodecContext物件;
uint8_t **poutbuf:AVPacket::data的地址,儲存解析完成的包資料;
int *poutbuf_size:AVPacket的實際資料長度;如果沒解析出完整的一個包,這個值為0;
const uint8_t *buf, int buf_size:輸入引數,快取的地址和長度;
int64_t pts, int64_t dts:顯示和解碼的時間戳;
nt64_t pos :碼流中的位置;
返回值為解析所使用的位元位的長度;

具體的呼叫方式為:

len = av_parser_parse2(ctx.pCodecParserCtx, ctx.pCodecContext,
&(ctx.pkt.data), &(ctx.pkt.size),
pDataPtr, uDataSize,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);

如果引數poutbuf_size的值為0,那麼應繼續解析快取中剩餘的碼流;如果快取中的資料全部解析後依然未能找到一個完整的包,那麼繼續從輸入檔案中讀取資料到快取,繼續解析操作,直到pkt.size不為0為止。

第二步: 解碼

第三步: 將解碼之後的 AVFrame寫入檔案

void write_out_yuv_frame(const CodecCtx &ctx, IOParam &in_out)
{
uint8_t ***pBuf = ctx.frame->data;
int** pStride = ctx.frame->linesize;

for (int color_idx = 0; color_idx < 3; color_idx++)
{
int nWidth = color_idx = 0 ? ctx.frame->width : ctx.frame->width / 2;
int nHeight = color_idx =
0 ? ctx.frame->height : ctx.frame->height / 2;
for(int idx=0;idx < nHeight; idx++)
{
fwrite(pBuf[color_idx],1, nWidth, in_out.pFout);
pBuf[color_idx] += pStride[color_idx]; //可能有空白資料填充,要跳過
}
fflush(in_out.pFout);
}
}

收尾: 解碼解碼器中快取的資料

最後,同編碼器一樣,解碼過程的最後一幀可能也存在延遲。處理最後這一幀的方法也跟解碼器類似:將AVPacket::data設為NULL,AVPacket::size設為0,然後在呼叫avcodec_encode_video2完成最後的解碼過程:

ctx.pkt.data = NULL;
ctx.pkt.size = 0;
while(1)
{
//將編碼器中剩餘的資料繼續輸出完
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf(“Decode Error.\n”);
return ret;
}

if (got\_picture)  
{  
    write\_out\_yuv\_frame(ctx, inputoutput);  
    printf("Flush Decoder: Succeed to decode 1 frame!\n");  
}  
else  
{  
    break;  
}  

} //while(1)

收尾工作

收尾工作主要包括關閉輸入輸出檔案、關閉FFMpeg解碼器各個元件。其中關閉解碼器元件需要:

avcodec_close(ctx.pCodecContext);
av_free(ctx.pCodecContext);
av_frame_free(&(ctx.frame));