1. 程式人生 > >【FFMpeg視訊開發與應用基礎】二、呼叫FFmpeg SDK對YUV視訊序列進行編碼

【FFMpeg視訊開發與應用基礎】二、呼叫FFmpeg SDK對YUV視訊序列進行編碼

《FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK》視訊教程已經在“CSDN學院”上線,視訊中包含了從0開始逐行程式碼實現FFMpeg視訊開發的過程,歡迎觀看!連結地址:FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK

視訊由畫素格式編碼為碼流格式是FFMpeg的一項基本功能。通常,視訊編碼器的輸入視訊通常為原始的影象畫素值,輸出格式為符合某種格式規定的二進位制碼流。

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進行視訊編碼的主要流程如:

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

相關推薦

FFMpeg視訊開發應用基礎呼叫FFmpeg SDKYUV視訊序列進行編碼

《FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK》視訊教程已經在“CSDN學院”上線,視訊中包含了從0開始逐行程式碼實現FFMpeg視訊開發的過程,歡迎觀看!連結地址:FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK

FFMpeg視訊開發應用基礎 呼叫FFMpeg SDK實現視訊縮放

《FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK》視訊教程已經在“CSDN學院”上線,視訊中包含了從0開始逐行程式碼實現FFMpeg視訊開發的過程,歡迎觀看!連結地址:FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK

FFMpeg視訊開發應用基礎呼叫FFMpeg SDK封裝音訊和視訊視訊檔案

《FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK》視訊教程已經在“CSDN學院”上線,視訊中包含了從0開始逐行程式碼實現FFMpeg視訊開發的過程,歡迎觀看!連結地址:FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK

FFMpeg視訊開發應用基礎使用FFmpeg命令列工具和批處理指令碼進行簡單的音視訊檔案編輯

《FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK》視訊教程已經在“CSDN學院”上線,視訊中包含了從0開始逐行程式碼實現FFMpeg視訊開發的過程,歡迎觀看!連結地址:FFMpeg視訊開發與應用基礎——使用FFMpeg工具與SDK

算法設計分析基礎11廣度優先遍歷

append path str src http adt ise uil tor package cn.xf.algorithm.ch03; import java.util.ArrayDeque; import java.util.Iterator; import j

算法設計分析基礎12插入排序

排序 org vision out sys pac data image 技術 package cn.xf.algorithm.ch04; import org.junit.Test; /** * * * . * @author xiaof * @ver

算法設計分析基礎14快速排序

元素 right get class ima div true cnblogs param package cn.xf.algorithm.ch04; import java.util.ArrayList; import java.util.Arrays; import

算法設計分析基礎15最近問題

filename com 算法設計 2個 junit 開始 替換 lis 之間 1、由於Java中沒有存放單個鍵值對的類型使用起來不是很方便 package cn.xf.util; /** * * 功能:相當於一個key value * @author xi

算法設計分析基礎16高斯消元法

ane sys cnblogs 根據 gauss tostring logs junit air package cn.xf.algorithm.ch06ChangeRule; import java.util.ArrayList; import java.util.L

算法設計分析基礎23堆排序-2

mov 完全二叉樹 return 遍歷 滿足 指定 val cti eap package cn.xf.algorithm.ch09Greedy.util; import java.util.ArrayList; import java.util.List; /**

.NET基礎拾遺(7)Web Service的開發應用基礎

一、SOAP和Web Service的基本概念   Web Service基於SOAP協議,而SOAP本身符合XML語法規範。雖然.NET為Web Service提供了強大的支援,但瞭解其基本機制對於程式設計師來說仍然是必需的。 1.1 神馬是SOAP協議?   SOAP協議的全稱是簡單物件訪問協議

數據結構算法叉樹遞歸非遞歸遍歷(附完整源碼)(轉)

style stack gravity text 一個 eat 遞歸遍歷 deb 雙向 轉自:http://blog.csdn.net/ns_code/article/details/12977901 二叉樹是一種非常重要的數據結構,很多其他數據機構都是基於二叉樹的基礎

數據結構算法叉樹——哈夫曼編碼

個人 分享 recode sort 遞歸 運用 數據結構 light 什麽是 最近有很多的小朋友問我什麽是哈夫曼編碼,哈夫曼編碼是一種可變字長的編碼,那什麽是可變字長呢?就是一句話裏的每一個字符(ASCII碼)它的位數(長度)是不一樣的。就像我們一句話(AAAACCCCCD

electron+vue3+ts實戰便箋exeelectron+vue3開發內容

> 不要讓自己的上限成為你的底線 本來以為有萬字的。。沒想到才堪堪近6000字。為了水文的嫌疑,只挑了重點的地方講,比如`component`內的元件就挑了`右鍵彈窗去說明`,建議在看本文的時候邊檢視專案,有不懂的可以在下方評論,謝謝。 > github > github: http

小白學遊戲常用演算法A*啟發式搜尋演算法

  在上一篇部落格中,我們一起學習了隨機迷宮演算法,在本篇部落格中,我們將一起了解一下尋路演算法中常用的A*演算法。   通常情況下,迷宮尋路演算法可以使用深度優先或者廣度優先演算法,但是由於效率的原因,不會直接使用這些演算法,在路徑搜尋演算法中最常見的就是A*尋路演算法。使用A*演算法的魅力之處在於它不僅

平差軟體學習---科傻原始資料檔名的規則

關鍵詞:科傻平差檔案平面檔案編輯器、徠卡gis|天寶原始資料轉科傻in1平差檔案、高程平差軟體、高程資料怎麼平差,附和導線怎麼平差、無定向導線平差軟體。 科傻的資料編輯讓現在的人很蛋疼。他視覺化,但是就差那麼一部就完美了,這點我局的人家南方平差易和清華山維做的不錯。最起碼是個表格方式的編輯方式。 科傻要自

11月11日-13日 黃山SWAT模型-水迴圈模型的開發應用培訓班

SWAT模型-水迴圈模型的開發與應用培訓班 培訓背景 受全球環境變化和經濟快速發展的影響,我國水短缺、水汙染、水生態、水災害、水管理五方面問題複雜交叉,直接涉及國家多方面的安全,是一個複雜的水系統問題。解決上述水問題的核心是水迴圈基礎研究,需要深入研究以流域水迴圈

交流平臺歡迎加入“視訊開發編碼技術”QQ群

從2013年開始寫技術部落格以來,到目前已經過去了快4年的時間。這段時間裡看了不少的書和資料,寫了不多的程式碼,走了不少的彎路,沒取得多大的成績。很多時候感覺是缺乏足夠的有效溝通交流導致的。也許我們加入了兩千人的大群,不過很多時候,大多數的群友與我們的研究方向並

2018.10.11 CC++基礎C Preprocessor的功能及缺陷(草稿)

repr 繼承 logs 作用域 ces 變量 找到 可變參數 體系 一、前言及參考資料 C Preprocessor即所謂的C預處理器,C++也繼承了C的預處理程序,但在C++語言的設計與演化一書中,C++的設計者Bjarne Strustrup提及他從未喜歡過C預處理器

軟體開發底層知識修煉 深入淺出處理器之 中斷的概念意義

上一篇文章我們學習了微處理器與微控制器的區別。點選連結檢視上一篇文章的內容:微處理器與微控制器 本片文章我們學習中斷的概念與意義。本片文章只學習中斷概念與意義,並不深入研究中斷向量表與ISR中斷服務程式的實現。同時本片文章講解一個與中斷有關的應用:斷點除錯。當然具體的斷點除錯方法會在