呼叫ffmpeg SDK對YUV視訊序列進行編碼

NO IMAGE

1、FFMpeg進行視訊編碼所需要的結構:

為了實現呼叫FFMpeg的API實現視訊的編碼,以下結構是必不可少的:

AVCodec:AVCodec結構儲存了一個編解碼器的例項,實現實際的編碼功能。通常我們在程式中定義一個指向AVCodec結構的指標指向該例項。
AVCodecContext:AVCodecContext表示AVCodec所代表的上下文資訊,儲存了AVCodec所需要的一些引數。對於實現編碼功能,我們可以在這個結構中設定我們指定的編碼引數。通常也是定義一個指標指向AVCodecContext。
AVFrame:AVFrame結構儲存編碼之前的畫素資料,並作為編碼器的輸入資料。其在程式中也是一個指標的形式。
AVPacket:AVPacket表示碼流包結構,包含編碼之後的碼流資料。該結構可以不定義指標,以一個物件的形式定義。
在我們的程式中,我們將這些結構整合在了一個結構體中:

/***************************************
Struct: CodecCtx
Description: FFMpeg編解碼器上下文
***************************************/
typedef struct
{
AVCodec *codec; //指向編解碼器例項
AVFrame *frame; //儲存解碼之後/編碼之前的畫素資料
AVCodecContext *c; //編解碼器上下文,儲存編解碼器的一些引數設定
AVPacket pkt; //碼流包結構,包含編碼碼流資料
} CodecCtx;
2、FFMpeg編碼的主要步驟:

(1)、輸入編碼引數

這一步我們可以設定一個專門的配置檔案,並將引數按照某個事寫入這個配置檔案中,再在程式中解析這個配置檔案獲得編碼的引數。如果引數不多的話,我們可以直接使用命令列將編碼引數傳入即可。

(2)、按照要求初始化需要的FFMpeg結構

首先,所有涉及到編解碼的的功能,都必須要註冊音視訊編解碼器之後才能使用。註冊編解碼呼叫下面的函式:

avcodec_register_all();
編解碼器註冊完成之後,根據指定的CODEC_ID查詢指定的codec例項。CODEC_ID通常指定了編解碼器的格式,在這裡我們使用當前應用最為廣泛的H.264格式為例。查詢codec呼叫的函式為avcodec_find_encoder,其宣告格式為:

AVCodec *avcodec_find_encoder(enum AVCodecID id);
該函式的輸入引數為一個AVCodecID的列舉型別,返回值為一個指向AVCodec結構的指標,用於接收找到的編解碼器例項。如果沒有找到,那麼該函式會返回一個空指標。呼叫方法如下:

/* find the mpeg1 video encoder */
ctx.codec = avcodec_find_encoder(AV_CODEC_ID_H264); //根據CODEC_ID查詢編解碼器物件例項的指標
if (!ctx.codec)
{
fprintf(stderr, “Codec not found\n”);
return false;
}
AVCodec查詢成功後,下一步是分配AVCodecContext例項。分配AVCodecContext例項需要我們前面查詢到的AVCodec作為引數,呼叫的是avcodec_alloc_context3函式。其宣告方式為:

AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
其特點同avcodec_find_encoder類似,返回一個指向AVCodecContext例項的指標。如果分配失敗,會返回一個空指標。呼叫方式為:

ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext例項
if (!ctx.c)
{
fprintf(stderr, “Could not allocate video codec context\n”);
return false;
}
需注意,在分配成功之後,應將編碼的引數設定賦值給AVCodecContext的成員。

現在,AVCodec、AVCodecContext的指標都已經分配好,然後以這兩個物件的指標作為引數開啟編碼器物件。呼叫的函式為avcodec_open2,宣告方式為:

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
該函式的前兩個引數是我們剛剛建立的兩個物件,第三個引數為一個字典型別物件,用於儲存函式執行過程總未能識別的AVCodecContext和另外一些私有設定選項。函式的返回值表示編碼器是否開啟成功,若成功返回0,失敗返回一個負數。呼叫方式為:

if (avcodec_open2(ctx.c, ctx.codec, NULL) < 0) //根據編碼器上下文開啟編碼器
{
fprintf(stderr, “Could not open codec\n”);
exit(1);
}
然後,我們需要處理AVFrame物件。AVFrame表示視訊原始畫素資料的一個容器,處理該型別資料需要兩個步驟,其一是分配AVFrame物件,其二是分配實際的畫素資料的儲存空間。分配物件空間類似於new操作符一樣,只是需要呼叫函式av_frame_alloc。如果失敗,那麼函式返回一個空指標。AVFrame物件分配成功後,需要設定影象的解析度和畫素格式等。實際呼叫過程如下:

ctx.frame = av_frame_alloc(); //分配AVFrame物件
if (!ctx.frame)
{
fprintf(stderr, “Could not allocate video frame\n”);
return false;
}
ctx.frame->format = ctx.c->pix_fmt;
ctx.frame->width = ctx.c->width;
ctx.frame->height = ctx.c->height;
分配畫素的儲存空間需要呼叫av_image_alloc函式,其宣告方式為:

int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt, int align);
該函式的四個引數分別表示AVFrame結構中的快取指標、各個顏色分量的寬度、影象解析度(寬、高)、畫素格式和記憶體對其的大小。該函式會返回分配的記憶體的大小,如果失敗則返回一個負值。具體呼叫方式如:

ret = av_image_alloc(ctx.frame->data, ctx.frame->linesize, ctx.c->width, ctx.c->height, ctx.c->pix_fmt, 32);
if (ret < 0)
{
fprintf(stderr, “Could not allocate raw picture buffer\n”);
return false;
}
(3)、編碼迴圈體

