1. 程式人生 > >ffmpeg:將YUV原始資料編碼封裝為mp4格式

ffmpeg:將YUV原始資料編碼封裝為mp4格式

        因為需要看了一些關於視訊編解碼相關的知識,並在學習過程中接觸到了ffmepg這個強大的視音訊處理工具,針對ffmpeg基礎庫進行了一個初步的學習,基本把其編解碼流程熟悉,這裡做一個總結。備以後複習用。本人使用的ffmpeg版本為3.1window版本的。

   一、ffmpeg庫包在window上配置安裝

         該網站中的FFMPEG分為3個版本:Static,Shared,Dev。介紹如下:

        前兩個版本可以直接在命令列中使用,他們的區別在於:

        Static裡面只有3個應用程式:ffmpeg.exe,ffplay.exe,ffprobe.exe,每個exe的體積都很大,相關的Dll已經被編譯到exe裡面去了。       

       Shared裡面除了3個應用程式:ffmpeg.exe,ffplay.exe,ffprobe.exe之外,還有一些Dll,比如說avcodec-54.dll之類的。Shared裡面的exe體積很小,他們在執行的時候,到相應的Dll中呼叫功能。

        Dev版本是用於開發的,裡面包含了include(標頭檔案xxx.h)和lib(庫檔案xxx.lib),這個版本不包含exe檔案。

        在vs使用和一般的庫包一樣只要把Dev版本下的include檔案新增到包含目錄,lib資料夾下的lib檔案新增到附加依賴性項中。不要忘記在環境變數中新增bin目錄。bin資料夾位於Shared版本

裡面。

二、視訊編碼封裝

       本文中實現的一個小功能是把一個YUV原始視訊資料(時間序列影象)經過h264編碼為視訊碼流,然後在使用mp4封裝格式封裝。流程圖如下:


   簡單敘述一下ffmpeg編碼流程:

          1、首先使用av_register_all()函式註冊所有的編碼器和複用器(理解為格式封裝器)。該步驟必須放在所有ffmpeg程式碼前第一個執行

          2、avformat_alloc_output_context2():初始化包含有輸出碼流(AVStream)和解複用器(AVInputFormat)的AVFormatContext

          3、avio_open( )開啟輸出檔案

          4、av_new_stream() 建立視訊碼流 該函式生成一個空AVstream 該結構存放編碼後的視訊碼流 。視訊碼流被拆分為AVPacket新式儲存在AVStream中。

         5、設定編碼器資訊,該步驟主要是為AVCodecContext(從AVStream->codec 獲取指標)結構體設定一些引數,包括codec_id、codec_type、width、height、pix_fmt .....  根據編碼器的不同,還要額外設定一些引數(如 h264 要設定qmax、qmin、qcompress引數才能正常使用h264編碼)

         6、查詢並開啟編碼器,根據前一步設定的編碼器引數資訊,來查詢初始化一個編碼其,並將其開啟。用到函式為av_fine_encoder()和av_open2()。

         7、寫標頭檔案  avformat_write_header()。這一步主要是將封裝格式的資訊寫入檔案頭部位置。

         8、編碼幀。用到的函式 avcodec_encode_video2() 將AVFrame編碼為AVPacket

         9、在寫入檔案之前 還需要做一件事情就是設定AVPacket一些資訊。這些資訊關乎最後封裝格式能否被正確讀取。後面回詳細講述該部分內容

        10、編碼幀寫入檔案 av_write_frame()

        11、flush_encoder():輸入的畫素資料讀取完成後呼叫此函式。用於輸出編碼器中剩餘的AVPacket。

        12、av_write_trailer():寫檔案尾(對於某些沒有檔案頭的封裝格式,不需要此函式。比如說MPEG2TS)。

下面貼上完整的程式程式碼:      

extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswscale/swscale.h>
}

using namespace std;

int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index);

