ffmpeg開發指南(使用 libavformat 和 libavcodec)

ffmpeg開發指南(使用 libavformat 和 libavcodec)

Ffmpeg 中的Libavformatlibavcodec是訪問大多數視頻文件格式的一個很好的方法。不幸的是,在開發您本身的程序時,這套庫基本上沒有提供什麼實際的文檔能夠用來做爲參考(至少我沒有找到任何文檔),而且它的例程也並無太多的幫助。

這種狀況意味着,當我在最近某個項目中須要用到 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 linux

*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」,就像這樣:

#include <ffmpeg/avcodec.h>
#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);程序員

相關文章
相關標籤/搜索