Qt與FFmpeg聯合開發指南(序)-- FFmpeg框架的基礎知識

ffmpeg是一個非常有用的命令行程序,它可以用來轉碼媒體文件。它是領先的多媒體框架FFmpeg的一部分,其有很多功能,比如解碼、編碼、轉碼、混流、分離、轉化爲流、過濾以及播放幾乎所有的由人和機器創建的媒體文件。
在這個框架中包含有各種工具,每一個用於完成特定的功能。例如:

  • ffserver能夠將多媒體文件轉化爲用於實時廣播的流
  • ffprobe用於分析多媒體流
  • ffplay可以當作一個簡易的媒體播放器
  • ffmpeg則能夠轉換多媒體文件格式。

FFMPEG從功能上劃分爲幾個模塊分別爲:

  • 核心工具(libutils)
  • 媒體格式(libavformat)
  • 編解碼(libavcodec)
  • 設備(libavdevice)
  • 後處理(libavfilter, libswscale, libpostproc)

分別負責提供公用的功能函數、實現多媒體文件的讀包和寫包、完成音視頻的編解碼、管理音視頻設備的操作以及進行音視頻後處理。

libavutil是一個包含簡化編程功能的庫,其中包括隨機數生成器,數據結構,數學代碼,核心多媒體工具等更多東西。
libavcodec是一個包含音頻/視頻解碼器和編碼器的庫。
libavformat是一個包含了多媒體格式的分離器和混流器的庫。
libavdevice是一個包含輸入輸出設備的庫,用於捕捉和渲染很多來自常用的多媒體輸入/輸出軟件框架的數據,包括Video4Linux,Video4Linux2,VfW和ALSA。
libavfilter是一個包含媒體過濾器的庫。AVFilter可以給視音頻添加各種濾鏡效果。可以給視頻添加水印,給YUV數據加特效。
libswscale是一個用於執行高度優化的圖像縮放和顏色空間/像素格式轉換操作的庫。
libswresample是一個用於執行高度優化的音頻重採樣,重新矩陣和取樣格式轉換操作的庫。

在視頻解碼前,先了解以下幾個基本的概念:

編解碼器(CODEC):能夠進行視頻和音頻壓縮(CO)與解壓縮(DEC),是視頻編解碼的核心部分。
容器/多媒體文件(Container/File):沒有了解視頻的編解碼之前,總是錯誤的認爲平常下載的電影的文件的後綴(avi,mkv,rmvb等)就是視頻的編碼方式。事實上,剛纔提到的幾種文件的後綴並不是視頻的編碼方式,只是其封裝的方式。一個視頻文件通常有視頻數據、音頻數據以及字幕等,封裝的格式決定這些數據在文件中是如何的存放的,封裝在一起音頻、視頻等數據組成的多媒體文件,也可以叫做容器(其中包含了視音頻數據)。所以,只看多媒體文件的後綴名是難以知道視音頻的編碼方式的。
流數據 Stream,例如視頻流(Video Stream),音頻流(Audio Stream)。流中的數據元素被稱爲幀Frame。一個多媒體文件包含有多個流(視頻流 video stream,音頻流 audio stream,字幕等);流是一種抽象的概念,表示一連串的數據元素;流中的數據元素稱爲幀Frame。

FFMPEG視頻解碼流程:
通常來說,FFmpeg的視頻解碼過程有以下幾個步驟:

 

1. 註冊所有容器格式及其對應的CODEC: av_register_all()
2. 打開文件: av_open_input_file()
3. 從文件中提取流信息: av_find_stream_info()
4. 窮舉所有的流,查找其中種類爲CODEC_TYPE_VIDEO的視頻流video stream
5. 查找對應的解碼器: avcodec_find_decoder()
6. 打開編解碼器: avcodec_open2()
7. 爲解碼幀分配內存: avcodec_alloc_frame()
8. 不停地從碼流中提取出幀數據到Packet中: av_read_frame()  avcodec_send_packet()
9. 判斷幀的類型,對於視頻幀調用: avcodec_decode_video2()  avcodec_receive_frame()
10. 解碼完後,釋放解碼器: avcodec_close()
11. 關閉輸入文件: avformat_close_input_file()

解碼過程的具體說明

  1. 註冊
    av_register_all該函數註冊支持的所有的文件格式(容器)及其對應的CODEC,只需要調用一次,故一般放在main函數中。也可以註冊某個特定的容器格式,但通常來說不需要這麼做。

  2. 打開文件
    avformat_open_input該函數讀取文件的頭信息,並將其信息保存到AVFormatContext結構體中。其調用如下

    AVFormatContext* pFormatCtx = nullptr;  
    avformat_open_input(&pFormatCtx, filenName, nullptr, nullptr)  
    

