ffmpeg是編解碼的利器,用了好久,之前看過dranger 的教程,很是精彩,受益頗多,是學習ffmpeg api很好的材料。惋惜的是其針對的ffmpeg版本已經比較老了,而ffmpeg的更新又很快,有些API已經徹底換掉了,致使dranger教程中的 代碼已經沒法編譯,正好最近須要使用ffmpeg,因而就利用dranger的教程和代碼,本身邊學邊記錄,因而也就有了這個所謂的 New FFmpeg Tutorial,但願對學習ffmpeg的人有所幫助。php
Tutorial 1: Decoding video frames
source code:videoframe.chtml
視頻播放過程
首先簡單介紹如下視頻文件的相關知識。咱們平時看到的視頻文件有許多格式,好比 avi, mkv, rmvb, mov, mp4等等,這些被稱爲容器(Container), 不一樣的容器格式規定了其中音視頻數據的組織方式(也包括其餘數據,好比字幕等)。容器中通常會封裝有視頻和音頻軌,也稱爲視頻流(stream)和音頻 流,播放視頻文件的第一步就是根據視頻文件的格式,解析(demux)出其中封裝的視頻流、音頻流以及字幕(若是有的話),解析的數據讀到包 (packet)中,每一個包裏保存的是視頻幀(frame)或音頻幀,而後分別對視頻幀和音頻幀調用相應的解碼器(decoder)進行解碼,好比使用 H.264編碼的視頻和MP3編碼的音頻,會相應的調用H.264解碼器和MP3解碼器,解碼以後獲得的就是原始的圖像(YUV or RGB)和聲音(PCM)數據,而後根據同步好的時間將圖像顯示到屏幕上,將聲音輸出到聲卡,最終就是咱們看到的視頻。git
FFmpeg的API就是根據這個過程設計的,所以使用FFmpeg來處理視頻文件的方法很是直觀簡單。下面就一步一步介紹從視頻文件中解碼出圖片的過程。api
聲明變量
首先定義整個過程當中須要使用到的變量:緩存
01 |
int main( int argc, const char *argv[]) |
03 |
AVFormatContext *pFormatCtx = NULL; |
05 |
AVCodecContext *pCodecCtx; |
打開文件
接下來咱們打開一個視頻文件。數據結構
av_register_all 定義在 libavformat 裏,調用它用以註冊全部支持的文件格式以及編解碼器,從其實現代碼裏能夠看到它會調用 avcodec_register_all,所以以後就能夠用全部ffmpeg支持的codec了。ide
1 |
if ( avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0 ) |
使用新的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 ) |
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 中已經有全部流了,所以如今咱們遍歷它找到第一條視頻流:ui
2 |
for ( i = 0; i < pFormatCtx->nb_streams; i++ ) |
3 |
if ( pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) { |
8 |
if ( videoStream == -1 ) |
codec_type 的宏定義已經由之前的 CODEC_TYPE_VIDEO 改成 AVMEDIA_TYPE_VIDEO 了。接下來咱們經過這條 video stream 的編解碼信息打開相應的解碼器:
1 |
pCodecCtx = pFormatCtx->streams[videoStream]->codec; |
3 |
pCodec = avcodec_find_decoder(pCodecCtx->codec_id); |
7 |
if ( avcodec_open2(pCodecCtx, pCodec, NULL) < 0 ) |
分配圖像緩存
接下來咱們準備給即將解碼的圖片分配內存空間。
1 |
pFrame = avcodec_alloc_frame(); |
5 |
pFrameRGB = avcodec_alloc_frame(); |
6 |
if ( pFrameRGB == NULL ) |
調用 avcodec_alloc_frame 分配幀,由於最後咱們會將圖像寫成 24-bits RGB 的 PPM 文件,所以這裏須要兩個 AVFrame,pFrame用於存儲解碼後的數據,pFrameRGB用於存儲轉換後的數據:
1 |
numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width, |
這裏調用 avpicture_get_size,根據 pCodecCtx 中原始圖像的寬高計算 RGB24 格式的圖像須要佔用的空間大小,這是爲了以後給 pFrameRGB 分配空間:
1 |
buffer = av_malloc(numBytes); |
3 |
avpicture_fill( (AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24, |
4 |
pCodecCtx->width, pCodecCtx->height); |
接着上面的,首先是用 av_malloc 分配上面計算大小的內存空間,而後調用 avpicture_fill 將 pFrameRGB 跟 buffer 指向的內存關聯起來。
獲取圖像
OK,一切準備好就能夠開始從文件中讀取視頻幀並解碼獲得圖像了。
02 |
while ( av_read_frame(pFormatCtx, &packet) >= 0 ) { |
03 |
if ( packet.stream_index == videoStream ) { |
04 |
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet); |
07 |
struct SwsContext *img_convert_ctx = NULL; |
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, |
14 |
if ( !img_convert_ctx ) { |
15 |
fprintf (stderr, "Cannot initialize sws conversion context\n" ); |
18 |
sws_scale(img_convert_ctx, ( const uint8_t* const *)pFrame->data, |
19 |
pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, |
22 |
SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i); |
25 |
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 文件。最後釋放圖像以及關閉文件:
04 |
avcodec_close(pCodecCtx); |
05 |
avformat_close_input(&pFormatCtx); |
10 |
static void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) |
16 |
sprintf (szFilename, "frame%d.ppm" , iFrame); |
17 |
pFile = fopen (szFilename, "wb" ); |
20 |
fprintf (pFile, "P6\n%d %d\n255\n" , width, height); |
22 |
for ( y = 0; y < height; y++ ) |
23 |
fwrite (pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile); |