1. 程式人生 > >FFmpeg框架解析及核心資料結構

FFmpeg框架解析及核心資料結構

ffmpeg關鍵資料結構及對應的協議層

功能 結構體
整個音視訊檔案的抽象(包含下面所有結構體) AVFormatContext
協議層 AVIOContext
封裝格式層 AVInputFormat/AVOutputFormat
解碼之前的資料 AVPacket
編碼層 AVStream[0]->AVCodecContext->AVCodec
AVStream[1]->AVCodecContext->AVCodec
解碼之後的資料 AVFrame

協議層(http,rtsp,rtmp,mms)

AVIOContext,URLProtocol,URLContext主要儲存視音訊使用的協議的型別以及狀態。
URLProtocol儲存輸入視音訊使用的封裝格式。
每種協議都對應一個URLProtocol結構。(注意:FFMPEG中檔案也被當做一種協議“file”)

封裝層(flv,avi,rmvb,mp4)

解封裝

結果: 產生壓縮的碼流資料(解碼前資料)——AVPacket
視訊的話,每個結構一般是存一幀;音訊可能有好幾幀

封裝

將h.264等裸流,封裝為檔案

主要結構體及初始化

AVFormatContext主要儲存視音訊封裝格式中包含的資訊(非常重要,包含封裝層、編碼層)

  1. AVFormatContext結構體簡介

    struct AVInputFormat * iformat:輸入資料的封裝格式
    
    AVIOContext * pb:輸入資料的快取
    
    unsigned int nb_streams:視音訊流的個數
    
    AVStream ** streams:視音訊流
    
    char filename[1024]:檔名
    
    int64_t duration:時長(單位:微秒us,轉換為秒需要除以1000000)
    
    int bit_rate:位元率(單位bps,轉換為kbps需要除以1000)
    
    AVDictionary * metadata:元資料
    
  2. 建立 AVFormatContext的方法

    1. 方法一: 通過開啟檔案獲取 AVFormatContext(解封裝輸入檔案格式) (解封裝,自頂向下初始化)

      1. 第一步: FFMpeg進行其他操作一樣,首先需註冊FFMpeg元件

        av_register_all();

      2. 第二步: 開啟待處理音視訊檔案

        然而在此我們不使用開啟檔案的fopen函式,而是使用avformat_open_input函式。該函式不但會開啟輸入檔案,而且可以根據輸入檔案讀取相應的格式資訊。該函式的宣告如下:

        int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

        該函式的各個引數的作用為:

        ps:根據輸入檔案接收與格式相關的控制代碼資訊;可以指向NULL,那麼AVFormatContext型別的例項將由該函式進行分配。
        url:視訊url或者檔案路徑;
        fmt:強制輸入格式,可設定為NULL以自動檢測;
        options:儲存檔案格式無法識別的資訊;
        返回值:成功返回0,失敗則返回負的錯誤碼;

        該函式的呼叫方式為:

        if (avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL) < 0)
        {
        fprintf(stderr, “Could not open source file %s\n”, files.src_filename);
        return -1;
        }

      3. 第三步: 獲取檔案中的流資訊

        該函式的宣告為:

        int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

        該函式的第一個引數即前面的檔案控制代碼,第二個引數也是用於儲存無法識別的資訊的AVDictionary的結構,通常可設為NULL。呼叫方式如:

        * retrieve stream information *
        if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0)
        {
        fprintf(stderr, “Could not find stream information\n”);
        return -1;
        }

    2. 方法二: 手動創造 AVFormatContext(封裝輸出檔案格式)(封裝,自底向上初始化)

      1. 第一步: 根據輸出檔案的格式獲取AVFormatContext結構

        根據輸出檔案的格式獲取AVFormatContext結構,獲取AVFormatContext結構使用函式avformat_alloc_output_context2實現。該函式的宣告為:

        int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);

        其中:

        ctx:輸出到AVFormatContext結構的指標,如果函式失敗則返回給該指標為NULL;
        oformat:指定輸出的AVOutputFormat型別,如果設為NULL則使用format_name和filename生成;
        format_name:輸出格式的名稱,如果設為NULL則使用filename預設格式;
        filename:目標檔名,如果不使用,可以設為NULL;

      2. 第二步: 判斷 AVFormatContext中是否存在音訊流或者視訊流

        if (fmt->video_codec != AV_CODEC_ID_NONE)
        {
        add_stream(video_st, oc, &video_codec, fmt->video_codec);
        video_st->st->codec->width = io.frame_width;
        video_st->st->codec->height = io.frame_height;
        ret |= HAVE_VIDEO;
        ret |= ENCODE_VIDEO;
        }
        if (fmt->audio_codec != AV_CODEC_ID_NONE)
        {
        add_stream(audio_st, oc, &audio_codec, fmt->audio_codec);
        ret |= HAVE_AUDIO;
        ret |= ENCODE_AUDIO;
        }

      3. 第三步: 向AVFormatContext結構中所代表的媒體檔案中新增資料流

        新增流首先需要查詢流所包含的媒體的編碼器,這需要傳入codec_id後使用avcodec_find_encoder函式實現,將查詢到的編碼器儲存在AVCodec指標中。

        呼叫avformat_new_stream函式向AVFormatContext結構中所代表的媒體檔案中新增資料流。該函式的宣告如下:

        AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);

        其中各個引數的含義:

        s:AVFormatContext結構,表示要封裝生成的視訊檔案;
        c:上一步根據codec_id產生的編碼器指標;
        返回值:指向生成的stream物件的指標;如果失敗則返回NULL指標。

        此時,一個新的AVStream便已經加入到輸出檔案中。

