學習FFmpeg API

學習FFmpeg API – 解碼視頻

 

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來處理視頻文件的方法非常直觀簡單。下面就一步一步介紹從視頻文件中解碼出圖片的過程。

聲明變量

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

01 int main(int argc, const char *argv[])
02 {
03   AVFormatContext *pFormatCtx = NULL;
04   int             i, videoStream;
05   AVCodecContext  *pCodecCtx;
06   AVCodec         *pCodec;
07   AVFrame         *pFrame;
08   AVFrame         *pFrameRGB;
09   AVPacket        packet;
10   int             frameFinished;
11   int             numBytes;
12   uint8_t         *buffer;

  • AVFormatContext:保存需要讀入的文件的格式信息,比如流的個數以及流數據等
  • AVCodecCotext:保存了相應流的詳細編碼信息,比如視頻的寬、高,編碼類型等。
  • pCodec:真正的編解碼器,其中有編解碼需要調用的函數
  • AVFrame:用於保存數據幀的數據結構,這裏的兩個幀分別是保存顏色轉換前後的兩幀圖像
  • AVPacket:解析文件時會將音/視頻幀讀入到packet中

打開文件

接下來我們打開一個視頻文件。

1 av_register_all();

av_register_all 定義在 libavformat 裏,調用它用以註冊所有支持的文件格式以及編解碼器,從其實現代碼裏可以看到它會調用 avcodec_register_all,因此之後就可以用所有ffmpeg支持的codec了。

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

使用新的API avformat_open_input來打開一個文件,第一個參數是一個AVFormatContext指針變量的地址,它會根據打開的文件信息填充AVFormatContext,需要注意的是,此處的pFormatContext必須爲NULL或由avformat_alloc_context分配得到,這也是上一節中將其初始化爲NULL的原因,否則此函數調用會出問題。第二個參數是打開的文件名,通過argv[1]指定,也就是命令行的第一個參數。後兩個參數分別用於指定特定的輸入格式(AVInputFormat)以及指定文件打開額外參數的AVDictionary結構,這裏均留作NULL。

1 if( avformat_find_stream_info(pFormatCtx, NULL ) < 0 )
2     return -1;
3
4   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 中已經有所有流了,因此現在我們遍歷它找到第一條視頻流:

1 videoStream = -1;
2 for( i = 0; i < pFormatCtx->nb_streams; i++ )
3   if( pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
4     videoStream = i;
5     break;
6   }
7  
8 if( videoStream == -1 )
9   return -1;

codec_type 的宏定義已經由以前的 CODEC_TYPE_VIDEO 改爲 AVMEDIA_TYPE_VIDEO 了。接下來我們通過這條 video stream 的編解碼信息打開相應的解碼器:

1 pCodecCtx = pFormatCtx->streams[videoStream]->codec;
2  
3 pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
4 if( pCodec == NULL )
5   return -1;
6  
7 if( avcodec_open2(pCodecCtx, pCodec, NULL) < 0 )
8   return -1;

分配圖像緩存

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

1 pFrame = avcodec_alloc_frame();
2   if( pFrame == NULL )
3     return -1;
4
5   pFrameRGB = avcodec_alloc_frame();
6   if( pFrameRGB == NULL )
7     return -1;

調用 avcodec_alloc_frame 分配幀,因爲最後我們會將圖像寫成 24-bits RGB 的 PPM 文件,因此這裏需要兩個 AVFrame,pFrame用於存儲解碼後的數據,pFrameRGB用於存儲轉換後的數據:

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

這裏調用 avpicture_get_size,根據 pCodecCtx 中原始圖像的寬高計算 RGB24 格式的圖像需要佔用的空間大小,這是爲了之後給 pFrameRGB 分配空間:

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

接着上面的,首先是用 av_malloc 分配上面計算大小的內存空間,然後調用 avpicture_fill 將 pFrameRGB 跟 buffer 指向的內存關聯起來。

獲取圖像

OK,一切準備好就可以開始從文件中讀取視頻幀並解碼得到圖像了。

01 i = 0;
02 while( av_read_frame(pFormatCtx, &packet) >= 0 ) {
03   if( packet.stream_index == videoStream ) {
04     avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
05  
06     if( frameFinished ) {
07   struct SwsContext *img_convert_ctx = NULL;
08   img_convert_ctx =
09     sws_getCachedContext(img_convert_ctx, pCodecCtx->width,
10                  pCodecCtx->height, pCodecCtx->pix_fmt,
11                  pCodecCtx->width, pCodecCtx->height,
12                  PIX_FMT_RGB24, SWS_BICUBIC,
13                  NULL, NULL, NULL);
14   if( !img_convert_ctx ) {
15     fprintf(stderr, "Cannot initialize sws conversion context\n");
16     exit(1);
17   }
18   sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data,
19         pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
20         pFrameRGB->linesize);
21   if( i++ < 50 )
22     SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
23     }
24   }
25   av_free_packet(&packet);
26 }

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 文件。最後釋放圖像以及關閉文件:

01 av_free(buffer);
02   av_free(pFrameRGB);
03   av_free(pFrame);
04   avcodec_close(pCodecCtx);
05   avformat_close_input(&pFormatCtx);
06
07   return 0;
08 }
09
10 static void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame)
11 {
12   FILE *pFile;
13   char szFilename[32];
14   int y;
15
16   sprintf(szFilename, "frame%d.ppm", iFrame);
17   pFile = fopen(szFilename, "wb");
18   if( !pFile )
19     return;
20   fprintf(pFile, "P6\n%d %d\n255\n", width, height);
21
22   for( y = 0; y < height; y++ )
23     fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
24
25   fclose(pFile);
26
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章