Ffmpeg快速應用開發

ljj_software發表於2009-02-17

轉自:From Ffmpeg工程組

Jump to: navigation, search

從這一步開始,這裡放幾個簡單的例子,手把手知道初學者馬上進入開發狀態

Ffmpeg 中的Libavformat 和 libavcodec庫是訪問大多數視訊檔案格式的一個很好的方法。不幸的是,在開發您自己的程式時,這套庫基本上沒有提供什麼實際的文件可以用來作為參考(至少我沒有找到任何文件),並且它的例程也並沒有太多的幫助。

這種情況意味著,當我在最近某個專案中需要用到 libavformat/libavcodec 庫時,需要作很多試驗來搞清楚怎樣使用它們。這裡是我所學習的--希望我做的這些能夠幫助一些人,以免他們重蹈我的覆轍,作同樣的試驗,遇到同樣的錯誤。你還可以從這裡下載一個demo程式。我將要公開的這部分程式碼需要0.4.8 版本的ffmpeg庫中的 libavformat/libavcodec 的支援(我正在寫最新版本)。如果您發現以後的版本與我寫的程式不能相容,請告知我。

在這個文件裡,我僅僅涉及到如何從檔案中讀入視訊流;音訊流使用幾乎同樣的方法可以工作的很好,不過,我並沒有實際使用過它們,所以,我沒於辦法提供任何示例程式碼。

或許您會覺得奇怪,為什麼需要兩個庫檔案 libavformat 和 libavcodec :許多視訊檔案格式(AVI就是一個最好的例子)實際上並沒有明確指出應該使用哪種編碼來解析音訊和視訊資料;它們只是定義了音訊流和視訊流(或者,有可能是多個音訊視訊流)如何被繫結在一個檔案裡面。這就是為什麼有時候,當你開啟了一個AVI檔案時,你只能聽到聲音,卻不能看到圖象--因為你的系統沒有安裝合適的視訊解碼器。所以, libavformat 用來處理解析視訊檔案並將包含在其中的流分離出來, 而libavcodec 則處理原始音訊和視訊流的解碼。

開啟視訊檔案:首先第一件事情--讓我們來看看怎樣開啟一個視訊檔案並從中得到流。我們要做的第一件事情就是初始化libavformat/libavcodec:

av_register_all(); 這一步註冊庫中含有的所有可用的檔案格式和編碼器,這樣當開啟一個檔案時,它們才能夠自動選擇相應的檔案格式和編碼器。要注意你只需呼叫一次av_register_all(),所以,儘可能的在你的初始程式碼中使用它。如果你願意,你可以僅僅註冊個人的檔案格式和編碼,不過,通常你不得不這麼做卻沒有什麼原因。

下一步,開啟檔案: AVFormatContext *pFormatCtx; const char *filename="myvideo.mpg"; // 開啟視訊檔案 if(av_open_input_file(&pFormatCtx, filename, NULL, 0, NULL)!=0)

   handle_error(); // 不能開啟此檔案


最後三個引數描述了檔案格式,緩衝區大小(size)和格式引數;我們通過簡單地指明NULL或0告訴 libavformat 去自動探測檔案格式並且使用預設的緩衝區大小。請在你的程式中用合適的出錯處理函式替換掉handle_error()。下一步,我們需要取出包含在檔案中的流資訊: // 取出流資訊 if(av_find_stream_info(pFormatCtx)<0)

   handle_error(); // 不能夠找到流資訊

這一步會用有效的資訊把 AVFormatContext 的流域(streams field)填滿。作為一個可除錯的診斷,我們會將這些資訊全盤輸出到標準錯誤輸出中,不過你在一個應用程式的產品中並不用這麼做: dump_format(pFormatCtx, 0, filename, false);

就像在引言中提到的那樣,我們僅僅處理視訊流,而不是音訊流。為了讓這件事情更容易理解,我們只簡單使用我們發現的第一種視訊流:

int i, videoStream; AVCodecContext *pCodecCtx; // 尋找第一個視訊流 videoStream=-1; for(i=0; i<pFormatCtx->nb_streams; i++)

   if(pFormatCtx->streams->codec.codec_type==CODEC_TYPE_VIDEO)
   {
       videoStream=i;
       break;
   }

if(videoStream==-1)

   handle_error(); // Didn't find a video stream

// 得到視訊流編碼上下文的指標 pCodecCtx=&pFormatCtx->streams[videoStream]->codec;

好了,我們已經得到了一個指向視訊流的稱之為上下文的指標。但是我們仍然需要找到真正的編碼器開啟它。

