2010年1月7日

FFMpeg與SDL之播放器~心得 - 1

FFMpeg是Linux上知名的影音編解碼函式庫。
有多知名呢?這樣描述吧~Linux上播放程式並不多,但有數的播放程式幾乎都靠它進行解碼,就算這類播放程式有自帶解碼器或介面,也都會支援FFMpeg。

在這篇開始之前我必須要說清楚,FFMpeg是有軟體專利問題的,理論上無法用於商業開發,一旦被軟體專利的組織查到,是要花大錢的。
我在一年前左右就開始看FFMpeg,當時就是因為看到軟體專利問題,就停止了,這次會繼續的原因主要是:
1. FFMpeg是Linux上主要的編解碼函式庫,除了它別無選擇,Google Chrome的影音播放器目前也是以它為基礎,連Google Browser都用它,我只能相信在一般使用上,軟體專利問題應該不很大。
2. FFMpeg本身其實只是個編解碼函式庫的介面,意思是它在編譯時,可以選擇要編譯的編碼器和解碼器,我認為在商業化使用時,應該可以把有問題的編解碼器全部拿掉,只留下沒問題的(theora/ogg),如此一來對於自主的播放器來說,沒有軟體專利的問題,但程式又不需要更動即可使用。
3. 現在新版的FFMpeg有提供Nvidia的VDAPU的支援,可以直接用VDAPU進行H264的解碼,我一廂情願的認為,Nvidia的VDAPU應該已經有付過相關解碼器的專利費用了,所以用FFMpeg的VDAPU對影片解碼應該是沒軟體專利的問題。

因為上述的幾個想法,所以決定繼續FFMpeg和SDL的學習。

FFMpeg和SDL的播放器,最主要的學習是以An ffmpeg and SDL Tutorial - ffmpeg tutorial為主,不幸的是,隨著2010年跨年,跨完年它網站資料就不見了...@_@!
好加在它html雖然都不見了,但txt和source code倒是到還在,連結如下:
Tutorial 01文字版 Tutorial 01原始碼
Tutorial 02文字版 Tutorial 02原始碼
Tutorial 03文字版 Tutorial 03原始碼
Tutorial 04文字版 Tutorial 04原始碼
Tutorial 05文字版 Tutorial 05原始碼
Tutorial 06文字版 Tutorial 06原始碼
Tutorial 07文字版 Tutorial 07原始碼
Tutorial 08文字版 Tutorial 08原始碼

最後,有一點必須要說的,整篇FFMpeg與SDL的教學,是基礎在ffplay這個播放器,我按照他的教學寫了2週,在EeePC上播放都有問題,一執行CPU就滿載且播放一陣子程式就卡住,必須要強制關閉,結果搞半天,我用ffplay也遇到一樣的問題,所以測試上有問題,目前我打算改看mplayer,看播放器的設計上有何不同,來瞭解ffplay為啥在EeePC上執行會有這樣的問題。
前言結束。

第一篇:
本篇的教學在理解之後其實很簡單,目前網路上找得到的FFMpeg範例,程式碼幾乎都類似這篇的內容,如果只是想要進行影片的轉檔(不含聲音),那麼本篇足矣。

再次重申,FFMpeg是影音的編碼/解碼函式庫,因此它的主要工作都是在編碼解碼上,今天我們要寫個播放器,那麼單單只有FFMpeg是不夠的,所以還會需要SDL這樣的函式庫,不過這在第二篇才會細說,這邊僅針對FFMpeg的影片解碼過程進行解說(不含聲音)。

在瞭解FFMpeg的操作前,我們要先瞭解兩樣東西,一個是「檔案讀取流程」,另一個是「FFMpeg解碼處理流程」。
首先我們看到「檔案讀取流程」 。

檔案讀取流程:
char ch;
FILE *fp = fopen("test.txt","r");
while((ch = fgetc( fp )) != EOF)
printf("%c", ch);
fclose(fp);


上述這是一個非常常見的檔案讀取的程式段,它的流程很單純,就是下面三個步驟:


這裡關於流程圖的符號細節我們就不要考究了,菱形、圓弧型之類的會讓圖片一大團。

在FFMpeg也是一樣,FFMpeg操作的大架構事實上和檔案讀取相同,下面是它部份的程式段:
AVFormatContext *VideoFormatInfo;
av_open_input_file(&VideoFormatInfo, "test.mpg", NULL, 0, NULL);
.....
while(av_read_frame(VideoFormatInfo, &VideoPacket) >= 0)
{
影片操作;
.....
}
av_close_input_file(VideoFormatInfo);