第一個參數是AVFormatContext結構體的指針,第二個參數爲文件路徑;第三個參數用來設定輸入文件的格式,如果設爲null,將自動檢測文件格式;第四個參數用來填充AVFormatContext一些字段以及Demuxer的private選項。
AVFormatContext包含有較多的碼流信息參數,通常由avformat_open_input創建並填充關鍵字段。

  1. 獲取必要的CODEC參數
    avformat_open_input通過解析多媒體文件或流的頭信息及其他的輔助數據,能夠獲取到足夠多的關於文件、流和CODEC的信息,並將這些信息填充到AVFormatContext結構體中。但任何一種多媒體格式(容器)提供的信息都是有限的,而且不同的多媒體制作軟件對頭信息的設置也不盡相同,在製作多媒體文件的時候難免會引入一些錯誤。也就是說,僅僅通過avformat_open_input並不能保證能夠獲取所需要的信息,所以一般要使用

    avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
    avformat_find_stream_info主要用來獲取必要的CODEC參數,設置到ic->streams[i]->codec。
    

在解碼的過程中,首先要獲取到各個stream所對應的CODEC類型和id,CODEC的類型和id是兩個枚舉值,其定義如下:

 

   enum AVMediaType { 
      AVMEDIA_TYPE_UNKNOWN = -1,     
      AVMEDIA_TYPE_VIDEO,     
      AVMEDIA_TYPE_AUDIO,     
      AVMEDIA_TYPE_DATA, 
      AVMEDIA_TYPE_SUBTITLE,    
      AVMEDIA_TYPE_ATTACHMENT,     
      AVMEDIA_TYPE_NB
    }; 

   enum CodecID { 
     CODEC_ID_NONE,     /* video codecs */ 
     CODEC_ID_MPEG1VIDEO, 
     CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding     
     CODEC_ID_MPEG2VIDEO_XVMC,     
     CODEC_ID_H261,     
     CODEC_ID_H263, 
     ...
   }

通常,如果多媒體文件具有完整而正確的頭信息,通過avformat_open_input即可用獲得這兩個參數。

  1. 打開解碼器
    經過上面的步驟,已經將文件格式信息讀取到了AVFormatContext中,要打開流數據相應的CODEC需要經過下面幾個步驟
    找到視頻流 video stream
    一個多媒體文件包含有多個原始流,例如 movie.mkv這個多媒體文件可能包含下面的流數據

    原始流 1 h.264 video
    原始流 2 aac audio for Chinese
    原始流 3 aac audio for English
    原始流 4 Chinese Subtitle
    原始流 5 English Subtitle
    

要解碼視頻,首先要在AVFormatContext包含的多個流中找到CODEC類型爲AVMEDIA_TYPE_VIDEO,代碼如下:

 

   //查找視頻流 video stream
   int videoStream = -1;
   for (int i = 0; i < pFormatCtx->nb_streams; i++)
   {
       if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
       {
           videoStream = i;
           break;
       }
   }
   if (videoStream == -1)
       return -1; // 沒有找到視頻流video stream  

結構體AVFormatContext中的streams字段是一個AVStream指針的數組,包含了文件所有流的描述,上述上述代碼在該數組中查找CODEC類型爲AVMEDIA_TYPE_VIDEO的流的下標。

根據codec_id找到相應的CODEC,並打開結構體AVCodecContext描述了CODEC上下文,包含了衆多CODEC所需要的參數信息。

 

AVCodecContext* pCodecCtxOrg = nullptr; 
AVCodec* pCodec = nullptr;
pCodecCtxOrg = pFormatCtx->streams[videoStream]->codec; // codec context
// 找到video stream的 decoder
pCodec = avcodec_find_decoder(pCodecCtxOrg->codec_id); 
// open codec
if (avcodec_open2(pCodecCtxOrg , pCodec, nullptr) < 0)
   return -1; // Could open codec  

上述代碼,首先通過codec_id找到相應的CODEC,然後調用avcodec_open2打開相應的CODEC。

  1. 讀取數據幀並解碼
    已經有了相應的解碼器,下面的工作就是將數據從流中讀出,並解碼爲沒有壓縮的原始數據

    AVPacket packet; 
    while (av_read_frame(pFormatCtx, &packet) >= 0)
    {
         if (packet.stream_index == videoStream)
         {
             int frameFinished = 0;
             avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
             if (frameFinished)
             {
                 doSomething();
             }
         }
     
    } 

上述代碼調用av_read_frame將數據從流中讀取數據到packet中,並調用avcodec_receive_frame對讀取的數據進行解碼。

  1. 關閉
    需要關閉avformat_open_input打開的輸入流,avcodec_open2打開的CODEC

    avcodec_close(pCodecCtxOrg);
    avformat_close_input(&pFormatCtx);  
    