到此為止,我們的準備工作已經大致完成,下面開始執行實際編碼的迴圈過程。用虛擬碼大致表示編碼的流程為:

while (numCoded < maxNumToCode)
{
read_yuv_data();
encode_video_frame();
write_out_h264();
}
其中,read_yuv_data部分直接使用fread語句讀取即可,只需要知道的是,三個顏色分量Y/U/V的地址分別為AVframe::data[0]、AVframe::data[1]和AVframe::data[2],影象的寬度分別為AVframe::linesize[0]、AVframe::linesize[1]和AVframe::linesize[2]。需要注意的是,linesize中的值通常指的是stride而不是width,也就是說,畫素儲存區可能是帶有一定寬度的無效邊區的,在讀取資料時需注意。

編碼前另外需要完成的操作時初始化AVPacket物件。該物件儲存了編碼之後的碼流資料。對其進行初始化的操作非常簡單,只需要呼叫av_init_packet並傳入AVPacket物件的指標。隨後將AVPacket::data設為NULL,AVPacket::size賦值0.

成功將原始的YUV畫素值儲存到了AVframe結構中之後,便可以呼叫avcodec_encode_video2函式進行實際的編碼操作。該函式可謂是整個工程的核心所在,其宣告方式為:

int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame, int *got_packet_ptr);
其引數和返回值的意義:

avctx: AVCodecContext結構,指定了編碼的一些引數;
avpkt: AVPacket物件的指標,用於儲存輸出碼流;
frame:AVframe結構,用於傳入原始的畫素資料;
got_packet_ptr:輸出引數,用於標識AVPacket中是否已經有了完整的一幀;
返回值:編碼是否成功。成功返回0,失敗則返回負的錯誤碼
通過輸出引數*got_packet_ptr,我們可以判斷是否應有一幀完整的碼流資料包輸出,如果是,那麼可以將AVpacket中的碼流資料輸出出來,其地址為AVPacket::data,大小為AVPacket::size。具體呼叫方式如下:

/* encode the image */
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //將AVFrame中的畫素資訊編碼為AVPacket中的碼流
if (ret < 0)
{
fprintf(stderr, “Error encoding frame\n”);
exit(1);
}

if (got_output)
{
//獲得一個完整的編碼幀
printf(“Write frame %3d (size=%5d)\n”, frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}
因此,一個完整的編碼迴圈提就可以使用下面的程式碼實現:

/* encode 1 second of video */
for (frameIdx = 0; frameIdx < io_param.nTotalFrames; frameIdx )
{
av_init_packet(&(ctx.pkt)); //初始化AVPacket例項
ctx.pkt.data = NULL; // packet data will be allocated by the encoder
ctx.pkt.size = 0;

fflush(stdout);
Read_yuv_data(ctx, io_param, 0);        //Y分量
Read_yuv_data(ctx, io_param, 1);        //U分量
Read_yuv_data(ctx, io_param, 2);        //V分量
ctx.frame->pts = frameIdx;
/* encode the image */
ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), ctx.frame, &got_output); //將AVFrame中的畫素資訊編碼為AVPacket中的碼流
if (ret < 0) 
{
fprintf(stderr, "Error encoding frame\n");
exit(1);
}
if (got_output) 
{
//獲得一個完整的編碼幀
printf("Write frame %3d (size=%5d)\n", frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}

} //for (frameIdx = 0; frameIdx < io_param.nTotalFrames; frameIdx )
(4)、收尾處理

如果我們就此結束編碼器的整個執行過程,我們會發現,編碼完成之後的碼流對比原來的資料少了一幀。這是因為我們是根據讀取原始畫素資料結束來判斷迴圈結束的,這樣最後一幀還保留在編碼器中尚未輸出。所以在關閉整個解碼過程之前,我們必須繼續執行編碼的操作,直到將最後一幀輸出為止。執行這項操作依然呼叫avcodec_encode_video2函式,只是表示AVFrame的引數設為NULL即可:

/* get the delayed frames */
for (got_output = 1; got_output; frameIdx )
{
fflush(stdout);

ret = avcodec_encode_video2(ctx.c, &(ctx.pkt), NULL, &got_output);      //輸出編碼器中剩餘的碼流
if (ret < 0)
{
fprintf(stderr, "Error encoding frame\n");
exit(1);
}
if (got_output) 
{
printf("Write frame %3d (size=%5d)\n", frameIdx, ctx.pkt.size);
fwrite(ctx.pkt.data, 1, ctx.pkt.size, io_param.pFout);
av_packet_unref(&(ctx.pkt));
}

} //for (got_output = 1; got_output; frameIdx )
此後,我們就可以按計劃關閉編碼器的各個元件,結束整個編碼的流程。編碼器元件的釋放流程可類比建立流程,需要關閉AVCocec、釋放AVCodecContext、釋放AVFrame中的影象快取和物件本身:

avcodec_close(ctx.c);
av_free(ctx.c);
av_freep(&(ctx.frame->data[0]));
av_frame_free(&(ctx.frame));
3、總結

使用FFMpeg進行視訊編碼的主要流程如:

首先解析、處理輸入引數,如編碼器的引數、影象的引數、輸入輸出檔案;
建立整個FFMpeg編碼器的各種元件工具,順序依次為:avcodec_register_all -> avcodec_find_encoder -> avcodec_alloc_context3 -> avcodec_open2 -> av_frame_alloc -> av_image_alloc;
編碼迴圈:av_init_packet -> avcodec_encode_video2(兩次) -> av_packet_unref
關閉編碼器元件:avcodec_close,av_free,av_freep,av_frame_free