操作上大概就上述這樣,不過我把大部分細節都去除了,因為這部份還沒提到。
這裡要讓人瞭解的是,FFMpeg的核心操作,其實就像是檔案讀取一般,只是FFMpeg要使用
1. av_open_input_file() 代換 fopen()
2. av_read_frame() 代換 fread()
3. av_close_input_file() 代換 fclose()

接著我們看到「FFMpeg解碼處理流程」。

FFMpeg解碼處理流程:
要瞭解FFMpeg的對影音檔的處理流程,基本上流程如下圖:


幾乎所有的播放程式,解碼的處理流程都像上圖這樣。

上述圖片中,關於聲音處理的部份都以半透明方式表示,原因在於,目前這篇文章僅討論影片處理,並沒有處理聲音部份,因此聲音部份並沒有在下面的程式碼和說明中。

上述的流程相對應到FFMpeg,會類似下面這樣的程式段:
.....
1. 解析出影音檔中的串流資訊
av_find_stream_info(VideoFormatInfo);
.....

2. 從影音檔的串流資訊取得影片軌是哪一軌
int VideoStreamIndex = -1;
int i = 0;
for(; i < VideoFormatInfo->nb_streams; i++)
{
if(VideoFormatInfo->streams[i]->codec->codec_type == CODEC_TYPE_VIDEO)
{
VideoStreamIndex = i;
break;
}
}
.....

3. 從影片軌中取得影片的編碼
AVCodecContext *VideoCodecInfo = VideoFormatInfo->streams[VideoStreamIndex]->codec;
.....

4. 根據影片的編碼找出相對應的解碼器
AVCodec *VideoDecoder = avcodec_find_decoder(VideoCodecInfo->codec_id);
.....

5. 開啟解碼器
avcodec_open(VideoCodecInfo, VideoDecoder);
.....

6. 對讀取出來的影片資料進行解碼
avcodec_decode_video(VideoCodecInfo, VideoFrame, &frameFinished, VideoPacket.data, VideoPacket.size);
.....


到此,FFMpeg解碼的主要部份都解說了,接著我們把讀取影音檔的程式段和影片解碼的程式段整合,並且加入一些細節做說明。
#include "libavcodec.h"
#include "libavformat.h"
#include "stdio.h"

int main(int argc, char **argv)
{
//註冊所有FFMpeg的編碼器
av_register_all();

AVFormatContext *VideoFormatInfo;

if( av_open_input_file(&VideoFormatInfo, argv[1], NULL, 0, NULL) !=0 )
return -1; // Couldn't open file

if( av_find_stream_info(VideoFormatInfo)< 0 ) return -1; //顯示 dump_format(VideoFormatInfo, 0, argv[1], 0); AVCodecContext *VideoCodecInfo; int VideoStreamIndex = -1; int i = 0; for(; i < VideoFormatInfo->nb_streams; i++)
{
if(VideoFormatInfo->streams[i]->codec->codec_type == CODEC_TYPE_VIDEO)
{
VideoStreamIndex = i;
break;
}
}
if(VideoStreamIndex == -1)
return -1;

VideoCodecInfo = VideoFormatInfo->streams[VideoStreamIndex]->codec;

AVCodec *VideoDecoder;

VideoDecoder = avcodec_find_decoder(VideoCodecInfo->codec_id);
if(VideoDecoder == NULL)
{
fprintf(stderr, "Unsupported codec!\n");
return -1;
}

if(avcodec_open(VideoCodecInfo, VideoDecoder) < 0) return -1; int frameFinished; AVPacket VideoPacket; i = 0; while(av_read_frame(VideoFormatInfo, &VideoPacket) >= 0)
{
if(VideoPacket.stream_index == VideoStreamIndex)
{
avcodec_decode_video(VideoCodecInfo, VideoFrame, &frameFinished, VideoPacket.data, VideoPacket.size);

if(frameFinished)
{
//影片檔圖片讀取完成,進行圖片操作或影像處理
}
}
av_free_packet(&VideoPacket);
}

av_free(VideoFrame);
avcodec_close(VideoCodecInfo);
av_close_input_file(VideoFormatInfo);

return 0;
}