AVStream是視訊檔案中某一音訊流或者視訊流的抽象。

儲存一個視訊/音訊流(一系列包的集合)的結構。
AVStream可以表示封裝格式中的音訊或者視訊資料。通過AVStream可以得到音訊流或者視訊流對應的AVCodecContext,儲存該視訊/音訊流使用解碼方式的相關資料。
主要有下面兩種獲取AVStream的方法:

  1. 方法一: 通過開啟檔案的 AVFormatContext,獲取音訊和視訊AVStream

    1. 第一步: 通過開啟檔案獲取 AVFormatContext(解封裝輸入檔案格式

    2. 第二步: 獲取檔案中的音訊和視訊AVStream

      獲取檔案中的流使用av_find_best_stream函式,其宣告如:

      int av_find_best_stream(AVFormatContext *ic,
      enum AVMediaType type,
      int wanted_stream_nb,
      int related_stream,
      AVCodec **decoder_ret,
      int flags);

      其中各個引數的意義:

      ic:視訊檔案控制代碼;
      type:表示資料的型別,常用的有AVMEDIA_TYPE_VIDEO表示視訊,AVMEDIA_TYPE_AUDIO表示音訊等;
      wanted_stream_nb:我們期望獲取到的資料流的數量,設定為-1使用自動獲取;
      related_stream:獲取相關的音視訊流,如果沒有則設為-1;
      decoder_ret:返回這一路資料流的解碼器;
      flags:未定義;
      返回值:函式執行成功返回 type流的序號 ,失敗則返回負的錯誤碼; (解封裝)

  2. 方法二: 手動構造 AVStream (封裝)

其他結構體

AVInputFormat儲存輸入視音訊使用的封裝格式,每種視音訊封裝格式都對應一個AVInputFormat 結構。

編碼或者解碼

主要結構體及初始化

編解碼器結構體 AVCodec

每個AVCodecContext中對應一個AVCodec,包含該視訊/音訊對應的解碼器。每種解碼器都對應一個AVCodec結構。

AVCodec儲存一個編解碼器的例項,實現時實際的編碼功能。通常我們在程式中定義一個指向AVCodec結構的指標指向該例項。

根據編解碼器ID,獲取編解碼器指標的方法有以下兩種:

  1. 第一步: 所有涉及到編解碼的的功能,都必須要註冊音視訊編解碼器之後才能使用。

    avcodec_register_all();

  2. 第二步: 通過編解碼器ID獲取編解碼器指標

    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;
    }

編解碼器引數配置結構體 AVCodecContext

AVCodecContext表示AVCodec所需要的上下文資訊,儲存AVCodec所需要的一些引數。實現編碼功能時,通過AVCodecContext設定編碼器引數。

  1. 方法一: 解封裝解碼過程,通過AVStream獲取編解碼器上下文指標

    AVCodecContext *c;
    c = AVFormatContext->AVStream->codec;

  2. 方法二: 通過編解碼器AVCodec指標,獲取編解碼器上下文

    分配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;
    }

編碼

成功將原始的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));
}

解碼

在最終解析出一個完整的包之後,我們就可以呼叫解碼API進行實際的解碼過程了。解碼過程呼叫的函式為avcodec_decode_video2,該函式的宣告為:

int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
int *got_picture_ptr,
const AVPacket *avpkt);

這個函式與前篇所遇到的編碼函式avcodec_encode_video2有些類似,只是引數的順序略有不同,解碼函式的輸入輸出引數與編碼函式相比交換了位置。該函式各個引數的意義:

