概要
電影文件有很多基本的組成部分。首先,文件本身被稱為容器Container,容器的類型決定了信息被存放在文件中的位置。AVI和Quicktime就是容器的例子。接著,你有一組流,例如,你經常有的是一個音頻流和一個視頻流。(一個流只是一種想像出來的詞語,用來表示一連串的通過時間來串連的數據元素)。在流中的數據元素被稱為幀Frame。 每個流是由不同的編碼器來編碼生成的。編解碼器描述了實際的數據是如何被編碼Coded和解碼DECoded的,因此它的名字叫做CODEC。Divx和 MP3就是編解碼器的例子。接著從流中被讀出來的叫做包(Packets)。包是包含了一段可以被解碼成方便我們最後在應用程序中操作的原始幀的 數據。根據我們的目的,每個包包含了完整的幀或者對於音頻來說是許多格式的完整幀。
基本上來說,處理視頻和音頻流是很容易的:
10 從video.avi文件中打開視頻流video_stream
20 從視頻流中讀取包到幀中
30 如果這個幀還不完整,跳到20
40 對這個幀進行一些操作
50 跳回到20
在這個程序中使用ffmpeg來處理多種媒體是相當容易的,雖然很多程序可能在對幀進行操作的時候非常的復雜。因此在這篇指導中,我們將打開一個文件,讀取裡面的視頻流,而且我們對幀的操作將是把這個幀寫到一個PPM文件中。打開文件
首先,來看一下我們如何打開一個文件。通過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()中來調用它。如果你喜歡,也可以只注冊特定的格式和編解碼器,但是通常你沒有必要這樣做。
現在我們可以真正的打開文件:
AVFormatContext *pFormatCtx;
// Open video file
if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
return -1; // Couldn't open file
我們通過第一個參數來獲得文件名。這個函數讀取文件的頭部並且把信息保存到我們給的AVFormatContext結構體中。最後三個參數用來指定特殊的文件格式,
緩沖大小和格式參數,但如果把它們設置為空NULL或者0,libavformat將自動檢測這些參數。
這個函數只是檢測了文件的頭部,所以接著我們需要檢查在文件中的流的信息:
// Retrieve stream information
if(av_find_stream_info(pFormatCtx)<0)
return -1; // Couldn't find stream information
這個函數為pFormatCtx->streams填充上正確的信息。我們引進一個手工調試的函數來看一下裡面有什麼:
// Dump information about file onto standard error
dump_format(pFormatCtx, 0, argv[1], 0);
現在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->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"(編解碼器上下文)的東西。這裡面包含了流中所使用的關於編解碼器的所有信息,現在我們有了一個
指向他的指針。但是我們必需要找到真正的編解碼器並且打開它:
AVCodec *pCodec;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!/n");
return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec)<0)
return -1; // Could not open codec
有些人可能會從舊的指導中記得有兩個關於這些代碼其它部分:添加CODEC_FLAG_TRUNCATED到pCodecCtx->flags和添 加一個hack來粗糙的修正幀率。這兩個修正已經不在存在於ffplay.c中。因此,我必需假設它們不再必要。我們移除了那些代碼後還有一個需要指出的 不同點:pCodecCtx->time_base現在已經保存了幀率的信息。time_base是一個結構體,它裡面有一個分子和分母 (AVRational)。我們使用分數的方式來表示幀率是因為很多編解碼器使用非整數的幀率(例如NTSC使用29.97fps)。
保存數據
現在我們需要找到一個地方來保存幀:
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);
最後,我們已經准備好來從流中讀取數據了。
讀取數據
我們將要做的是通過讀取包來讀取整個視頻流,然後把它解碼成幀,最好後轉換格式並且保存。
int frameFinished;
AVPacket packet;
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
// 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,
(AVPicture*)pFrame, pCodecCtx->pix_fmt,
pCodecCtx->width, pCodecCtx->height);
注:上面的函數已經不再支持,下面給出新的API函數使用,後面的教程,不再說明
struct SwsContext *ctx;
ctx = sws_getContext(
pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtx->width,
pCodecCtx->height,
PIX_FMT_RGB24,
SWS_BICUBIC, NULL, NULL, NULL);
if (ctx == NULL)
{
fprintf(stderr, "Cannot get resampling context/n");
exit(1);
}
sws_scale( ctx,pFrameRGB->data,pFrameRGB->linesize,0,pCodecCtx->height,pFrame->data,pFrame->linesize);
// Save the frame to disk
if(++i<=5)
SaveFrame(pFrameRGB, pCodecCtx->width,
pCodecCtx->height, i);
}
}
// 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函數。
關於包Packets的注釋從技術上講一個包可以包含部分幀或者其它的數據,但是ffmpeg的解釋器保證了我們得到的包Packets包含的要麼是完整的要麼是多種完整的幀。【Linux公社 http://www.linuxidc.com 】
現在我們需要做的是讓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文件。可以使用一些電影文件來進行測試。