上述的程式碼比之前2段程式段都更複雜些,但相似度非常高,沒加入的部份很少,這裡會對沒加入的部份進行說明。
這個程式碼應該算是可以動作的,但它並不會對取出的影片圖片進行處理,只是單純的解碼而已,更完整的部份再接著才會進行。
在這個程式碼中,有幾個前面沒提到的部份。
首先:
av_register_all();
這個function是用來告訴FFMpeg,我們要註冊所有的FFMpeg解碼器,註冊之後,我們後面的解碼器操作才能順利進行。

接著:
while(av_read_frame(VideoFormatInfo, &VideoPacket) >= 0) //跟讀取檔案方式相同,只要影片還有 Frame,就一直讀,av_read_frame會將讀到的 Frame 放入 AVPacket結構 中
{
// Is this a packet from the video stream?(這個 packet 是從 video stream 來的?)
if(VideoPacket.stream_index == VideoStreamIndex) //從已經讀到的 AVPacket結構 中,判斷這個 Packet 是哪個 stream 的,在這裡我們只需要 video stream,因此這行判斷
{
}
}

在使用av_read_frame()遞迴的讀取時,因為讀取到的影音資料是影片、聲音混合的,所以我們在讀取後要使用 if(VideoPacket.stream_index == VideoStreamIndex) {} 來判斷影片或聲音,判斷出影片後才能進一步操作,聲音在此則不管它。

接著:
if(frameFinished)
{
//影片檔圖片讀取完成,進行圖片操作或影像處理
}

這裡這段想必會很疑惑,frameFinish用途何在。
其實FFMpeg使用 av_read_frame() 讀出時,讀出的單位並不是一張圖片,而是影音的 Packet(封包),因為一部分的「Packet」並不能表示成一張「畫面」,因此我們在使用 avcodec_decode_video() 進行影片解碼時,要傳入 frameFinish,讓FFMpeg透過 frameFinish 告訴我們是否完成了一張完整的圖片解碼,當完成了一張圖片解碼時,if(frameFinished) {} 才會符合,我們也才能對這個解出的圖片(畫面)進行進一步的處理。

為了讓這個程式有意義,而不是虛無飄渺的甚麼東西都沒有,我們加上最後一個部份,把解碼出來的畫面進行輸出,整個程式可以把影片的最前面5張畫面輸出成5個圖片檔。

首先加入下面這段程式碼:
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; ydata[0]+y*pFrame->linesize[0], 1, width*3, pFile);

// Close file
fclose(pFile);
}


這段程式碼功能很簡單,將讀取出來的畫面(圖片),以 fwrite() 的方式寫入到檔案中,就成為了 ppm 格式的圖片檔。

為了要能使用 SaveFrame(),我們還需要改寫剛剛的程式碼。
.....
if(avcodec_open(VideoCodecInfo, VideoDecoder) < 0) return -1; //針對 SaveFrame() 新加程式段 開始 AVFrame *VideoFrame; AVFrame *VideoFrameRGB; VideoFrame = avcodec_alloc_frame(); VideoFrameRGB = avcodec_alloc_frame(); if(VideoFrameRGB == NULL) return -1; uint8_t *VideoBuffer; int numBytes; numBytes = avpicture_get_size(PIX_FMT_RGB24, VideoCodecInfo->width, VideoCodecInfo->height);
VideoBuffer = (uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

avpicture_fill((AVPicture *)VideoFrameRGB, VideoBuffer, PIX_FMT_RGB24, VideoCodecInfo->width, VideoCodecInfo->height);
//新增 結束

int frameFinished;
AVPacket VideoPacket;
i = 0;
while(av_read_frame(VideoFormatInfo, &VideoPacket) >= 0)
{
.....
if(frameFinished)
{
//針對 SaveFrame() 新加程式段 開始
if(new_img_convert == NULL)
{
new_img_convert = sws_getContext(VideoCodecInfo->width, VideoCodecInfo->height, VideoCodecInfo->pix_fmt, VideoCodecInfo->width, VideoCodecInfo->height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);
}
if(new_img_convert == NULL)
{
fprintf(stderr, "Cannot initialize the conversion context!\n");
exit(1);
}
sws_scale(new_img_convert, VideoFrame->data, VideoFrame->linesize, 0, VideoCodecInfo->height, VideoFrameRGB->data, VideoFrameRGB->linesize);

// 將 frame 存入檔案中
if(++i <= 5) { SaveFrame(VideoFrameRGB, VideoCodecInfo->width, VideoCodecInfo->height, i);
}
}
.....

最後,下面這個連結就是完整的程式碼檔案。
ffmpeg_1x.c