AVCodec *pCodec;

// 尋找視訊流的解碼器 pCodec=avcodec_find_decoder(pCodecCtx->codec_id); if(pCodec==NULL)

   handle_error(); // 找不到解碼器

// 通知解碼器我們能夠處理截斷的bit流--ie, // bit流幀邊界可以在包中 if(pCodec->capabilities & CODEC_CAP_TRUNCATED)

   pCodecCtx->flags|=CODEC_FLAG_TRUNCATED;

// 開啟解碼器 if(avcodec_open(pCodecCtx, pCodec)<0)

   handle_error(); // 打不開解碼器

(那麼什麼是“截斷bit流”?好的,就像一會我們看到的,視訊流中的資料是被分割放入包中的。因為每個視訊幀的資料的大小是可變的,那麼兩幀之間的邊界就不一定剛好是包的邊界。這裡,我們告知解碼器我們可以處理bit流。)


儲存在 AVCodecContext結構中的一個重要的資訊就是視訊幀速率。為了允許非整數的幀速率(比如 NTSC的 29.97幀),速率以分數的形式儲存,分子在 pCodecCtx->frame_rate,分母在 pCodecCtx->frame_rate_base 中。在用不同的視訊檔案測試庫時,我注意到一些編碼器(很顯然ASF)似乎並不能正確的給予賦值( frame_rate_base 用1代替1000)。下面給出修復補丁:

// 加入這句話來糾正某些編碼器產生的幀速錯誤 if(pCodecCtx->frame_rate>1000 && pCodecCtx->frame_rate_base==1)

   pCodecCtx->frame_rate_base=1000;

注意即使將來這個bug解決了,留下這幾句話也並沒有什麼壞處。視訊不可能擁有超過1000fps的幀速。

只剩下一件事情要做了:給視訊幀分配空間以便儲存解碼後的圖片:

AVFrame *pFrame;

pFrame=avcodec_alloc_frame();

就這樣,現在我們開始解碼這些視訊。

解碼視訊幀就像我前面提到過的,視訊檔案包含數個音訊和視訊流,並且他們各個獨自被分開儲存在固定大小的包裡。我們要做的就是使用libavformat依次讀取這些包,過濾掉所有那些視訊流中我們不感興趣的部分,並把它們交給 libavcodec 進行解碼處理。在做這件事情時,我們要注意這樣一個事實,兩幀之間的邊界也可以在包的中間部分。聽起來很複雜?幸運的是,我們在一個例程中封裝了整個過程,它僅僅返回下一幀:

bool GetNextFrame(AVFormatContext *pFormatCtx, AVCodecContext *pCodecCtx,

   int videoStream, AVFrame *pFrame)

{

   static AVPacket packet;
   static int      bytesRemaining=0;
   static uint8_t  *rawData;
   static bool     fFirstTime=true;
   Int bytesDecoded;
   Int frameFinished;

// 我們第一次呼叫時,將 packet.data 設定為NULL指明它不用釋放了

   if(fFirstTime)
   {
       fFirstTime=false;
       packet.data=NULL;
   }

// 解碼直到成功解碼完整的一幀

   while(true)
   {
        //  除非解碼完畢,否則一直在當前包中工作
       while(bytesRemaining > 0)
       {
       //  解碼下一塊資料
           bytesDecoded=avcodec_decode_video(pCodecCtx, pFrame,
               &frameFinished, rawData, bytesRemaining);
               // 出錯了?
           if(bytesDecoded < 0)
           {
               fprintf(stderr, "Error while decoding frame/n");
               return false;
           }
           bytesRemaining-=bytesDecoded;
           rawData+=bytesDecoded;
               // 我們完成當前幀了嗎?接著我們返回
           if(frameFinished)
               return true;
       }
       // 讀取下一包,跳過所有不屬於這個流的包
       do
       {
           // 釋放舊的包
           if(packet.data!=NULL)
               av_free_packet(&packet);
           // 讀取新的包
           if(av_read_packet(pFormatCtx, &packet)<0)
               goto loop_exit;
       } while(packet.stream_index!=videoStream);
       bytesRemaining=packet.size;
       rawData=packet.data;
   }

loop_exit:

       // 解碼最後一幀的餘下部分
   bytesDecoded=avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, 
       rawData, bytesRemaining);
       // 釋放最後一個包
   if(packet.data!=NULL)
       av_free_packet(&packet);
   return frameFinished!=0;

}

