學習FFmpeg API – 解碼視訊

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

ffmpeg是編解碼的利器,用了很久,以前看過dranger 的教程,非常精彩,受益頗多,是學習ffmpeg api很好的材料。可惜的是其針對的ffmpeg版本已經比較老了,而ffmpeg的更新又很快,有些API已經完全換掉了,導致dranger教程中的 程式碼已經無法編譯,正好最近需要使用ffmpeg,於是就利用dranger的教程和程式碼,自己邊學邊記錄,於是也就有了這個所謂的 New FFmpeg Tutorial,希望對學習ffmpeg的人有所幫助。

Tutorial 1: Decoding video frames

source code:videoframe.c

視訊播放過程

首先簡單介紹以下視訊檔案的相關知識。我們平時看到的視訊檔案有許多格式,比如 avi, mkv, rmvb, mov, mp4等等,這些被稱為容器Container), 不同的容器格式規定了其中音視訊資料的組織方式(也包括其他資料,比如字幕等)。容器中一般會封裝有視訊和音訊軌,也稱為視訊流(stream)和音訊 流,播放視訊檔案的第一步就是根據視訊檔案的格式,解析(demux)出其中封裝的視訊流、音訊流以及字幕(如果有的話),解析的資料讀到包 (packet)中,每個包裡儲存的是視訊幀(frame)或音訊幀,然後分別對視訊幀和音訊幀呼叫相應的解碼器(decoder)進行解碼,比如使用 H.264編碼的視訊和MP3編碼的音訊,會相應的呼叫H.264解碼器和MP3解碼器,解碼之後得到的就是原始的影象(YUV or RGB)和聲音(PCM)資料,然後根據同步好的時間將影象顯示到螢幕上,將聲音輸出到音效卡,最終就是我們看到的視訊。

FFmpeg的API就是根據這個過程設計的,因此使用FFmpeg來處理視訊檔案的方法非常直觀簡單。下面就一步一步介紹從視訊檔案中解碼出圖片的過程。

宣告變數

首先定義整個過程中需要使用到的變數:

int main(int argc, const char *argv[])
{
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream;
  AVCodecContext  *pCodecCtx;
  AVCodec         *pCodec;
  AVFrame         *pFrame;
  AVFrame         *pFrameRGB;
  AVPacket        packet;
  int             frameFinished;
  int             numBytes;
  uint8_t         *buffer;

AVFormatContext:儲存需要讀入的檔案的格式資訊,比如流的個數以及流資料等

AVCodecCotext:儲存了相應流的詳細編碼資訊,比如視訊的寬、高,編碼型別等。

pCodec:真正的編解碼器,其中有編解碼需要呼叫的函式

AVFrame:用於儲存資料幀的資料結構,這裡的兩個幀分別是儲存顏色轉換前後的兩幀影象

AVPacket:解析檔案時會將音/視訊幀讀入到packet中

開啟檔案

接下來我們開啟一個視訊檔案。

av_register_all();

av_register_all 定義在 libavformat 裡,呼叫它用以註冊所有支援的檔案格式以及編解碼器,從其實現程式碼裡可以看到它會呼叫 avcodec_register_all,因此之後就可以用所有ffmpeg支援的codec了。

if( avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0 )
    return -1;

使用新的API avformat_open_input來開啟一個檔案,第一個引數是一個AVFormatContext指標變數的地址,它會根據開啟的檔案資訊填充AVFormatContext,需要注意的是,此處的pFormatContext必須為NULL或由avformat_alloc_context分配得到,這也是上一節中將其初始化為NULL的原因,否則此函式呼叫會出問題。第二個引數是開啟的檔名,通過argv[1]指定,也就是命令列的第一個引數。後兩個引數分別用於指定特定的輸入格式(AVInputFormat)以及指定檔案開啟額外引數的AVDictionary結構,這裡均留作NULL。

if( avformat_find_stream_info(pFormatCtx, NULL ) < 0 )
    return -1;
 
  av_dump_format(pFormatCtx, -1, argv[1], 0);

avformat_open_input函式只是讀檔案頭,並不會填充流資訊,因此我們需要接下來呼叫avformat_find_stream_info獲取檔案中的流資訊,此函式會讀取packet,並確定檔案中所有的流資訊,設定pFormatCtx->streams指向檔案中的流,但此函式並不會改變檔案指標,讀取的packet會給後面的解碼進行處理。
最後呼叫一個幫助函式av_dump_format,輸出檔案的資訊,也就是我們在使用ffmpeg時能看到的檔案詳細資訊。第二個引數指定輸出哪條流的資訊,-1表示給ffmpeg自己選擇。最後一個引數用於指定dump的是不是輸出檔案,我們dump的是輸入檔案,因此一定要是0。

現在 pFormatCtx->streams 中已經有所有流了,因此現在我們遍歷它找到第一條視訊流:

  videoStream = -1;
  for( 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;

codec_type 的巨集定義已經由以前的 CODEC_TYPE_VIDEO 改為 AVMEDIA_TYPE_VIDEO 了。接下來我們通過這條 video stream 的編解碼資訊開啟相應的解碼器:

  pCodecCtx = pFormatCtx->streams[videoStream]->codec;
 
  pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
  if( pCodec == NULL )
    return -1;
 
  if( avcodec_open2(pCodecCtx, pCodec, NULL) < 0 )
    return -1;

分配影象快取

接下來我們準備給即將解碼的圖片分配記憶體空間。

pFrame = avcodec_alloc_frame();
  if( pFrame == NULL )
    return -1;
 
  pFrameRGB = avcodec_alloc_frame();
  if( pFrameRGB == NULL )
    return -1;

呼叫 avcodec_alloc_frame 分配幀,因為最後我們會將影象寫成 24-bits RGB 的 PPM 檔案,因此這裡需要兩個 AVFrame,pFrame用於儲存解碼後的資料,pFrameRGB用於儲存轉換後的資料:

  numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);

這裡呼叫 avpicture_get_size,根據 pCodecCtx 中原始影象的寬高計算 RGB24 格式的影象需要佔用的空間大小,這是為了之後給 pFrameRGB 分配空間:

buffer = av_malloc(numBytes);
 
  avpicture_fill( (AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
  pCodecCtx->width, pCodecCtx->height);

接著上面的,首先是用 av_malloc 分配上面計算大小的記憶體空間,然後呼叫 avpicture_fill 將 pFrameRGB 跟 buffer 指向的記憶體關聯起來。

獲取影象

OK,一切準備好就可以開始從檔案中讀取視訊幀並解碼得到影象了。

  i = 0;
  while( av_read_frame(pFormatCtx, &packet) >= 0 ) {
    if( packet.stream_index == videoStream ) {
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
 
      if( frameFinished ) {
struct SwsContext *img_convert_ctx = NULL;
img_convert_ctx = 
  sws_getCachedContext(img_convert_ctx, pCodecCtx->width,
       pCodecCtx->height, pCodecCtx->pix_fmt,
       pCodecCtx->width, pCodecCtx->height,
       PIX_FMT_RGB24, SWS_BICUBIC,
       NULL, NULL, NULL);
if( !img_convert_ctx ) {
  fprintf(stderr, "Cannot initialize sws conversion context\n");
  exit(1);
}
sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data,
  pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
  pFrameRGB->linesize);
if( i   < 50 )
  SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
      }
    }
    av_free_packet(&packet);
  }

av_read_frame 從檔案中讀取一個packet,對於視訊來說一個packet裡面包含一幀影象資料,音訊可能包含多個幀(當音訊幀長度固定時),讀到這一幀後,如果是視訊幀,則使用 avcodec_decode_video2 對packet中的幀進行解碼,有時候解碼器並不能從一個packet中解碼得到一幀影象資料(比如在需要其他參考幀的情況下),因此會設定 frameFinished,如果已經得到下一幀影象則設定 frameFinished 非零,否則為零。所以這裡我們判斷 frameFinished 是否為零來確定 pFrame 中是否已經得到解碼的影象。注意在每次處理完後需要呼叫 av_free_packet 釋放讀取的packet。

解碼得到影象後,很有可能不是我們想要的 RGB24 格式,因此需要使用 swscale 來做轉換,呼叫 sws_getCachedContext 得到轉換上下文,使用 sws_scale 將圖形從解碼後的格式轉換為 RGB24,最後將前50幀寫人 ppm 檔案。最後釋放影象以及關閉檔案:

 av_free(buffer);
  av_free(pFrameRGB);
  av_free(pFrame);
  avcodec_close(pCodecCtx);
  avformat_close_input(&pFormatCtx);
 
  return 0;
}
 
static void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame)
{
  FILE *pFile;
  char szFilename[32];
  int y;
 
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile = fopen(szFilename, "wb");
  if( !pFile )
    return;
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);
 
  for( y = 0; y < height; y   )
    fwrite(pFrame->data[0]   y * pFrame->linesize[0], 1, width * 3, pFile);
 
  fclose(pFile);
}


(adsbygoogle = window.adsbygoogle || []).push({});

function googleAdJSAtOnload() {
var element = document.createElement(“script”);
element.src = “//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js”;
element.async = true;
document.body.appendChild(element);
}
if (window.addEventListener) {
window.addEventListener(“load”, googleAdJSAtOnload, false);
} else if (window.attachEvent) {
window.attachEvent(“onload”, googleAdJSAtOnload);
} else {
window.onload = googleAdJSAtOnload;
}

人工智慧 最新文章