也就是說多媒體文件中,主要有兩種數據:流Stream 及其數據元素 幀Frame,在FFmpeg自然有與這兩種數據相對應的抽象:AVStream和AVPacket。
使用FFmpeg的解碼,數據的傳遞過程可歸納如下:

調用avformat_open_input打開流,將信息填充到AVFormatContext中
調用av_read_frame從流中讀取數據幀到 AVPacket,AVPacket保存仍然是未解碼的數據。
調用avcodec_decode_video2將AVPacket的數據解碼,並將解碼後的數據填充到AVFrame中,AVFrame中保存的是解碼後的原始數據。

 

結構體的存儲空間的分配與釋放

  • FFmpeg並沒有垃圾回收機制,所分配的空間都需要自己維護。而由於視頻處理過程中數據量是非常大,對於動態內存的使用更要謹慎。
  • AVFormatContext 在FFmpeg中有很重要的作用,描述一個多媒體文件的構成及其基本信息,存放了視頻編解碼過程中的大部分信息。通常該結構體由avformat_open_input分配存儲空間,在最後調用avformat_input_close關閉。
  • AVStream 描述一個媒體流,在解碼的過程中,作爲AVFormatContext的一個字段存在,不需要單獨的處理。
  • AVpacket 用來存放解碼之前的數據,它只是一個容器,其data成員指向實際的數據緩衝區,在解碼的過程中可有av_read_frame創建和填充AVPacket中的數據緩衝區,當數據緩衝區不再使用的時候可以調用av_free_apcket釋放這塊緩衝區。
  • AVFrame 存放從AVPacket中解碼出來的原始數據,其必須通過av_frame_alloc來創建,通過av_frame_free來釋放。和AVPacket類似,AVFrame中也有一塊數據緩存空間,在調用av_frame_alloc的時候並不會爲這塊緩存區域分配空間,需要使用其他的方法。在解碼的過程使用了兩個AVFrame,這兩個AVFrame分配緩存空間的方法也不相同
一個AVFrame用來存放從AVPacket中解碼出來的原始數據,這個AVFrame的數據緩存空間通過調avcodec_decode_video分配和填充。
另一個AVFrame用來存放將解碼出來的原始數據變換爲需要的數據格式(例如RGB,RGBA)的數據,這個AVFrame需要手動的分配數據緩存空間。代碼如下:

     AVFrame *pFrameYUV;
     pFrameYUV = av_frame_alloc();
     // 手動爲 pFrameYUV分配數據緩存空間
     int numBytes = avpicture_get_size(AV_PIX_FMT_YUV420P,pCodecCtx->widht,pCodecCtx->width);
     uint8_t *buffer = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t));
     // 將分配的數據緩存空間和AVFrame關聯起來
     avpicture_fill((AVPicture *)pFrameYUV, buffer, AV_PIX_FMT_YUV420P,pCodecCtx->width,  pCodecCtx->height)

首先計算需要緩存空間大小,調用av_malloc分配緩存空間,最後調用avpicture_fill將分配的緩存空間和AVFrame關聯起來。
調用av_frame_free來釋放AVFrame,該函數不止釋放AVFrame本身的空間,還會釋放掉包含在其內的其他對象動態申請的空間,例如上面的緩存空間。

av_malloc和av_free,FFmpeg並沒有提供垃圾回收機制,所有的內存管理都要手動進行。av_malloc只是在申請內存空間的時候會考慮到內存對齊(2字節,4字節對齊),
其申請的空間要調用av_free釋放。

調用的函數

av_register_all 這個函數不用多說了,註冊庫所支持的容器格式及其對應的CODEC。
avformat_open_input 打開多媒體文件流,並讀取文件的頭,將讀取到的信息填充到AVFormatContext結構體中。在使用結束後,要調用avformat_close_input關閉打開的流
avformat_find_stream_info 上面提到,avformat_open_input只是讀取文件的頭來得到多媒體文件的信息,但是有些文件沒有文件頭或者文件頭的格式不正確,這就造成只調用
avformat_open_input可能得不到解碼所需要的必要信息,需要調用avformat_find_stream_info進一步得到流的信息。

通過上面的三個函數已經獲取了對多媒體文件進行解碼的所需要信息,下面要做的就是根據這些信息得到相應的解碼器。
結構體AVCodecContext描述了編解碼器的上下文信息,包含了流中所使用的關於編解碼器的所有信息,可以通過 AVFormatContext->AVStream->AVCodecContext來得到,在有了AVCodecContext後,可以通過codec_id來找到相應的解碼器,具體代碼如下:

AVCodec* pCodec = nullptr;
pCodecCtxOrg = pFormatCtx->streams[videoStream]->codec; // codec context
// 找到video stream的 decoder
pCodec = avcodec_find_decoder(pCodecCtxOrg->codec_id);  
avcodec_find_decoder 可以通過codec_id或者名稱來找到相應的解碼器,返回值是一個AVCodec的指針。
avcodec_open2 打開相應的編解碼器
av_read_frame 從流中讀取數據幀暫存到AVPacket中
avcodec_decode_video2 從AVPacket中解碼數據到AVFrame中