現在,我們要做的就是在一個迴圈中,呼叫 GetNextFrame () 直到它返回false。還有一處需要注意:大多數編碼器返回 YUV 420 格式的圖片(一個亮度和兩個色度通道,色度通道只佔亮度通道空間解析度的一半(譯者注:此句原句為the chrominance channels samples at half the spatial resolution of the luminance channel))。看你打算如何對視訊資料處理,或許你打算將它轉換至RGB格式。(注意,儘管,如果你只是打算顯示視訊資料,那大可不必要這麼做;檢視一下 X11 的 Xvideo 擴充套件,它可以在硬體層進行 YUV到RGB 轉換。)幸運的是, libavcodec 提供給我們了一個轉換例程 img_convert ,它可以像轉換其他圖象進行 YUV 和 RGB之間的轉換。這樣解碼視訊的迴圈就變成這樣:

while(GetNextFrame(pFormatCtx, pCodecCtx, videoStream, pFrame)) {

   img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, (AVPicture*)pFrame, 
       pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
   // 處理視訊幀(存檔等等)
   DoSomethingWithTheImage(pFrameRGB);

}

RGB圖象pFrameRGB (AVFrame *型別)的空間分配如下:

AVFrame *pFrameRGB; int numBytes; uint8_t *buffer;

// 分配一個AVFrame 結構的空間 pFrameRGB=avcodec_alloc_frame(); if(pFrameRGB==NULL)

   handle_error();

// 確認所需緩衝區大小並且分配緩衝區空間 numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,

   pCodecCtx->height);

buffer=new uint8_t[numBytes];

// 在pFrameRGB中給圖象位面賦予合適的緩衝區 avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,

   pCodecCtx->width, pCodecCtx->height);

清除好了,我們已經處理了我們的視訊,現在需要做的就是清除我們自己的東西: // 釋放 RGB 圖象 delete [] buffer; av_free(pFrameRGB);

// 釋放YUV 幀 av_free(pFrame);

// 關閉解碼器(codec) avcodec_close(pCodecCtx);

// 關閉視訊檔案 av_close_input_file(pFormatCtx);

完成!更新(2005年4月26號):有個讀者提出:在 Kanotix (一個 Debian 的發行版)上面編譯本例程,或者直接在 Debian 上面編譯,標頭檔案中avcodec.h 和avformat.h 需要加上字首“ffmpeg”,就像這樣:

  1. include <ffmpeg/avcodec.h>
  2. include <ffmpeg/avformat.h>

同樣的, libdts 庫在編譯程式時也要像下面這樣加入進來:

g++ -o avcodec_sample.0.4.9 avcodec_sample.0.4.9.cpp -lavformat -lavcodec -ldts -lz

幾個月前,我寫了一篇有關使用ffmpeg下libavformat 和 libavcodec庫的文章。從那以來,我收到過一些評論,並且新的ffmpeg預發行版(0.4.9-pre1) 最近也要出來了,增加了對在視訊檔案中定位的支援,新的檔案格式,和簡單的讀取視訊幀的介面。這些改變不久就會應用到CVS中,不過這次是我第一次在發行版中看到它們。(順便感謝 Silviu Minut 共享長時間學習CVS版的ffmpeg的成果--他的有關ffmpeg的資訊和demo程式在這裡。)

在這篇文章裡,我僅僅會描述一下以前的版本(0.4.8)和最新版本之間的區別,所以,如果你是採用新的 libavformat / libavcodec ,我建議你讀前面的文章。

首先,說說有關編譯新發行版吧。用我的編譯器( SuSE 上的 gcc 3.3.1 ),在編譯原始檔 ffv1.c 時會報一個編譯器內部的錯誤。我懷疑這是個精簡版的gcc--我在編譯 OpenCV 時也遇到了同樣的事情--但是不論如何,一個快速的解決方法就是在編譯此檔案時不要加優化引數。最簡單的方法就是作一個make,當編譯時遇到編譯器錯誤,進入 libavcodec 子目錄(因為這也是 ffv1.c 所在之處),在你的終端中使用gcc命令去編譯ffv1.c,貼上,編輯刪除編譯器開關(譯者注:就是引數)"-O3",然後使用那個命令執行gcc。然後,你可以變回ffmpeg主目錄並且重新執行make,這次應該可以編譯了。

