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[]) |
03 |
AVFormatContext *pFormatCtx = NULL; |
05 |
AVCodecContext *pCodecCtx; |
打開文件
接下來我們打開一個視頻文件。
av_register_all 定義在
libavformat 裏,調用它用以註冊所有支持的文件格式以及編解碼器,從其實現代碼裏可以看到它會調用 avcodec_register_all,因此之後就可以用所有ffmpeg支持的codec了。
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 中已經有所有流了,因此現在我們遍歷它找到第一條視頻流:
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); |