AVCodecContext *avctx:編解碼器上下文物件,在開啟編解碼器時生成;
AVFrame *picture: 儲存解碼完成後的畫素資料;我們只需要分配物件的空間,畫素的空間codec會為我們分配好;
int *got_picture_ptr: 標識位,如果為1,那麼說明已經有一幀完整的畫素幀可以輸出了
const AVPacket *avpkt: 前面解析好的碼流包;

實際呼叫的方法為:

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(“Succeed to decode 1 frame!\n”);
}
結果: 產生解壓縮後的資料(解碼後資料)——AVFrame

編碼和解碼的資料輸入和輸出

編碼的輸入,解碼的輸出。AVFrame結構體

  1. AVFrame結構體

    即非壓縮資料,例如對視訊來說是YUV,RGB,對音訊來說是PCM
    FFMPEG結構體分析:AVFrame - 雷霄驊(leixiaohua1020)的專欄 - 部落格頻道 - CSDN.NET
    uint8_t *data[AV_NUM_DATA_POINTERS]:解碼後原始資料(對視訊來說是YUV,RGB,對音訊來說是PCM)
    對於packed格式的資料(例如RGB24),會存到data[0]裡面。
    對於planar格式的資料(例如YUV420P),則會分開成data[0],data[1],data[2]…(YUV420P中data[0]存Y,data[1]存U,data[2]存V)

    int linesize[AV_NUM_DATA_POINTERS]:data中“一行”資料的大小。注意:未必等於影象的寬,一般大於影象的寬。

    int width, height:視訊幀寬和高(1920x1080,1280x720…)

    int nb_samples:音訊的一個AVFrame中可能包含多個音訊幀,在此標記包含了幾個

    int format:解碼後原始資料型別(YUV420,YUV422,RGB24…)

    int key_frame:是否是關鍵幀

    enum AVPictureType pict_type:幀型別(I,B,P…)

    AVRational sample_aspect_ratio:寬高比(16:9,4:3…)

    int64_t pts:顯示時間戳

    int coded_picture_number:編碼幀序號

    int display_picture_number:顯示幀序號

    int8_t *qscale_table:QP表

    uint8_t *mbskip_table:跳過巨集塊表

    int16_t (*motion_val[2])[2]:運動矢量表

    uint32_t *mb_type:巨集塊型別表

    short *dct_coeff:DCT係數,這個沒有提取過

    int8_t *ref_index[2]:運動估計參考幀列表(貌似H.264這種比較新的標準才會涉及到多參考幀)

    int interlaced_frame:是否是隔行掃描

    uint8_t motion_subsample_log2:一個巨集塊中的運動向量取樣個數,取log的

    其他的變數不再一一列舉,原始碼中都有詳細的說明。在這裡重點分析一下幾個需要一定的理解的變數:

  2. 方法一: 配置編碼的輸入資料

    1. 第一步: 分配AVFrame物件。

      分配物件空間類似於new操作符一樣,只是需要呼叫函式av_frame_alloc。如果失敗,那麼函式返回一個空指標。

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

    2. 第二步: AVFrame物件分配成功後,需要設定影象的解析度和畫素格式等

      ctx.frame->format = ctx.c->pix_fmt;
      ctx.frame->width = ctx.c->width;
      ctx.frame->height = ctx.c->height;

    3. 第三步: 分配實際的畫素資料的儲存空間。

      分配畫素的儲存空間需要呼叫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;
      }

      需要注意的是,linesize中的值通常指的是stride而不是width,也就是說,畫素儲存區可能是帶有一定寬度的無效邊區的,在讀取資料時需注意。

  3. 方法二: 為frame分配空間

    static AVFrame *alloc_picture(enum AVPixelFormat pix_fmt, int width, int height)
    {
    AVFrame *picture;
    int ret;

    picture = av_frame_alloc();
    if (!picture)
    {
    return NULL;
    }

    picture->format = pix_fmt;
    picture->width = width;
    picture->height = height;

    * allocate the buffers for the frame data *
    ret = av_frame_get_buffer(picture, 32);
    if (ret < 0)
    {
    fprintf(stderr, “Could not allocate frame data.\n”);
    exit(1);
    }

    return picture;  
    

    }

編碼的輸出,解碼的輸入。AVPacket結構體

各層結構體初始化步驟

解封裝->解碼(自頂向下初始化)

AVFormatContext->AVStream->AVCodecContext->AVCodec

封裝(自底向上初始化)

AVCodec->AVStream->AVFormatContext
->AVCodecContext