經過以上的過程,AVFrame中的數據緩存中存放的就是解碼後的原始數據了。整個流程梳理如下:

 

(1)RGB轉換成YUV

Y = 0.299R + 0.587G + 0.114B
U = 0.567(B - Y)
V = 0.713(R - Y)

值得注意的是,Y值範圍爲[0, 1.0]、UV值範圍都是[-0.5, 0.5]。

(2)YUV轉換成RGB

R = Y + 1.402V
G = Y - 0.344U - 0.714V
B = Y + 1.772U

視音頻技術主要包含以下幾點:封裝技術,視頻壓縮編碼技術以及音頻壓縮編碼技術。如果考慮到網絡傳輸的話,還包括流媒體協議技術。

視頻播放器播放一個互聯網上的視頻文件,需要經過以下幾個步驟:解協議,解封裝,解碼視音頻,視音頻同步。

  • 解協議的作用,就是將流媒體協議的數據,解析爲標準的相應的封裝格式數據。視音頻在網絡上傳播的時候,常常採用各種流媒體協議,例如HTTP,RTMP,或是MMS等等。這些協議在傳輸視音頻數據的同時,也會傳輸一些信令數據。這些信令數據包括對播放的控制(播放,暫停,停止),或者對網絡狀態的描述等。解協議的過程中會去除掉信令數據而只保留視音頻數據。例如,採用RTMP協議傳輸的數據,經過解協議操作後,輸出FLV格式的數據。
  • 解封裝的作用,就是將輸入的封裝格式的數據,分離成爲音頻流壓縮編碼數據和視頻流壓縮編碼數據。封裝格式種類很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是將已經壓縮編碼的視頻數據和音頻數據按照一定的格式放到一起。例如,FLV格式的數據,經過解封裝操作後,輸出H.264編碼的視頻碼流和AAC編碼的音頻碼流。 
  • 解碼的作用,就是將視頻/音頻壓縮編碼數據,解碼成爲非壓縮的視頻/音頻原始數據。音頻的壓縮編碼標準包含AAC,MP3,AC-3等等,視頻的壓縮編碼標準則包含H.264,MPEG2,VC-1等等。解碼是整個系統中最重要也是最複雜的一個環節。通過解碼,壓縮編碼的視頻數據輸出成爲非壓縮的顏色數據,例如YUV420P,RGB等等;壓縮編碼的音頻數據輸出成爲非壓縮的音頻抽樣數據,例如PCM數據。
  • 視音頻同步的作用,就是根據解封裝模塊處理過程中獲取到的參數信息,同步解碼出來的視頻和音頻數據,並將視頻音頻數據送至系統的顯卡和聲卡播放出來。

一般來說,視頻同步指的是視頻和音頻同步,也就是說播放的聲音要和當前顯示的畫面保持一致。想象以下,看一部電影的時候只看到人物嘴動沒有聲音傳出;或者畫面是激烈的戰鬥場景,而聲音不是槍炮聲卻是人物說話的聲音,這是非常差的一種體驗。在視頻流和音頻流中已包含了其以怎樣的速度播放的相關數據,視頻的幀率(Frame Rate)指示視頻一秒顯示的幀數(圖像數);音頻的採樣率(Sample Rate)表示音頻一秒播放的樣本(Sample)的個數。可以使用以上數據通過簡單的計算得到其在某一Frame(Sample)的播放時間,以這樣的速度音頻和視頻各自播放互不影響,在理想條件下,其應該是同步的,不會出現偏差。但,理想條件是什麼大家都懂得。如果用上面那種簡單的計算方式,慢慢的就會出現音視頻不同步的情況。要不是視頻播放快了,要麼是音頻播放快了,很難準確的同步。這就需要一種隨着時間會線性增長的量,視頻和音頻的播放速度都以該量爲標準,播放快了就減慢播放速度;播放快了就加快播放的速度。所以呢,視頻和音頻的同步實際上是一個動態的過程,同步是暫時的,不同步則是常態。以選擇的播放速度量爲標準,快的等待慢的,慢的則加快速度,是一個你等我趕的過程。

播放速度標準量的的選擇一般來說有以下三種:

將視頻同步到音頻上,就是以音頻的播放速度爲基準來同步視頻。視頻比音頻播放慢了,加快其播放速度;快了,則延遲播放。
將音頻同步到視頻上,就是以視頻的播放速度爲基準來同步音頻。
將視頻和音頻同步外部的時鐘上,選擇一個外部時鐘爲基準,視頻和音頻的播放速度都以該時鐘爲標準。


作者:城市之光
鏈接:https://www.jianshu.com/p/907bfcaa9a59
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章