都有哪些更新?有那些更新呢?從一個程式設計師的角度來看,最大的變化就是儘可能的簡化了從視訊檔案中讀取個人的視訊幀的操作。在ffmpeg 0.4.8 和其早期版本中,在從一個視訊檔案中的包中用例程av_read_packet()來讀取資料時,一個視訊幀的資訊通常可以包含在幾個包裡,而另情況更為複雜的是,實際上兩幀之間的邊界還可以存在於兩個包之間。幸虧ffmpeg 0.4.9 引入了新的叫做av_read_frame()的例程,它可以從一個簡單的包裡返回一個視訊幀包含的所有資料。使用av_read_packet()讀取視訊資料的老辦法仍然支援,但是不贊成使用--我說:擺脫它是可喜的。

這裡讓我們來看看如何使用新的API來讀取視訊資料。在我原來的文章中(與 0.4.8 API相關),主要的解碼迴圈就像下面這樣:

while(GetNextFrame(pFormatCtx, pCodecCtx, videoStream, pFrame)) {

   img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, (AVPicture*)pFrame, 
       pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
   // 處理視訊幀(存檔等等)
   DoSomethingWithTheImage(pFrameRGB);

}

GetNextFrame() 是個有幫助的例程,它可以處理這樣一個過程,這個過程彙編一個完整的視訊幀所需要的所有的包。新的API簡化了我們在主迴圈中實際直接讀取和解碼資料的操作:

while(av_read_frame(pFormatCtx, &packet)>=0) {

   // 這是視訊流中的一個包嗎?
   if(packet.stream_index==videoStream)
   {
       // 解碼視訊流
       avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, 
           packet.data, packet.size);
       // 我們得到一幀了嗎?
       if(frameFinished)
       {
           // 把原始影象轉換成 RGB
           img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, 
               (AVPicture*)pFrame, pCodecCtx->pix_fmt, pCodecCtx->width, 
               pCodecCtx->height);
           // 處理視訊幀(存檔等等)
           DoSomethingWithTheImage(pFrameRGB);
       }
   }
   // 釋放用av_read_frame分配空間的包
   av_free_packet(&packet);

}

看第一眼,似乎看上去變得更為複雜了。但那僅僅是因為這塊程式碼做的都是要隱藏在GetNextFrame()例程中實現的(檢查包是否屬於視訊流,解碼幀並釋放包)。總的說來,因為我們能夠完全排除 GetNextFrame (),事情變得更簡單了。我已經更新了demo程式使用最新的API。簡單比較一下行數(老版本222行 Vs新版本169行)顯示出新的API大大的簡化了這件事情。

0.4.9的另一個重要的更新是能夠在視訊檔案中定位一個時間戳。它通過函式av_seek_frame() 來實現,此函式有三個引數:一個指向 AVFormatContext 的指標,一個流索引和定位時間戳。此函式在給定時間戳以前會去定位第一個關鍵幀。所有這些都來自於文件。我並沒有對av_seek_frame()進行測試,所以這裡我並不能夠給出任何示例程式碼。如果你成功的使用av_seek_frame() ,我很高興聽到這個訊息。

捕獲視訊(Video4Linux and IEEE1394) Toru Tamaki 發給我了一些使用 libavformat / libavcodec 庫從 Video4Linux 或者 IEEE1394 視訊裝置源中抓捕視訊幀的樣例程式碼。對 Video4Linux,呼叫av_open_input_file() 函式應該修改如下: AVFormatParameters formatParams; AVInputFormat *iformat;

formatParams.device = "/dev/video0"; formatParams.channel = 0; formatParams.standard = "ntsc"; formatParams.width = 640; formatParams.height = 480; formatParams.frame_rate = 29; formatParams.frame_rate_base = 1; filename = ""; iformat = av_find_input_format("video4linux");

av_open_input_file(&ffmpegFormatContext,

                filename, iformat, 0, &formatParams);

For IEEE1394, call av_open_input_file() like this:

AVFormatParameters formatParams; AVInputFormat *iformat;

formatParams.device = "/dev/dv1394"; filename = ""; iformat = av_find_input_format("dv1394");

av_open_input_file(&ffmpegFormatContext,

                filename, iformat, 0, &formatParams);

繼續。。。如果我碰巧遇到了一些有關 libavformat / libavcodec 的有趣的資訊,我計劃在這裡公佈。所以,如果你有任何的評論,請通過這篇文章頂部給出的地址聯絡我。標準棄權:我沒有責任去糾正這些程式碼的功能和這篇文章中涉及的技術。

注:本文為譯文,與原文相比,遺漏了獲取媒體檔案資訊相關部分,完整的資訊請參考原文: http://www.inb.uni-luebeck.de/~boehme/using_libavcodec.html ,感謝wotobo在論壇:http://bbs.chinavideo.org/viewthread.php?tid=3312&extra=page%3D1 指正該問題。

相關文章