int main(int argc, char *argv[])
{
    AVFormatContext *pFormatCtx=nullptr;
    AVOutputFormat *fmt=nullptr;
    AVStream *video_st=nullptr;
    AVCodecContext *pCodecCtx=nullptr;
    AVCodec *pCodec=nullptr;

    uint8_t *picture_buf=nullptr;
    AVFrame *picture=nullptr;
    int size;

    //開啟視訊
    FILE *in_file = fopen("F://src01_480x272.yuv", "rb");
    if(!in_file)
    {
        cout<<"can not open file!"<<endl;
        return -1;
    }

    int in_w=480,in_h=272;
    int framenum=50;
    const char* out_file="src01.mp4";

    //[1] --註冊所有ffmpeg元件
    avcodec_register_all();
    av_register_all();
    //[1]

    //[2] --初始化AVFormatContext結構體,根據檔名獲取到合適的封裝格式
    avformat_alloc_output_context2(&pFormatCtx,NULL,NULL,out_file);
    fmt = pFormatCtx->oformat;
    //[2]

    //[3] --開啟檔案
    if(avio_open(&pFormatCtx->pb,out_file,AVIO_FLAG_READ_WRITE))
    {
        cout<<"output file open fail!";
        goto end;
    }
    //[3]

    //[4] --初始化視訊碼流
    video_st = avformat_new_stream(pFormatCtx,0);
    if(video_st==NULL)
    { printf("failed allocating output stram\n");
        goto end;
    }
    video_st->time_base.num = 1;
    video_st->time_base.den =25;
    //[4]

    //[5] --編碼器Context設定引數
    pCodecCtx = video_st->codec;
    pCodecCtx->codec_id = fmt->video_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    pCodecCtx->width=in_w;
    pCodecCtx->height=in_h;
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    pCodecCtx->bit_rate = 400000;
    pCodecCtx->gop_size = 12;

    if(pCodecCtx->codec_id == AV_CODEC_ID_H264)
    {
        pCodecCtx->qmin = 10;
        pCodecCtx->qmax = 51;
        pCodecCtx->qcompress = 0.6;
    }
    if (pCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO)
        pCodecCtx->max_b_frames = 2;
    if (pCodecCtx->codec_id == AV_CODEC_ID_MPEG1VIDEO)
        pCodecCtx->mb_decision = 2;
    //[5]

    //[6] --尋找編碼器並開啟編碼器
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if(!pCodec)
    {
        cout<<"no right encoder!"<<endl;
        goto end;
    }
    if(avcodec_open2(pCodecCtx,pCodec,NULL)<0)
    {
        cout<<"open encoder fail!"<<endl;
        goto end;
    }
    //[6]

    //輸出格式資訊
    av_dump_format(pFormatCtx,0,out_file,1);

    //初始化幀
    picture = av_frame_alloc();
    picture->width=pCodecCtx->width;
    picture->height=pCodecCtx->height;
    picture->format=pCodecCtx->pix_fmt;
    size = avpicture_get_size(pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height);
    picture_buf = (uint8_t*)av_malloc(size);
    avpicture_fill((AVPicture*)picture,picture_buf,pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height);

    //[7] --寫標頭檔案
    avformat_write_header(pFormatCtx,NULL);
    //[7]

    AVPacket pkt; //建立已編碼幀
    int y_size = pCodecCtx->width*pCodecCtx->height;
    av_new_packet(&pkt,size*3);

    //[8] --迴圈編碼每一幀
    for(int i=0;i<framenum;i++)
    {
        //讀入YUV
        if(fread(picture_buf,1,y_size*3/2,in_file)<0)
        {
            cout<<"read file fail!"<<endl;
            goto end;
        }
        else if(feof(in_file))
            break;

        picture->data[0] = picture_buf; //亮度Y
        picture->data[1] = picture_buf+y_size; //U
        picture->data[2] = picture_buf+y_size*5/4; //V
        //AVFrame PTS
        picture->pts=i;
        int got_picture = 0;

        //編碼
        int ret = avcodec_encode_video2(pCodecCtx,&pkt,picture,&got_picture);
        if(ret<0)
        {
            cout<<"encoder fail!"<<endl;
            goto end;
        }

        if(got_picture == 1)
        {
            cout<<"encoder success!"<<endl;

            // parpare packet for muxing
            pkt.stream_index = video_st->index;
            av_packet_rescale_ts(&pkt, pCodecCtx->time_base, video_st->time_base);
            pkt.pos = -1;
            ret = av_interleaved_write_frame(pFormatCtx,&pkt);
            av_free_packet(&pkt);
        }
    }
    //[8]

    //[9] --Flush encoder
    int ret = flush_encoder(pFormatCtx,0);
    if(ret < 0)
    {
        cout<<"flushing encoder failed!"<<endl;
        goto end;
    }
    //[9]

    //[10] --寫檔案尾
    av_write_trailer(pFormatCtx);
    //[10]

end:
    //釋放記憶體
    if(video_st)
    {
        avcodec_close(video_st->codec);
        av_free(picture);
        av_free(picture_buf);
    }
    if(pFormatCtx)
    {
        avio_close(pFormatCtx->pb);
        avformat_free_context(pFormatCtx);
    }

    fclose(in_file);

    return 0;
}

int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
          CODEC_CAP_DELAY))
        return 0;
    while (1) {
        printf("Flushing stream #%u encoder\n", stream_index);
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                     NULL, &got_frame);
        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame)
        {ret=0;break;}
        cout<<"success encoder 1 frame"<<endl;

        // parpare packet for muxing
        enc_pkt.stream_index = stream_index;
        av_packet_rescale_ts(&enc_pkt,
                             fmt_ctx->streams[stream_index]->codec->time_base,
                             fmt_ctx->streams[stream_index]->time_base);
        ret = av_interleaved_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}

  在步驟9中已說明要完成最後的格式封裝(mux),需要對AVPacket的引數進行設定。具體設定項如下:

 // parpare packet for muxing
        enc_pkt.stream_index = stream_index;
        av_packet_rescale_ts(&enc_pkt,
                             fmt_ctx->streams[stream_index]->codec->time_base,
                             fmt_ctx->streams[stream_index]->time_base);
1、包括設定AVPacket所屬的碼流index,

2、將pts(顯示時間戳)和dts(編碼時間戳)和duration(nextpts - curpts)按照不同time_base(時間基準)進行轉換。具體的pts、dts、time_base概念可以見參考文獻一。至於轉換原因可以參考參考文獻二。

參考文獻: