1. 程式人生 > >ffmpeg入門學習——文件1:製作螢幕錄影

ffmpeg入門學習——文件1:製作螢幕錄影

指導1:製作螢幕錄影

原始碼:tutorial01.c

概要

電影檔案有很多基本的組成部分。首先,檔案本身被稱為容器Container,容器的型別決定了資訊被存放在檔案中的位置。AVI和Quicktime就是容器的例子。接著,你有一組,例如,你經常有的是一個音訊流和一個視訊流。(一個流只是一種想像出來的詞語,用來表示一連串的通過時間來串連的資料元素)。在流中的資料元素被稱為幀Frame。 每個流是由不同的編碼器來編碼生成的。編解碼器描述了實際的資料是如何被編碼Coded和解碼DECoded的,因此它的名字叫做CODEC。Divx和 MP3就是編解碼器的例子。接著從流中被讀出來的叫做包Packets

。包是一段資料,它包含了一段可以被解碼成方便我們最後在應用程式中操作的原始幀的 資料。根據我們的目的,每個包包含了完整的幀或者對於音訊來說是許多格式的完整幀。

基本上來說,處理視訊和音訊流是很容易的:

10 從video.avi檔案中開啟視訊流video_stream

20 從視訊流中讀取包到幀中

30 如果這個幀還不完整,跳到20

40 對這個幀進行一些操作

50 跳回到20

在這個程式中使用ffmpeg來處理多種媒體是相當容易的,雖然很多程式可能在對幀進行操作的時候非常的複雜。因此在這篇指導中,我們將開啟一個檔案,讀取裡面的視訊流,而且我們對幀的操作將是把這個幀寫到一個PPM檔案中。

1、註冊

首先,來看一下我們如何開啟一個檔案。通過ffmpeg,你必需先初始化這個庫。(注意在某些系統中必需用<ffmpeg/avcodec.h>和<ffmpeg/avformat.h>來替換)

#include <avcodec.h>

#include <avformat.h>

...

int main(int argc, charg *argv[]) {

av_register_all();//註冊了所有的檔案格式和編解碼器的庫

這裡註冊了所有的檔案格式和編解碼器的庫,所以它們將被自動的使用在被開啟的合適格式的檔案上。注意你只需要呼叫av_register_all()一 次,因此我們在主函式main()中來呼叫它。如果你喜歡,也可以只註冊特定的格式和編解碼器,但是通常你沒有必要這樣做。

2、開啟檔案

現在我們可以真正的開啟檔案:

AVFormatContext *pFormatCtx;

// Open video file

if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)//開啟一個媒體檔案,存放到pFormatCtx中,只讀取了檔案的頭資訊

return -1; // Couldn't open file

我們通過第一個引數來獲得檔名。這個函式讀取檔案的頭部並且把資訊儲存到我們給的AVFormatContext結構體中。最後三個引數用來指定特殊的檔案格式,緩衝大小和格式引數,但如果把它們設定為空NULL或者0,libavformat將自動檢測這些引數。

3、從檔案中取出流資訊

上面函式只是檢測了檔案的頭部,所以接著我們需要檢查在檔案中的流的資訊:

// Retrieve stream information

if(av_find_stream_info(pFormatCtx)<0) //取出流資訊,為pFormatCtx->streams填充上正確的資訊

return -1; // Couldn't find stream information

我們引進一個手工除錯的函式來看一下里面有什麼:

// Dump information about file onto standard error

dump_format(pFormatCtx, 0, argv[1], 0);

4、窮舉所有的流

現在pFormatCtx->streams僅僅是一組大小為pFormatCtx->nb_streams的指標,所以讓我們先跳過它直到我們找到一個視訊流。

int i;

AVCodecContext *pCodecCtx;

// Find the first video stream

videoStream=-1;

for(i=0; i<pFormatCtx->nb_streams; i++)

if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) { videoStream=i; break; }

if(videoStream==-1)

return -1; // Didn't find a video stream

// Get a pointer to the codec context for the video stream

pCodecCtx=pFormatCtx->streams[videoStream]->codec;

流中關於編解碼器的資訊就是被我們叫做"codec context"(編解碼器上下文)的東西。這裡麵包含了流中所使用的關於編解碼器的所有資訊,現在我們有了一個指向他的指標。但是我們必需要找到真正的編解碼器並且開啟它

5、尋找視訊流解碼器並開啟解碼器

AVCodec *pCodec;

// Find the decoder for the video stream

pCodec=avcodec_find_decoder(pCodecCtx->codec_id); //找到解碼器pCodec

if(pCodec==NULL) {

fprintf(stderr, "Unsupported codec!\n"); return -1; // Codec not found

}

// Open codec

if(avcodec_open(pCodecCtx, pCodec)<0) //開啟解碼器pCodec

return -1; // Could not open codec

有些人可能會從舊的指導中記得有兩個關於這些程式碼其它部分:新增CODEC_FLAG_TRUNCATED到pCodecCtx->flags和添 加一個hack來粗糙的修正幀率。這兩個修正已經不在存在於ffplay.c中。因此,我必需假設它們不再必要。我們移除了那些程式碼後還有一個需要指出的 不同點:pCodecCtx->time_base現在已經儲存了幀率的資訊。time_base是一個結構體,它裡面有一個分子和分母 (AVRational)。我們使用分數的方式來表示幀率是因為很多編解碼器使用非整數的幀率(例如NTSC使用29.97fps)。

6、為解碼幀分配記憶體

現在我們需要找到一個地方來儲存幀:

AVFrame *pFrame;

// Allocate video frame

pFrame=avcodec_alloc_frame();

因為我們準備輸出儲存24位RGB色的PPM檔案,我們必需把幀的格式從原來的轉換為RGB。FFMPEG將為我們做這些轉換。在大多數專案中(包括我們的這個)我們都想把原始的幀轉換成一個特定的格式。讓我們先為轉換來申請一幀的記憶體。

// Allocate an AVFrame structure

pFrameRGB=avcodec_alloc_frame();

if(pFrameRGB==NULL)

return -1;

即使我們申請了一幀的記憶體,當轉換的時候,我們仍然需要一個地方來放置原始的資料。我們使用avpicture_get_size來獲得我們需要的大小,然後手工申請記憶體空間:

uint8_t *buffer;

int numBytes;

// Determine required buffer size and allocate buffer

numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,

pCodecCtx->height);

buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

av_malloc是ffmpeg的malloc,用來實現一個簡單的malloc的包裝,這樣來保證記憶體地址是對齊的(4位元組對齊或者2位元組對齊)。它並不能保護你不被記憶體洩漏,重複釋放或者其它malloc的問題所困擾。

現在我們使用avpicture_fill來把幀和我們新申請的記憶體來結合。關於AVPicture的結成:AVPicture結構體是AVFrame結構體的子集——AVFrame結構體的開始部分與AVPicture結構體是一樣的。

// Assign appropriate parts of buffer to image planes in pFrameRGB

// Note that pFrameRGB is an AVFrame, but AVFrame is a superset

// of AVPicture

avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,

pCodecCtx->width, pCodecCtx->height);

最後,我們已經準備好來從流中讀取資料了。

7、讀取資料

我們將要做的是通過讀取包來讀取整個視訊流,然後把它解碼成幀,最好後轉換格式並且儲存

int frameFinished;

AVPacket packet;

i=0;

while(av_read_frame(pFormatCtx, &packet)>=0) { //從流中讀取包資料放到packet

// Is this a packet from the video stream? if(packet.stream_index==videoStream) { // Decode video frame avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, //把包轉換為幀 packet.data, packet.size); // Did we get a video frame? if(frameFinished) { // Convert the image from its native format to RGB img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, //幀從原始格式(pCodecCtx->pix_fmt轉換成為RGB格式,pFrameRGB儲存轉化後的 (AVPicture*)pFrame, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); // Save the frame to disk if(++i<=5) SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i); //把幀和高度寬度資訊傳遞給我們的SaveFrame函式 } } // Free the packet that was allocated by av_read_frame av_free_packet(&packet);

}

這個迴圈過程是比較簡單的:av_read_frame()讀取一個包並且把它儲存到AVPacket結構體中。注意我們僅僅申請了一個包的結構體 ——ffmpeg為我們申請了內部的資料的記憶體並通過packet.data指標來指向它。這些資料可以在後面通過av_free_packet()來釋 放。函式avcodec_decode_video()把包轉換為幀。然而當解碼一個包的時候,我們可能沒有得到我們需要的關於幀的資訊。因此,當我們得 到下一幀的時候,avcodec_decode_video()為我們設定了幀結束標誌frameFinished。最後,我們使用 img_convert()函式來把幀從原始格式(pCodecCtx->pix_fmt)轉換成為RGB格式。要記住,你可以把一個 AVFrame結構體的指標轉換為AVPicture結構體的指標。最後,我們把幀和高度寬度資訊傳遞給我們的SaveFrame函式。

注:新版本的函式庫裡沒有img_convert函式,在0.4.8以前的版本中還有這個函式,新版本中用sws_getContext和sws_scale代替了

#include <ffmpeg/swscale.h> static struct SwsContext *img_convert_ctx; img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL); sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);

8、儲存資料

現在我們需要做的是讓SaveFrame函式能把RGB資訊定稿到一個PPM格式的檔案中。我們將生成一個簡單的PPM格式檔案,請相信,它是可以工作的。

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {

FILE *pFile; char szFilename[32]; int y; // Open file sprintf(szFilename, "frame%d.ppm", iFrame); pFile=fopen(szFilename, "wb"); if(pFile==NULL) return; // Write header fprintf(pFile, "P6\n%d %d\n255\n", width, height); // Write pixel data for(y=0; y<height; y++) fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile); // Close file fclose(pFile);

}

我們做了一些標準的檔案開啟動作,然後寫入RGB資料。我們一次向檔案寫入一行資料。PPM格式檔案的是一種包含一長串的RGB資料的檔案。如果你瞭解 HTML色彩表示的方式,那麼它就類似於把每個畫素的顏色頭對頭的展開,就像#ff0000#ff0000....就表示了了個紅色的螢幕。(它被儲存成 二進位制方式並且沒有分隔符,但是你自己是知道如何分隔的)。檔案的頭部表示了影象的寬度和高度以及最大的RGB值的大小。

現在,回顧我們的main()函式。一旦我們開始讀取完視訊流,我們必需清理一切:

// Free the RGB image

av_free(buffer);

av_free(pFrameRGB);

// Free the YUV frame

av_free(pFrame);

// Close the codec

avcodec_close(pCodecCtx);

// Close the video file

av_close_input_file(pFormatCtx);

return 0;

你會注意到我們使用av_free來釋放我們使用avcode_alloc_fram和av_malloc來分配的記憶體。

上面的就是程式碼!下面,我們將使用Linux或者其它類似的平臺,你將執行:

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil -lm

如果你使用的是老版本的ffmpeg,你可以去掉-lavutil引數:

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lm

大多數的影象處理函式可以開啟PPM檔案。可以使用一些電影檔案來進行測試。